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 257ebbb..9ae7829 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ ${source.encoding} - 17 + 21 ${java.version} ${java.version} diff --git a/src/main/java/eu/ztsh/wymiana/web/controller/UserController.java b/src/main/java/eu/ztsh/wymiana/web/controller/UserController.java index c9f7bdc..da83862 100644 --- a/src/main/java/eu/ztsh/wymiana/web/controller/UserController.java +++ b/src/main/java/eu/ztsh/wymiana/web/controller/UserController.java @@ -1,10 +1,13 @@ package eu.ztsh.wymiana.web.controller; +import eu.ztsh.wymiana.exception.UserAlreadyExistsException; 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 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; @@ -17,7 +20,7 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @Validated @RestController -@RequestMapping("/api/user") +@RequestMapping(path = "/api/user", produces = "application/json") public class UserController { private final UserService userService; @@ -28,8 +31,17 @@ public class UserController { } @PostMapping - public ResponseEntity create(@Valid @RequestBody UserCreateRequest request) { - userService.create(request); + 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/test/java/eu/ztsh/wymiana/EntityCreator.java b/src/test/java/eu/ztsh/wymiana/EntityCreator.java index c0bb186..5cb9498 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", pln)); + } + 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..9603e00 100644 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -37,7 +37,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 +46,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 +62,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 +78,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 +94,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 +116,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 +141,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) @@ -203,7 +203,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 index 8781c04..38fdfae 100644 --- a/src/test/java/eu/ztsh/wymiana/web/ApplicationIntegrationTests.java +++ b/src/test/java/eu/ztsh/wymiana/web/ApplicationIntegrationTests.java @@ -1,6 +1,11 @@ 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.model.User; +import eu.ztsh.wymiana.util.UserMapper; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; @@ -14,6 +19,12 @@ 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; /** @@ -28,10 +39,20 @@ import static org.assertj.core.api.Assertions.assertThat; 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 @@ -53,40 +74,66 @@ class ApplicationIntegrationTests { @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().is2xxSuccessful(); } @Test @DisplayName("02.2: Try to create invalid user") void createInvalidUserTest() { - + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.userRequest().pesel(INVALID_PESEL).build()) + .exchange() + .expectStatus() + .is4xxClientError(); } @Test @DisplayName("02.3: Try to create duplicated user") void createDuplicatedUserTest() { - + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.userRequest().build()) + .exchange() + .expectStatus().is4xxClientError() + .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().is2xxSuccessful() + .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().is4xxClientError(); } @Test @DisplayName("02.6: Get user by incorrect PESEL") void getIncorrectPeselUserTest() { - + webTestClient.get() + .uri(endpoint.concat("/").concat(INVALID_PESEL)) + .exchange() + .expectStatus().is4xxClientError(); } } @@ -96,34 +143,98 @@ class ApplicationIntegrationTests { @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().is5xxServerError(); } @Test @DisplayName("03.2: Perform valid money exchange") - void exchangeTest() { - + void exchangeTest() throws JsonProcessingException { + var date = getTodayOrLastFriday(); + WireMockExtension.response( + URI_PATTERN.formatted(date), + 200, + new ObjectMapper().writeValueAsString(EntityCreator.rates(date)) + ); + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(PLN) + .build()) + .exchange() + .expectStatus().is2xxSuccessful() + .expectBody(User.class).isEqualTo(EntityCreator.user(100 - PLN, USD_BUY)); } @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().is4xxClientError() + .expectBody(String.class).isEqualTo("An exchange error has occurred"); } @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().is4xxClientError(); } @Test @DisplayName("03.5: Try to perform invalid money exchange: insufficient funds") void insufficientFundsTest() { + webTestClient.post() + .uri(endpoint) + .bodyValue(EntityCreator.exchangeRequest() + .pesel(ANOTHER_PESEL) + .from(USD_SYMBOL) + .to(PLN_SYMBOL) + .toBuy(PLN) + .build()) + .exchange() + .expectStatus().is4xxClientError(); + } + 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); } }