Skip to content

Commit

Permalink
add integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Stefan Wilke committed Nov 24, 2023
1 parent 76a4fae commit c2fc790
Show file tree
Hide file tree
Showing 12 changed files with 641 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
java-version: '17'

- name: Build with Maven
run: mvn clean package
run: mvn clean verify

release:
runs-on: ubuntu-latest
Expand Down
16 changes: 16 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -107,6 +111,18 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,35 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

import static com.github.binpastes.paste.api.model.ListView.ListItemView;

@Validated
@RestController
@RequestMapping("/api/v1/paste")
@RequestMapping({"/api/v1/paste", "/api/v1/paste/"})
class PasteController {

private static final Logger log = LoggerFactory.getLogger(PasteController.class);
Expand All @@ -49,23 +60,26 @@ public Mono<SingleView> findPaste(@PathVariable("pasteId") String pasteId, Serve
.find(pasteId)
.doOnNext(paste -> {
if (paste.isOneTime()) {
response.getHeaders().add(HttpHeaders.CACHE_CONTROL, "no-store");
} else {
if (!paste.isPermanent()) {
var in5min = LocalDateTime.now().plusMinutes(5);
if (in5min.isAfter(paste.getDateOfExpiry())) {
response.getHeaders().add(HttpHeaders.CACHE_CONTROL, "max-age=" + Duration.between(LocalDateTime.now(), paste.getDateOfExpiry()).toSeconds());
return;
}
}

response.getHeaders().add(HttpHeaders.CACHE_CONTROL, "max-age=300");
response.getHeaders().setCacheControl(CacheControl.noStore());
return;
}

if (paste.isPermanent() || isAfter(paste.getDateOfExpiry(), 5, ChronoUnit.MINUTES)) {
response.getHeaders().setCacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES));
return;
}

response.getHeaders().setCacheControl(
CacheControl.maxAge(Duration.between(LocalDateTime.now(), paste.getDateOfExpiry())));
})
.map(reference -> SingleView.of(reference, remoteAddress(request)))
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)));
}

private static boolean isAfter(LocalDateTime dateTime, long amount, ChronoUnit unit) {
return LocalDateTime.now().plus(amount, unit).isBefore(dateTime);
}

@GetMapping
public Mono<ListView> findPastes() {
return pasteService
Expand All @@ -83,7 +97,7 @@ public Mono<SearchView> searchPastes(
final String term,
final ServerHttpResponse response
) {
response.getHeaders().add(HttpHeaders.CACHE_CONTROL, "max-age=60");
response.getHeaders().setCacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS));
return pasteService
.findByFullText(term)
.map(paste -> SearchItemView.of(paste, term))
Expand Down Expand Up @@ -129,5 +143,4 @@ private static String remoteAddress(ServerHttpRequest request) {

return request.getRemoteAddress().getAddress().getHostAddress();
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.binpastes.paste.business.tracking;

import com.github.binpastes.paste.business.tracking.MessagingClient.Message;
import com.github.binpastes.paste.domain.PasteRepository;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
Expand Down Expand Up @@ -35,7 +34,7 @@ public TrackingService(
private void run() {
this.messagingClient
.receiveMessage()
.doOnNext(this::receiveView)
.doOnNext(message -> receiveView(message.pasteId(), message.timeViewed()))
.repeat()
.subscribe();
}
Expand All @@ -55,8 +54,4 @@ public void receiveView(String pasteId, Instant timeViewed) {
.onErrorResume(OptimisticLockingFailureException.class, e -> Mono.empty())
.subscribe();
}

private void receiveView(Message message) {
this.receiveView(message.pasteId(), message.timeViewed());
}
}
19 changes: 12 additions & 7 deletions backend/src/main/java/com/github/binpastes/paste/domain/Paste.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Objects;

import static com.github.binpastes.paste.domain.Paste.PasteSchema;
import static java.util.Objects.isNull;

@Table(PasteSchema.TABLE_NAME)
public class Paste {
Expand Down Expand Up @@ -123,31 +124,35 @@ public boolean isPermanent() {
}

public boolean isErasable(String remoteAddress) {
if (this.isUnlisted() || this.isOneTime()) {
if (isUnlisted() || isOneTime()) {
return true;
}

if (this.isPublic()) {
final var createdBySameAuthor = Objects.equals(remoteAddress, this.getRemoteAddress());
if (isPublic()) {
final var createdBySameAuthor = Objects.equals(remoteAddress, getRemoteAddress());

if (createdBySameAuthor) {
return LocalDateTime.now().minusHours(1).isBefore(this.getDateCreated());
return LocalDateTime.now().minusHours(1).isBefore(getDateCreated());
}
}

return false;
}

public Paste trackView(LocalDateTime lastViewed) {
if (this.getLastViewed() == null || this.getLastViewed().isBefore(lastViewed)) {
if (getLastViewed() == null || getLastViewed().isBefore(lastViewed)) {
setLastViewed(lastViewed);
}

return this.setViews(this.getViews() + 1);
return setViews(this.getViews() + 1);
}

public Paste markAsExpired() {
return this.setDateOfExpiry(LocalDateTime.now());
if (isNull(getDateOfExpiry())) {
return setDateOfExpiry(LocalDateTime.now());
}

throw new IllegalStateException("Paste is already expired");
}

protected Paste setId(final String id) {
Expand Down
12 changes: 0 additions & 12 deletions backend/src/test/java/com/github/binpastes/BinPastesTests.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.github.binpastes.paste.api;

import com.github.binpastes.paste.domain.Paste;
import com.github.binpastes.paste.domain.Paste.PasteExposure;
import com.github.binpastes.paste.domain.PasteRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.CacheControl;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.reactive.server.WebTestClient;

import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@AutoConfigureWebTestClient
@DirtiesContext
class OneTimePastesIT {

@Autowired
private WebTestClient webClient;

@Autowired
private PasteRepository pasteRepository;

@BeforeEach
void setUp() {
pasteRepository.deleteAll().block();
}

@Test
@DisplayName("GET /{pasteId} - one-time paste is never cached")
void getOneTimePaste() {
var oneTimePaste = givenOneTimePaste();

webClient.get()
.uri("/api/v1/paste/" + oneTimePaste.getId())
.exchange()
.expectStatus().isOk()
.expectHeader().cacheControl(CacheControl.noStore());
}

@Test
@DisplayName("GET /{pasteId} - one-time paste is read-once")
void getOneTimePasteTwice() {
var oneTimePaste = givenOneTimePaste();

webClient.get()
.uri("/api/v1/paste/" + oneTimePaste.getId())
.exchange()
.expectStatus().isOk();

webClient.get()
.uri("/api/v1/paste/" + oneTimePaste.getId())
.exchange()
.expectStatus().isNotFound();
}

@Test
@DisplayName("GET / - one-time paste is never listed")
void findAllPastes() {
givenOneTimePaste();

assertThat(pasteRepository.count().block()).isOne();
webClient.get()
.uri("/api/v1/paste/")
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("pastes", emptyList());
}

@Test
@DisplayName("GET /search - one-time paste cannot be searched for")
void searchAllPastes() {
givenOneTimePaste();

assertThat(pasteRepository.count().block()).isOne();
webClient.get()
.uri("/api/v1/paste/search?term={term}", "ipsum")
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("pastes", emptyList());
}

@Test
@DisplayName("DELETE /{pasteId} - one-time paste might always be deleted before reading")
void deleteOneTimePaste() {
var oneTimePaste = givenOneTimePaste();

webClient.delete()
.uri("/api/v1/paste/" + oneTimePaste.getId())
.exchange()
.expectStatus().isNoContent()
.expectBody().isEmpty();

webClient.get()
.uri("/api/v1/paste/" + oneTimePaste.getId())
.exchange()
.expectStatus().isNotFound();
}

private Paste givenOneTimePaste() {
return givenOneTimePaste(
Paste.newInstance(
"someTitle",
"Lorem ipsum dolor sit amet",
null,
false,
PasteExposure.ONCE,
"1.1.1.1"
)
);
}

private Paste givenOneTimePaste(Paste paste) {
return pasteRepository.save(paste).block();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
Expand Down Expand Up @@ -45,7 +44,7 @@ void findUnknownPaste() {
.uri("/api/v1/paste/" + somePasteId)
.exchange()
.expectStatus().isNotFound()
.expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL);
.expectHeader().cacheControl(CacheControl.empty());
}

@Test
Expand All @@ -69,7 +68,7 @@ void searchPastes() {
.uri("/api/v1/paste/search?term={term}", "foobar")
.exchange()
.expectStatus().isOk()
.expectHeader().cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
.expectHeader().cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES))
.expectBody().jsonPath("pastes", emptyList());
}

Expand All @@ -92,20 +91,14 @@ void createPaste(Mono<String> payload) {
.contentType(MediaType.APPLICATION_JSON)
.body(payload, String.class)
.exchange()
.expectStatus().isBadRequest()
.expectBody().consumeWith(System.out::println);
.expectStatus().isBadRequest();
}

private static Stream<Arguments> invalidPayloads() {
return Stream.of(
arguments(named("body is null", Mono.empty())),
arguments(named("body blank", Mono.just(""))),
arguments(named("title too long", Mono.just("""
{
"title": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"content": "validContent"
}
"""))),
arguments(named("title too long", Mono.just("{\"content\": \"validContent\", \"title\": " + "X".repeat(256 + 1) + "\"}"))),
arguments(named("content blank", Mono.just("""
{
"content": " ",
Expand Down
Loading

0 comments on commit c2fc790

Please sign in to comment.