Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,28 @@ dependencies {
// Spring Boot dependencies
implementation(local.springboot.starter)
implementation(local.springboot.starter.validation)
implementation(local.springboot.starter.web)
implementation(local.springboot.starter.webmvc)

// Spring Boot Liquibase dependency for database migrations
implementation(local.springboot.starter.liquibase)

// H2 database dependency for in-memory database
runtimeOnly(local.h2database)

// FasterXML Jackson support for Java 8 date/time
implementation(local.jackson.datatype.jsr310)

// Liquibase core dependency for database migrations
runtimeOnly(local.liquibase.core)

// PostgreSQL database driver
runtimeOnly(local.postgres)

// Springdoc OpenAPI for providing Swagger documentation
implementation(local.springdoc.openapi.starter.webmvc)

// Spring Boot and Testcontainers test dependencies
testImplementation(local.springboot.resttestclient)
testImplementation(local.springboot.starter.test)
testImplementation(local.springboot.testcontainers)
testImplementation(local.testcontainers.junit.jupiter)
testImplementation(local.testcontainers.postgresql)

// JUnit platform launcher dependency for running JUnit tests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.github.thorlauridsen;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.client.RestTestClient;

/**
* This is the BaseMockMvc class that allows you to send and test HTTP requests.
*/
@AutoConfigureRestTestClient
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BaseControllerTest {

@Autowired
private RestTestClient restTestClient;

/**
* Test an HTTP GET request.
*
* @param getUrl the URL to send an HTTP GET request to.
* @return {@link RestTestClient.ResponseSpec} response.
*/
public RestTestClient.ResponseSpec get(String getUrl) {
return restTestClient.get()
.uri(getUrl)
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.exchange();
}

/**
* Test an HTTP POST request.
*
* @param postUrl the URL to send an HTTP POST request to.
* @param jsonBody the JSON body to send with the request.
* @return {@link RestTestClient.ResponseSpec} response.
*/
public RestTestClient.ResponseSpec post(String postUrl, String jsonBody) {
return restTestClient.post()
.uri(postUrl)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.body(jsonBody)
.exchange();
}
}
54 changes: 0 additions & 54 deletions apps/api/src/test/java/com/github/thorlauridsen/BaseMockMvc.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.thorlauridsen;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.thorlauridsen.dto.CustomerDto;
import com.github.thorlauridsen.dto.CustomerInputDto;
import com.github.thorlauridsen.dto.ErrorDto;
Expand All @@ -10,10 +9,13 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import tools.jackson.databind.json.JsonMapper;

import static com.github.thorlauridsen.controller.BaseEndpoint.CUSTOMER_BASE_ENDPOINT;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand All @@ -26,74 +28,71 @@
* A local Docker instance is required to run the tests as Testcontainers is used.
*/
@ActiveProfiles("postgres")
@Import(TestContainerConfig.class)
class CustomerControllerTest extends BaseMockMvc {
@Testcontainers
class CustomerControllerTest extends BaseControllerTest {

@Autowired
private ObjectMapper objectMapper;
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18");

@Autowired
public CustomerControllerTest(MockMvc mockMvc) {
super(mockMvc);
}
private JsonMapper jsonMapper;

@Test
void getCustomer_randomId_returnsNotFound() throws Exception {
void getCustomer_randomId_returnsNotFound() {
val id = UUID.randomUUID();
val response = mockGet(CUSTOMER_BASE_ENDPOINT + "/" + id);
assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatus());
val response = get(CUSTOMER_BASE_ENDPOINT + "/" + id);

response.expectStatus().isEqualTo(HttpStatus.NOT_FOUND);
}

@ParameterizedTest
@ValueSource(strings = {
"alice@gmail.com",
"bob@gmail.com"
})
void postCustomer_getCustomer_success(String mail) throws Exception {
void postCustomer_getCustomer_success(String mail) {
val customer = new CustomerInputDto(mail);
val json = objectMapper.writeValueAsString(customer);
val response = mockPost(json, CUSTOMER_BASE_ENDPOINT);
assertEquals(HttpStatus.CREATED.value(), response.getStatus());
val json = jsonMapper.writeValueAsString(customer);
val response = post(CUSTOMER_BASE_ENDPOINT, json);
response.expectStatus().isEqualTo(HttpStatus.CREATED);

val responseJson = response.getContentAsString();
val createdCustomer = objectMapper.readValue(responseJson, CustomerDto.class);
val createdCustomer = response.expectBody(CustomerDto.class).returnResult().getResponseBody();
assertNotNull(createdCustomer);
assertCustomer(createdCustomer, mail);

val response2 = mockGet(CUSTOMER_BASE_ENDPOINT + "/" + createdCustomer.id());
assertEquals(HttpStatus.OK.value(), response2.getStatus());
val response2 = get(CUSTOMER_BASE_ENDPOINT + "/" + createdCustomer.id());
response2.expectStatus().isEqualTo(HttpStatus.OK);

val responseJson2 = response2.getContentAsString();
val fetchedCustomer = objectMapper.readValue(responseJson2, CustomerDto.class);
val fetchedCustomer = response2.expectBody(CustomerDto.class).returnResult().getResponseBody();
assertCustomer(fetchedCustomer, mail);
}

@Test
void postCustomer_blankEmail_returnsBadRequest() throws Exception {
void postCustomer_blankEmail_returnsBadRequest() {
val customer = new CustomerInputDto("");
val json = objectMapper.writeValueAsString(customer);
val response = mockPost(json, CUSTOMER_BASE_ENDPOINT);
val json = jsonMapper.writeValueAsString(customer);
val response = post(CUSTOMER_BASE_ENDPOINT, json);

assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus());

val responseJson = response.getContentAsString();
val error = objectMapper.readValue(responseJson, ErrorDto.class);
response.expectStatus().isEqualTo(HttpStatus.BAD_REQUEST);
val error = response.expectBody(ErrorDto.class).returnResult().getResponseBody();

assertNotNull(error);
assertEquals("Validation failed", error.description());
assertTrue(error.fieldErrors().containsKey("mail"));
assertEquals("Email is required", error.fieldErrors().get("mail"));
}

@Test
void postCustomer_invalidEmailFormat_returnsBadRequest() throws Exception {
void postCustomer_invalidEmailFormat_returnsBadRequest() {
val customer = new CustomerInputDto("invalid-email");
val json = objectMapper.writeValueAsString(customer);
val response = mockPost(json, CUSTOMER_BASE_ENDPOINT);

assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus());
val json = jsonMapper.writeValueAsString(customer);
val response = post(CUSTOMER_BASE_ENDPOINT, json);

val responseJson = response.getContentAsString();
val error = objectMapper.readValue(responseJson, ErrorDto.class);
response.expectStatus().isEqualTo(HttpStatus.BAD_REQUEST);
val error = response.expectBody(ErrorDto.class).returnResult().getResponseBody();

assertNotNull(error);
assertEquals("Validation failed", error.description());
assertTrue(error.fieldErrors().containsKey("mail"));
assertEquals("Invalid email format", error.fieldErrors().get("mail"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand All @@ -22,9 +27,15 @@
* This class uses the @SpringBootTest annotation to spin up a Spring Boot instance.
* This ensures that Spring can automatically inject {@link CustomerService} with a {@link CustomerRepo}
*/
@ActiveProfiles("postgres")
@SpringBootTest
@Testcontainers
class CustomerServiceTest {

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18");

@Autowired
private CustomerService customerService;

Expand Down

This file was deleted.

15 changes: 7 additions & 8 deletions gradle/local.versions.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
[versions]
h2database = "2.4.240"
jackson = "2.20.1"
junitPlatformLauncher = "1.12.2"
liquibase = "5.0.1"
junitPlatformLauncher = "6.0.1"
lombok = "9.1.0"
postgres = "42.7.8"
springboot = "3.5.8"
springboot = "4.0.0"
springDependencyPlugin = "1.1.7"
springdoc = "2.8.14"
springdoc = "3.0.0"
testcontainers = "1.21.3"

[libraries]
Expand All @@ -20,21 +19,21 @@ jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-dat
# JUnit platform launcher for running JUnit tests
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitPlatformLauncher" }

# Liquibase for managing database changelogs
liquibase-core = { module = "org.liquibase:liquibase-core", version.ref = "liquibase" }

# PostgreSQL for a live database
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }

# Spring Boot libraries
springboot-resttestclient = { module = 'org.springframework.boot:spring-boot-resttestclient', version.ref = "springboot" }
springboot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot" }
springboot-starter-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "springboot" }
springboot-starter-liquibase = { module = "org.springframework.boot:spring-boot-starter-liquibase", version.ref = "springboot" }
springboot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "springboot" }
springboot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "springboot" }
springboot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springboot" }
springboot-starter-webmvc = { module = "org.springframework.boot:spring-boot-starter-webmvc", version.ref = "springboot" }
springboot-testcontainers = { module = "org.springframework.boot:spring-boot-testcontainers", version.ref = "springboot" }

# Testcontainers for running PostgreSQL in tests
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" }
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }

# Springdoc provides swagger docs with support for Spring Web MVC
Expand Down