Skip to content

Commit

Permalink
add tests to Paste domain object and controller
Browse files Browse the repository at this point in the history
  • Loading branch information
Stefan Wilke committed Nov 13, 2023
1 parent f68cda2 commit b325139
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 15 deletions.
4 changes: 0 additions & 4 deletions backend/src/main/java/com/github/binpastes/BinPastes.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.r2dbc.config.EnableR2dbcAuditing;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableR2dbcRepositories
@EnableR2dbcAuditing
@EnableScheduling
public class BinPastes {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.binpastes.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.EnableR2dbcAuditing;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;

@Configuration
@EnableR2dbcRepositories
@EnableR2dbcAuditing
public class DatabaseConfig {}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public Mono<SingleView> findPaste(@PathVariable("pasteId") String pasteId, Serve
if (paste.isOneTime()) {
response.getHeaders().add(HttpHeaders.CACHE_CONTROL, "no-store");
} else {
if (paste.getDateOfExpiry() != null ) {
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ public static String highlight(final String content, final String term) {
final int idx = content.indexOf(term);

if (idx == -1) {
return content.substring(0, Math.min(2 * HIGHLIGHT_RANGE, content.length()));
return content.substring(0, Math.min(2 * HIGHLIGHT_RANGE, content.length())).trim();
}

return content.substring(
Math.max(0, idx - HIGHLIGHT_RANGE),
Math.min(content.length(), idx + term.length() + HIGHLIGHT_RANGE)
);
).trim();
}
}
}
19 changes: 15 additions & 4 deletions backend/src/main/java/com/github/binpastes/paste/domain/Paste.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import org.springframework.data.relational.core.mapping.Table;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Objects;

import static com.github.binpastes.paste.domain.Paste.PasteSchema;
Expand Down Expand Up @@ -116,19 +115,23 @@ public boolean isUnlisted() {
}

public boolean isOneTime() {
return exposure == PasteExposure.ONCE;
return this.exposure == PasteExposure.ONCE;
}

public boolean isPermanent() {
return this.dateOfExpiry == null;
}

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

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

if (createdBySameAuthor) {
return ChronoUnit.MINUTES.between(this.getDateCreated(), LocalDateTime.now()) <= 60L;
return this.getDateCreated().isAfter(LocalDateTime.now().minusHours(1));
}
}

Expand Down Expand Up @@ -216,6 +219,14 @@ public int hashCode() {
return Objects.hash(id);
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Paste{");
sb.append("id='").append(id).append('\'');
sb.append('}');
return sb.toString();
}

public enum PasteExposure {
PUBLIC,
UNLISTED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;

@Repository
public interface PasteRepository extends R2dbcRepository<Paste, String>, ReactiveQueryByExampleExecutor<Paste>, PasteRepositoryCustom {

/**
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ server.address=localhost
server.port=8080

server.http2.enabled=true
server.error.whitelabel.enabled=false
server.compression.enabled=true
server.error.whitelabel.enabled=false

spring.jmx.enabled=false
spring.jpa.open-in-view=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
class BinPastesTests {

@Test
void contextLoads() {
}
void contextLoads() {}

}
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.PasteService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
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;
import reactor.core.publisher.Mono;

import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import static java.util.Collections.emptyList;
import static org.junit.jupiter.api.Named.named;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;

@WebFluxTest
class PasteControllerTest {

@Autowired
private WebTestClient webClient;

@MockBean
private PasteService pasteService;

private static final String somePasteId = "4711471147114711471147114711471147114711";

@Test
@DisplayName("GET /{pasteId} - 404 on unknown paste, no caching")
void findUnknownPaste() {
doReturn(Mono.empty()).when(pasteService).find(anyString());

webClient.get()
.uri("/api/v1/paste/" + somePasteId)
.exchange()
.expectStatus().isNotFound()
.expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL);
}

@Test
@DisplayName("GET / - empty list on no results")
void listPastes() {
doReturn(Flux.empty()).when(pasteService).findAll();

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

@Test
@DisplayName("GET /search - set cache header")
void searchPastes() {
doReturn(Flux.empty()).when(pasteService).findByFullText(anyString());

webClient.get()
.uri("/api/v1/paste/search?term=/{term}", "foobar")
.exchange()
.expectStatus().isOk()
.expectHeader().cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
.expectBody().jsonPath("pastes", emptyList());
}

@Test
@DisplayName("DELETE /{pasteId} - always return 204")
void deletePaste() {
webClient.delete()
.uri("/api/v1/paste/" + somePasteId)
.exchange()
.expectStatus().isNoContent()
.expectBody().isEmpty();
}

@ParameterizedTest
@DisplayName("POST / - 400 on invalid input")
@MethodSource("invalidPayloads")
void createPaste(Mono<String> payload) {
webClient.post()
.uri("/api/v1/paste")
.contentType(MediaType.APPLICATION_JSON)
.body(payload, String.class)
.exchange()
.expectStatus().isBadRequest()
.expectBody().consumeWith(System.out::println);
}

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("content blank", Mono.just("""
{
"content": " ",
}
"""))),
arguments(named("content too short", Mono.just("""
{
"content": "1234",
}
"""))),
arguments(named("content too long", Mono.just("{\"content\": \"" + "X".repeat(4096 + 1) + "\"}")))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.github.binpastes.paste.api.model;

import com.github.binpastes.paste.api.model.SearchView.SearchItemView;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.assertj.core.api.Assertions.assertThat;

class SearchItemViewTest {

private static final String content = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur egestas odio faucibus mi commodo faucibus. Mauris pellentesque vitae urna sed vehicula. Mauris sollicitudin rutrum condimentum. Sed vel cursus neque, vel imperdiet justo. Integer et volutpat erat, at ullamcorper nisi. Praesent viverra interdum ex, eget scelerisque augue. Nunc sagittis libero quis tristique rutrum. Vestibulum dapibus ex vel auctor mattis. Donec vel vulputate sem, at posuere magna. Curabitur sodales condimentum erat, et pellentesque est viverra quis.
""".trim();

@ParameterizedTest
@DisplayName("highlight - extract excerpt from content")
@CsvSource(delimiter = '|', textBlock = """
'elit' | 'amet, consectetur adipiscing elit. Curabitur egestas odio fauci'
'foobar' | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cur'
""")
void highlight(String term, String expectedHighlight) {

assertThat(SearchItemView.highlight(content, term)).isEqualTo(expectedHighlight);

}
}
106 changes: 106 additions & 0 deletions backend/src/test/java/com/github/binpastes/paste/domain/PasteTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.github.binpastes.paste.domain;

import com.github.binpastes.paste.domain.Paste.PasteExposure;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.time.LocalDateTime;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.params.provider.Arguments.arguments;

class PasteTest {

@Test
@DisplayName("new paste - requires mandatory fields")
void newInstanceMandatoryFields() {
assertThrows(
NullPointerException.class,
() -> Paste.newInstance(null, null, null, false, PasteExposure.PUBLIC, null),
"content is mandatory"
);

assertThrows(
NullPointerException.class,
() -> Paste.newInstance(null, "someContent", null, false, null, null),
"exposure is mandatory"
);
}

@Test
@DisplayName("new paste - sets defaults")
void newInstanceDefaults() {
var newPaste = Paste.newInstance(null, "someContent", null, false, PasteExposure.PUBLIC, null);

assertThat(newPaste.getId()).isNotEmpty();
assertThat(newPaste.getViews()).isZero();
assertThat(newPaste.getLastViewed()).isNull();

assertThat(newPaste.getTitle()).isNull();
assertThat(newPaste.getContent()).isEqualTo("someContent");
assertThat(newPaste.isPermanent()).isTrue();
assertThat(newPaste.isEncrypted()).isFalse();
assertThat(newPaste.isPublic()).isTrue();
assertThat(newPaste.getRemoteAddress()).isNull();
}

@Test
@DisplayName("track paste - increases view count")
void trackPasteViewCount() {
var newPaste = Paste.newInstance(null, "someContent", null, false, PasteExposure.PUBLIC, null);

newPaste.trackView(LocalDateTime.now());
newPaste.trackView(LocalDateTime.now());

assertThat(newPaste.getViews()).isEqualTo(2);
}

@Test
@DisplayName("track paste - updates lastViewed timestamp to most recent one")
void trackPasteLastViewed() {
var newPaste = Paste.newInstance(null, "someContent", null, false, PasteExposure.PUBLIC, null);
var now = LocalDateTime.now();
var yesterday = LocalDateTime.now().minusDays(1);

newPaste.trackView(now);
newPaste.trackView(yesterday);

assertThat(newPaste.getLastViewed()).isEqualTo(now);
assertThat(newPaste.getViews()).isEqualTo(2);
}

@ParameterizedTest
@DisplayName("erase paste - allowed only under certain conditions")
@MethodSource("pastesToErase")
void isErasable(Paste paste, String requestedBy, boolean erasable) {
assertThat(paste.isErasable(requestedBy))
.isEqualTo(erasable);
}

private static Stream<Arguments> pastesToErase() {
var unlistedPaste = Paste.newInstance(null, "someContent", null, false, PasteExposure.UNLISTED, "Alice");
var oneTimePaste = Paste.newInstance(null, "someContent", null, false, PasteExposure.ONCE, "Alice");

var publicPasteRecentlyCreatedByAlice = Paste.newInstance(null, "someContent", null, false, PasteExposure.PUBLIC, "Alice");
publicPasteRecentlyCreatedByAlice.setDateCreated(LocalDateTime.now().minusMinutes(59));

var publicPasteCreatedSomeTimeAgoByAlice = Paste.newInstance(null, "someContent", null, false, PasteExposure.PUBLIC, "Alice");
publicPasteCreatedSomeTimeAgoByAlice.setDateCreated(LocalDateTime.now().minusMinutes(60));

return Stream.of(
arguments(unlistedPaste, null, true),
arguments(oneTimePaste, null, true),

arguments(publicPasteRecentlyCreatedByAlice, "Alice", true),
arguments(publicPasteRecentlyCreatedByAlice, "Bob", false),

arguments(publicPasteCreatedSomeTimeAgoByAlice, "Alice", false),
arguments(publicPasteCreatedSomeTimeAgoByAlice, "Bob", false)
);
}
}
1 change: 0 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@
<artifactId>junixsocket-native-common</artifactId>
<version>${junixsocket.version}</version>
</dependency>

</dependencies>
</dependencyManagement>

Expand Down

0 comments on commit b325139

Please sign in to comment.