diff --git a/.woodpecker/maven.yaml b/.woodpecker/maven.yaml index 82461b2..6458761 100644 --- a/.woodpecker/maven.yaml +++ b/.woodpecker/maven.yaml @@ -1,5 +1,5 @@ variables: - &maven_image maven:3.9.6-eclipse-temurin-17-alpine + &maven_image maven:3.9.6-eclipse-temurin-21-alpine steps: - name: build diff --git a/pom.xml b/pom.xml index 3756469..9ae7829 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ ${source.encoding} - 17 + 21 ${java.version} ${java.version} @@ -51,6 +51,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-actuator + @@ -69,6 +73,12 @@ org.springframework.boot spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-webflux + test org.junit.jupiter @@ -109,6 +119,32 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + + **/*Tests.java + + + + + integration-tests + test + + test + + + + **/*Test.java + + + + + diff --git a/src/main/java/eu/ztsh/wymiana/config/NbpProperties.java b/src/main/java/eu/ztsh/wymiana/config/NbpProperties.java new file mode 100644 index 0000000..0ed6c42 --- /dev/null +++ b/src/main/java/eu/ztsh/wymiana/config/NbpProperties.java @@ -0,0 +1,8 @@ +package eu.ztsh.wymiana.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("nbp") +public record NbpProperties(String baseurl) { + +} diff --git a/src/main/java/eu/ztsh/wymiana/config/RestClientConfiguration.java b/src/main/java/eu/ztsh/wymiana/config/RestClientConfiguration.java index 74ee47b..f264dbe 100644 --- a/src/main/java/eu/ztsh/wymiana/config/RestClientConfiguration.java +++ b/src/main/java/eu/ztsh/wymiana/config/RestClientConfiguration.java @@ -8,9 +8,9 @@ import org.springframework.web.client.RestClient; public class RestClientConfiguration { @Bean - public RestClient restClient() { + public RestClient restClient(NbpProperties nbpProperties) { return RestClient.builder() - .baseUrl("http://api.nbp.pl") + .baseUrl(nbpProperties.baseurl()) .defaultHeader("Accept", "application/json") .build(); } diff --git a/src/main/java/eu/ztsh/wymiana/exception/UserNotFoundException.java b/src/main/java/eu/ztsh/wymiana/exception/UserNotFoundException.java new file mode 100644 index 0000000..3cf38c2 --- /dev/null +++ b/src/main/java/eu/ztsh/wymiana/exception/UserNotFoundException.java @@ -0,0 +1,15 @@ +package eu.ztsh.wymiana.exception; + +import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; + +public class UserNotFoundException extends RuntimeException { + + public UserNotFoundException(CurrencyExchangeRequest entity) { + this(entity.pesel()); + } + + public UserNotFoundException(String pesel) { + super("User with PESEL %s not found".formatted(pesel)); + } + +} diff --git a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java index b00f761..401626a 100644 --- a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java +++ b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java @@ -2,6 +2,7 @@ package eu.ztsh.wymiana.service; import eu.ztsh.wymiana.exception.ExchangeFailedException; import eu.ztsh.wymiana.exception.InsufficientFundsException; +import eu.ztsh.wymiana.exception.UserNotFoundException; import eu.ztsh.wymiana.model.Currency; import eu.ztsh.wymiana.model.User; import eu.ztsh.wymiana.validation.InstanceValidator; @@ -49,7 +50,7 @@ public class CurrencyService { user.currencies().putAll(exchanged); return userService.update(user); }) - .orElseThrow(ExchangeFailedException::new); + .orElseThrow(() -> new UserNotFoundException(request)); } private Currency create(String symbol) { diff --git a/src/main/java/eu/ztsh/wymiana/service/UserService.java b/src/main/java/eu/ztsh/wymiana/service/UserService.java index f9e7cde..857bc0f 100644 --- a/src/main/java/eu/ztsh/wymiana/service/UserService.java +++ b/src/main/java/eu/ztsh/wymiana/service/UserService.java @@ -7,10 +7,13 @@ import eu.ztsh.wymiana.util.UserMapper; import eu.ztsh.wymiana.validation.InstanceValidator; import eu.ztsh.wymiana.web.model.UserCreateRequest; import lombok.RequiredArgsConstructor; +import org.hibernate.validator.constraints.pl.PESEL; import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; import java.util.Optional; +@Validated @RequiredArgsConstructor @Service public class UserService { @@ -26,7 +29,7 @@ public class UserService { return UserMapper.entityToPojo(userRepository.save(UserMapper.requestToEntity(request))); } - public Optional get(String pesel) { + public Optional get(@PESEL String pesel) { return userRepository.findById(pesel).map(UserMapper::entityToPojo); } diff --git a/src/main/java/eu/ztsh/wymiana/web/controller/ExchangeController.java b/src/main/java/eu/ztsh/wymiana/web/controller/ExchangeController.java new file mode 100644 index 0000000..fb0ee13 --- /dev/null +++ b/src/main/java/eu/ztsh/wymiana/web/controller/ExchangeController.java @@ -0,0 +1,39 @@ +package eu.ztsh.wymiana.web.controller; + +import eu.ztsh.wymiana.exception.InsufficientFundsException; +import eu.ztsh.wymiana.exception.UserNotFoundException; +import eu.ztsh.wymiana.service.CurrencyService; +import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +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.RestController; + +@RequiredArgsConstructor +@Validated +@RestController +@RequestMapping(path = "/api/exchange", produces = "application/json") +public class ExchangeController { + + private final CurrencyService currencyService; + + @PostMapping + public ResponseEntity exchange(@Valid @RequestBody CurrencyExchangeRequest request) { + try { + return ResponseEntity.status(200).body(currencyService.exchange(request)); + } catch (Exception e) { + var status = switch (e) { + case InsufficientFundsException ignored -> HttpStatus.BAD_REQUEST; + case UserNotFoundException ignored -> HttpStatus.NOT_FOUND; + default -> HttpStatus.INTERNAL_SERVER_ERROR; + }; + return ResponseEntity.status(status).body(e.getMessage()); + } + } + +} diff --git a/src/main/java/eu/ztsh/wymiana/web/controller/UserController.java b/src/main/java/eu/ztsh/wymiana/web/controller/UserController.java new file mode 100644 index 0000000..bca74ce --- /dev/null +++ b/src/main/java/eu/ztsh/wymiana/web/controller/UserController.java @@ -0,0 +1,60 @@ +package eu.ztsh.wymiana.web.controller; + +import eu.ztsh.wymiana.exception.UserAlreadyExistsException; +import eu.ztsh.wymiana.exception.UserNotFoundException; +import eu.ztsh.wymiana.model.User; +import eu.ztsh.wymiana.service.UserService; +import eu.ztsh.wymiana.validation.ValidationFailedException; +import eu.ztsh.wymiana.web.model.UserCreateRequest; +import jakarta.validation.Valid; +import jakarta.validation.ValidationException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +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.RestController; + +@RequiredArgsConstructor +@Validated +@RestController +@RequestMapping(path = "/api/user", produces = "application/json") +public class UserController { + + private final UserService userService; + + @GetMapping("{pesel}") + public ResponseEntity get(@PathVariable("pesel") String pesel) { + try { + return userService.get(pesel) + .map(ResponseEntity::ok) + .orElseThrow(() -> new UserNotFoundException(pesel)); + } catch (Exception e) { + return ResponseEntity.status(switch (e) { + case ValidationException ignore -> 400; + case UserNotFoundException ignore -> 404; + default -> 500; + }).build(); + } + } + + @PostMapping + public ResponseEntity create(@Valid @RequestBody UserCreateRequest request) { + try { + userService.create(request); + } catch (Exception e) { + var status = switch (e) { + case ValidationFailedException ignored -> HttpStatus.BAD_REQUEST; + case UserAlreadyExistsException ignored -> HttpStatus.CONFLICT; + default -> HttpStatus.INTERNAL_SERVER_ERROR; + }; + return ResponseEntity.status(status).body(e.getMessage()); + } + return ResponseEntity.status(204).build(); + } + +} diff --git a/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java b/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java index 6c15a62..bd7b1b4 100644 --- a/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java +++ b/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java @@ -4,12 +4,13 @@ import eu.ztsh.wymiana.validation.Adult; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Builder; +import org.hibernate.validator.constraints.pl.PESEL; @Builder public record UserCreateRequest( @NotNull String name, @NotNull String surname, - @Adult String pesel, + @PESEL @Adult String pesel, @Min(0) double pln) { } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ae6306d..47b720c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,6 +2,9 @@ hsqldb: name: db port: 9090 +nbp: + baseurl: "http://api.nbp.pl" + spring: datasource: username: sa @@ -11,3 +14,12 @@ spring: jpa: hibernate: ddl-auto: create + +management: + endpoints: + jmx: + exposure: + exclude: '*' + web: + exposure: + include: health diff --git a/src/test/java/eu/ztsh/wymiana/EntityCreator.java b/src/test/java/eu/ztsh/wymiana/EntityCreator.java index c0bb186..617cbfa 100644 --- a/src/test/java/eu/ztsh/wymiana/EntityCreator.java +++ b/src/test/java/eu/ztsh/wymiana/EntityCreator.java @@ -2,13 +2,17 @@ package eu.ztsh.wymiana; import eu.ztsh.wymiana.data.entity.CurrencyEntity; import eu.ztsh.wymiana.data.entity.UserEntity; +import eu.ztsh.wymiana.model.Currency; import eu.ztsh.wymiana.model.Rate; import eu.ztsh.wymiana.model.Rates; +import eu.ztsh.wymiana.model.User; import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; import eu.ztsh.wymiana.web.model.UserCreateRequest; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; public class EntityCreator { @@ -16,6 +20,8 @@ public class EntityCreator { public static class Constants { public static String PESEL = "00281018264"; + public static String INVALID_PESEL = PESEL.replace('6', '7'); + public static String ANOTHER_PESEL = "08280959342"; public static String NAME = "Janina"; public static String SURNAME = "Kowalska"; public static double PLN = 20.10; @@ -28,10 +34,25 @@ public class EntityCreator { } - public static UserEntityBuilder user() { + public static UserEntityBuilder userEntity() { return new UserEntityBuilder(); } + public static User user() { + return user(Constants.PLN, 0); + } + + public static User user(double pln, double usd) { + Map currencies = new HashMap<>(); + if (pln > 0) { + currencies.put("PLN", new Currency("PLN", pln)); + } + if (usd > 0) { + currencies.put("USD", new Currency("USD", usd)); + } + return new User(Constants.NAME, Constants.SURNAME, Constants.PESEL, currencies); + } + public static UserCreateRequest.UserCreateRequestBuilder userRequest() { return UserCreateRequest.builder().name(Constants.NAME) .surname(Constants.SURNAME) diff --git a/src/test/java/eu/ztsh/wymiana/data/repository/UserRepositoryTest.java b/src/test/java/eu/ztsh/wymiana/data/repository/UserRepositoryTest.java index 63cd478..20b7868 100644 --- a/src/test/java/eu/ztsh/wymiana/data/repository/UserRepositoryTest.java +++ b/src/test/java/eu/ztsh/wymiana/data/repository/UserRepositoryTest.java @@ -18,7 +18,7 @@ class UserRepositoryTest extends RepositoryBasedTest { @Transactional @DisplayName("Basic insert & get test") void basicTest() { - var entity = EntityCreator.user().build(); + var entity = EntityCreator.userEntity().build(); userRepository.save(entity); expect(entity); } diff --git a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java index 95b1442..c44625e 100644 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -5,6 +5,7 @@ import eu.ztsh.wymiana.RepositoryBasedTest; import eu.ztsh.wymiana.data.repository.UserRepository; import eu.ztsh.wymiana.exception.ExchangeFailedException; import eu.ztsh.wymiana.exception.InsufficientFundsException; +import eu.ztsh.wymiana.exception.UserNotFoundException; import eu.ztsh.wymiana.validation.InstanceValidator; import eu.ztsh.wymiana.validation.ValidationFailedException; import jakarta.transaction.Transactional; @@ -37,7 +38,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { @Transactional @Test void plnToUsdToSellSuccessTest() { - var entity = EntityCreator.user().build(); + var entity = EntityCreator.userEntity().build(); userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(PLN_SYMBOL) @@ -46,14 +47,14 @@ class CurrencyServiceTest extends RepositoryBasedTest { .build()); assertThat(result.currencies()) .matches(map -> map.get(PLN_SYMBOL).amount() == 0 && map.get(USD_SYMBOL).amount() == USD_BUY); - var expected = EntityCreator.user().pln(0).usd(USD_BUY).build(); + var expected = EntityCreator.userEntity().pln(0).usd(USD_BUY).build(); expect(expected); } @Transactional @Test void plnToUsdToBuySuccessTest() { - var entity = EntityCreator.user().build(); + var entity = EntityCreator.userEntity().build(); userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(PLN_SYMBOL) @@ -62,14 +63,14 @@ class CurrencyServiceTest extends RepositoryBasedTest { .build()); assertThat(result.currencies()) .matches(map -> map.get(PLN_SYMBOL).amount() == 0 && map.get(USD_SYMBOL).amount() == USD_BUY); - var expected = EntityCreator.user().pln(0).usd(USD_BUY).build(); + var expected = EntityCreator.userEntity().pln(0).usd(USD_BUY).build(); expect(expected); } @Transactional @Test void usdToPlnToSellSuccessTest() { - var entity = EntityCreator.user().pln(-1).usd(USD_SELL).build(); + var entity = EntityCreator.userEntity().pln(-1).usd(USD_SELL).build(); userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(USD_SYMBOL) @@ -78,14 +79,14 @@ class CurrencyServiceTest extends RepositoryBasedTest { .build()); assertThat(result.currencies()) .matches(map -> map.get(PLN_SYMBOL).amount() == PLN && map.get(USD_SYMBOL).amount() == 0); - var expected = EntityCreator.user().pln(PLN).usd(0).build(); + var expected = EntityCreator.userEntity().pln(PLN).usd(0).build(); expect(expected); } @Transactional @Test void usdToPlnToBuySuccessTest() { - var entity = EntityCreator.user().pln(-1).usd(USD_SELL).build(); + var entity = EntityCreator.userEntity().pln(-1).usd(USD_SELL).build(); userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(USD_SYMBOL) @@ -94,14 +95,14 @@ class CurrencyServiceTest extends RepositoryBasedTest { .build()); assertThat(result.currencies()) .matches(map -> map.get(PLN_SYMBOL).amount() == PLN && map.get(USD_SYMBOL).amount() == 0); - var expected = EntityCreator.user().pln(PLN).usd(0).build(); + var expected = EntityCreator.userEntity().pln(PLN).usd(0).build(); expect(expected); } @Transactional @Test void usdToPlnNoUsdCurrencyTest() { - var entity = EntityCreator.user().build(); + var entity = EntityCreator.userEntity().build(); userRepository.save(entity); var request = EntityCreator.exchangeRequest() .from(USD_SYMBOL) @@ -116,7 +117,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { @Test void doubleExchangeTest() { var initialValue = 100; - var entity = EntityCreator.user().pln(initialValue).build(); + var entity = EntityCreator.userEntity().pln(initialValue).build(); userRepository.save(entity); var result1 = currencyService.exchange(EntityCreator.exchangeRequest() .from(PLN_SYMBOL) @@ -141,7 +142,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { @Transactional @Test void insufficientFundsTest() { - var entity = EntityCreator.user().build(); + var entity = EntityCreator.userEntity().build(); userRepository.save(entity); var request = EntityCreator.exchangeRequest() .from(PLN_SYMBOL) @@ -175,7 +176,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { .toSell(USD_SELL) .build(); assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ExchangeFailedException.class); + .isInstanceOf(UserNotFoundException.class); } @Test @@ -203,7 +204,7 @@ class CurrencyServiceTest extends RepositoryBasedTest { } private static Stream invalidPeselTest() { - return Stream.of("INVALID", PESEL.replace('6', '7')); + return Stream.of("INVALID", INVALID_PESEL); } } diff --git a/src/test/java/eu/ztsh/wymiana/service/UserServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/UserServiceTest.java index 8a0f9fc..1ab7690 100644 --- a/src/test/java/eu/ztsh/wymiana/service/UserServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/UserServiceTest.java @@ -31,7 +31,7 @@ class UserServiceTest extends RepositoryBasedTest { @DisplayName("Create new user") void createNewUserTest() { userService.create(EntityCreator.userRequest().build()); - var entity = EntityCreator.user().build(); + var entity = EntityCreator.userEntity().build(); expect(entity); } @@ -64,7 +64,7 @@ class UserServiceTest extends RepositoryBasedTest { @Test @DisplayName("Try to create too young user") void youngUserTest() { - var request = EntityCreator.userRequest().pesel("08280959342").build(); + var request = EntityCreator.userRequest().pesel("").build(); assertThatThrownBy(() -> userService.create(request)) .isInstanceOf(ValidationFailedException.class) .hasMessageContaining(Adult.MESSAGE); @@ -74,7 +74,7 @@ class UserServiceTest extends RepositoryBasedTest { @Transactional @DisplayName("Get existing user") void getExistingUserTest() { - var entity = EntityCreator.user().build(); + var entity = EntityCreator.userEntity().build(); userRepository.save(entity); var userOptional = userService.get(EntityCreator.Constants.PESEL); var expected = UserMapper.entityToPojo(entity); diff --git a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java index dc1eeb8..11503aa 100644 --- a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java +++ b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java @@ -13,13 +13,8 @@ class UserMapperTest { @Test void entityToPojoTest() { - var entity = EntityCreator.user().build(); - var expected = new User( - EntityCreator.Constants.NAME, - EntityCreator.Constants.SURNAME, - EntityCreator.Constants.PESEL, - Map.of("PLN", new Currency("PLN", EntityCreator.Constants.PLN)) - ); + var entity = EntityCreator.userEntity().build(); + var expected = EntityCreator.user(); assertThat(UserMapper.entityToPojo(entity)) .usingRecursiveComparison() .isEqualTo(expected); @@ -27,13 +22,8 @@ class UserMapperTest { @Test void pojoToEntityTest() { - var entity = new User( - EntityCreator.Constants.NAME, - EntityCreator.Constants.SURNAME, - EntityCreator.Constants.PESEL, - Map.of("PLN", new Currency("PLN", EntityCreator.Constants.PLN)) - ); - var expected = EntityCreator.user().build(); + var entity = EntityCreator.user(); + var expected = EntityCreator.userEntity().build(); assertThat(UserMapper.pojoToEntity(entity)) .usingRecursiveComparison() .isEqualTo(expected); @@ -42,7 +32,7 @@ class UserMapperTest { @Test void requestToEntityTest() { var request = EntityCreator.userRequest().build(); - var expected = EntityCreator.user().build(); + var expected = EntityCreator.userEntity().build(); assertThat(UserMapper.requestToEntity(request)) .usingRecursiveComparison() .isEqualTo(expected); diff --git a/src/test/java/eu/ztsh/wymiana/web/ApplicationIntegrationTests.java b/src/test/java/eu/ztsh/wymiana/web/ApplicationIntegrationTests.java new file mode 100644 index 0000000..272c140 --- /dev/null +++ b/src/test/java/eu/ztsh/wymiana/web/ApplicationIntegrationTests.java @@ -0,0 +1,242 @@ +package eu.ztsh.wymiana.web; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.ztsh.wymiana.EntityCreator; +import eu.ztsh.wymiana.WireMockExtension; +import eu.ztsh.wymiana.util.UserMapper; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.reactive.server.WebTestClient; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAdjusters; + +import static eu.ztsh.wymiana.EntityCreator.Constants.*; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test suite. + * Contrary to the principle of test independence, tests are dependent on one another to create continuous suite. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("it") +@ExtendWith(WireMockExtension.class) +@TestMethodOrder(MethodOrderer.DisplayName.class) +@TestClassOrder(ClassOrderer.DisplayName.class) +class ApplicationIntegrationTests { + + private final WebTestClient webTestClient; + private final ObjectMapper objectMapper; + + @Autowired + public ApplicationIntegrationTests(WebTestClient webTestClient) { + this.webTestClient = webTestClient; + objectMapper = new ObjectMapper(); + } + + private String asJson(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + } + + @Nested + @TestMethodOrder(MethodOrderer.DisplayName.class) + @DisplayName("01: Context") + class ContextTests { + + @Test + @DisplayName("01.1: Load context") + void contextLoads() { + assertThat(webTestClient).isNotNull(); + webTestClient.get().uri("/actuator/health").exchange().expectBody().json("{\"status\":\"UP\"}"); + } + + } + + @Nested + @TestMethodOrder(MethodOrderer.DisplayName.class) + @DisplayName("02: User") + class UserTests { + + private static final String endpoint = "/api/user"; + + @Test + @DisplayName("02.1: Create valid user") + void createUserTest() { + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.userRequest().pln(100).build()) + .exchange() + .expectStatus().isNoContent(); + } + + @Test + @DisplayName("02.2: Try to create invalid user") + void createInvalidUserTest() { + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.userRequest().pesel(INVALID_PESEL).build()) + .exchange() + .expectStatus() + .isBadRequest(); + } + + @Test + @DisplayName("02.3: Try to create duplicated user") + void createDuplicatedUserTest() { + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.userRequest().build()) + .exchange() + .expectStatus().isEqualTo(409) + .expectBody(String.class).isEqualTo("User with PESEL %s already exists".formatted(PESEL)); + } + + @Test + @DisplayName("02.4: Get valid user") + void getValidUserTest() { + webTestClient.get() + .uri(endpoint.concat("/").concat(PESEL)) + .exchange() + .expectStatus().isOk() + .expectBody().json(asJson(UserMapper.entityToPojo(EntityCreator.userEntity().pln(100).build()))); + } + + @Test + @DisplayName("02.5: Try to get non-existing user") + void getNonExistingUserTest() { + webTestClient.get() + .uri(endpoint.concat("/").concat(ANOTHER_PESEL)) + .exchange() + .expectStatus().isNotFound(); + } + + @Test + @DisplayName("02.6: Get user by incorrect PESEL") + void getIncorrectPeselUserTest() { + webTestClient.get() + .uri(endpoint.concat("/").concat(INVALID_PESEL)) + .exchange() + .expectStatus().isBadRequest(); + } + + } + + @Nested + @TestMethodOrder(MethodOrderer.DisplayName.class) + @DisplayName("03: Exchange") + class ExchangeTests { + + private static final String URI_PATTERN = "/api/exchangerates/rates/c/usd/%s/"; + private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final String endpoint = "/api/exchange"; + + @Test + @DisplayName("03.1: Try to perform invalid money exchange: no data") + void noNbpDataTest() { + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(PLN) + .build()) + .exchange() + .expectStatus().isEqualTo(500); + } + + @Test + @DisplayName("03.2: Perform valid money exchange") + void exchangeTest() { + var date = getTodayOrLastFriday(); + WireMockExtension.response( + URI_PATTERN.formatted(date), + 200, + asJson(EntityCreator.rates(date)) + ); + var expected = asJson(EntityCreator.user(100 - PLN, USD_BUY)); + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(PLN) + .build()) + .exchange() + .expectStatus().isOk() + .expectBody().json(expected); + WireMockExtension.verifyGet(1, URI_PATTERN.formatted(date)); + } + + @Test + @DisplayName("03.3: Try to perform invalid money exchange: not existing user") + void exchangeNotExistingUserTest() { + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.exchangeRequest() + .pesel(ANOTHER_PESEL) + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(PLN) + .build()) + .exchange() + .expectStatus().isNotFound() + .expectBody(String.class).isEqualTo("User with PESEL %s not found".formatted(ANOTHER_PESEL)); + } + + @Test + @DisplayName("03.4: Try to perform invalid money exchange: invalid PESEL") + void invalidPeselTest() { + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.exchangeRequest() + .pesel(INVALID_PESEL) + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(PLN) + .build()) + .exchange() + .expectStatus().isBadRequest(); + } + + @Test + @DisplayName("03.5: Try to perform invalid money exchange: insufficient funds") + void insufficientFundsTest() { + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.exchangeRequest() + .from(USD_SYMBOL) + .to(PLN_SYMBOL) + .toBuy(PLN) + .build()) + .exchange() + .expectStatus().isBadRequest(); + } + + private String getTodayOrLastFriday() { + var today = LocalDate.now(); + if (today.getDayOfWeek() == DayOfWeek.SATURDAY + || today.getDayOfWeek() == DayOfWeek.SUNDAY) { + today = today.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)); + } + return today.format(dtf); + } + + } + +} diff --git a/src/test/resources/application-it.yaml b/src/test/resources/application-it.yaml new file mode 100644 index 0000000..198e366 --- /dev/null +++ b/src/test/resources/application-it.yaml @@ -0,0 +1,2 @@ +nbp: + baseurl: "http://localhost:38080"