diff --git a/.editorconfig b/.editorconfig index 54a8570..72a7d08 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,5 +6,5 @@ indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true -[{*.yaml,*.json}] +[*.yaml] indent_size = 2 diff --git a/.woodpecker/maven.yaml b/.woodpecker/maven.yaml index 6458761..82461b2 100644 --- a/.woodpecker/maven.yaml +++ b/.woodpecker/maven.yaml @@ -1,5 +1,5 @@ variables: - &maven_image maven:3.9.6-eclipse-temurin-21-alpine + &maven_image maven:3.9.6-eclipse-temurin-17-alpine steps: - name: build diff --git a/pom.xml b/pom.xml index 78cfbdb..5cbeab4 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ eu.ztsh wymiana-walut - 1.1.0 + 1.0.0-SNAPSHOT @@ -22,16 +22,13 @@ ${source.encoding} - 21 + 17 ${java.version} ${java.version} - 3.5.4 - 2.5.0 - 1.2.1 @@ -52,10 +49,6 @@ org.springframework.boot spring-boot-starter-data-jpa - - org.springframework.boot - spring-boot-starter-actuator - @@ -63,11 +56,6 @@ lombok provided - - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${openapi.version} - @@ -79,12 +67,6 @@ org.springframework.boot spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-webflux - test org.junit.jupiter @@ -95,12 +77,6 @@ org.assertj assertj-core - - org.wiremock - wiremock-standalone - ${wiremock.version} - test - @@ -109,49 +85,6 @@ org.springframework.boot spring-boot-maven-plugin - - org.jsonschema2pojo - jsonschema2pojo-maven-plugin - ${jsonschema2pojo.version} - - ${basedir}/src/main/resources/schema - eu.ztsh.wymiana.model - true - - - - - generate - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - default-test - - - **/*Tests.java - - - - - integration-tests - test - - test - - - - **/*Test.java - - - - - diff --git a/readme.md b/readme.md deleted file mode 100644 index f2ce86f..0000000 --- a/readme.md +++ /dev/null @@ -1,202 +0,0 @@ -# Wymiana walut - -Prosty mikroserwis stworzony na potrzeby rekrutacji - -## Założenia - -- PESEL jedynym identyfikatorem -- Jedno konto na PESEL -- Użytkownik pełnoletni -- Wymiana walut na podstawie tabeli NBP -- Baza danych in-memory - -## Interfejsy - -- stworzenie konta - - ⇒ POST `/api/user` - ```json - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "surname": { - "type": "string" - }, - "pesel": { - "type": "string" - }, - "initial": { - "type": "number", - "description": "początkowy stan konta w domyślnej walucie" - } - }, - "required": [ - "name", - "surname", - "pesel", - "initial" - ] - } - ``` - ⇐ 204/400/409/500 - -- pobranie informacji o koncie - - ⇒ GET `/api/user/{pesel}` - - ⇐ 200/400/404/500 - ```json - { - "type": "object", - "def": { - "currency": { - "type": "object", - "properties": { - "symbol": { - "type": "string" - }, - "amount": { - "type": "number" - } - }, - "required": [ - "symbol", - "amount" - ] - } - }, - "properties": { - "name": { - "type": "string" - }, - "surname": { - "type": "string" - }, - "pesel": { - "type": "string" - }, - "currencies": { - "type": "array", - "items": { - "$ref": "#/def/currency" - } - } - }, - "required": [ - "name", - "surname", - "pesel", - "currencies" - ] - } - ``` -- zlecenie wymiany walut - - ⇒ POST `/api/exchange/{pesel}` - ```json - { - "type": "object", - "properties": { - "pesel": { - "type": "string" - }, - "from": { - "type": "string", - "description": "waluta źródłowa" - }, - "to": { - "type": "string", - "description": "waluta docelowa" - }, - "toBuy": { - "type": "number", - "description": "ilość do zakupu" - }, - "toSell": { - "type": "number", - "description": "ilość do sprzedaży" - } - }, - "oneOf": [ - { - "required": [ - "pesel", - "from", - "to", - "toBuy" - ] - }, - { - "required": [ - "pesel", - "from", - "to", - "toSell" - ] - } - ] - } - ``` - - ⇐ 200/400/404/500 - - patrz: pobranie informacji o koncie - -## Architektura - -```mermaid - flowchart LR - actor["fa:fa-person"] - subgraph NBP - tabC["Tabela C"] - end - subgraph proces - subgraph spring-boot - core - subgraph endpoint - user - exchange - end - end - hsqldb - end - - actor <--> user - actor <--> exchange - endpoint <--> core - core <--> tabC - core <-- hsql . port --> hsqldb - -``` - -## Konfiguracja - -Aplikacja posiada dostosowaną konfigurację, która obejmuje własności: - -### `hsqldb` - -Konfiguracja bazy danych - -| Nazwa | Opis | Typ | Wartość domyślna | -|-------|------------------|--------|------------------| -| name | host bazy danych | string | db | -| port | port bazy danych | int | 9090 | - -### `nbp` - -Konfiguracja połączenia z Narodowym Bankiem Polskim - -| Nazwa | Opis | Typ | Wartość domyślna | -|---------|--------------|--------|-------------------| -| baseurl | host API nbp | string | http://api.nbp.pl | - -### `currency` - -Konfiguracja walut - -| Nazwa | Opis | Typ | Wartość domyślna | -|---------|-----------------------------------------|--------|------------------| -| initial | waluta początkowa przy zakładaniu konta | string | PLN | diff --git a/src/main/java/eu/ztsh/wymiana/config/ClockConfiguration.java b/src/main/java/eu/ztsh/wymiana/config/ClockConfiguration.java deleted file mode 100644 index eea21a3..0000000 --- a/src/main/java/eu/ztsh/wymiana/config/ClockConfiguration.java +++ /dev/null @@ -1,16 +0,0 @@ -package eu.ztsh.wymiana.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.time.Clock; - -@Configuration -public class ClockConfiguration { - - @Bean - public Clock clock() { - return Clock.systemDefaultZone(); - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/config/CurrencyProperties.java b/src/main/java/eu/ztsh/wymiana/config/CurrencyProperties.java deleted file mode 100644 index 9cfbce1..0000000 --- a/src/main/java/eu/ztsh/wymiana/config/CurrencyProperties.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.ztsh.wymiana.config; - -import eu.ztsh.wymiana.model.Symbol; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties("currency") -public record CurrencyProperties(Symbol initial) { - -} diff --git a/src/main/java/eu/ztsh/wymiana/config/NbpProperties.java b/src/main/java/eu/ztsh/wymiana/config/NbpProperties.java deleted file mode 100644 index 0ed6c42..0000000 --- a/src/main/java/eu/ztsh/wymiana/config/NbpProperties.java +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index f264dbe..0000000 --- a/src/main/java/eu/ztsh/wymiana/config/RestClientConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -package eu.ztsh.wymiana.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestClient; - -@Configuration -public class RestClientConfiguration { - - @Bean - public RestClient restClient(NbpProperties nbpProperties) { - return RestClient.builder() - .baseUrl(nbpProperties.baseurl()) - .defaultHeader("Accept", "application/json") - .build(); - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/data/entity/CurrencyEntity.java b/src/main/java/eu/ztsh/wymiana/data/entity/CurrencyEntity.java index 95615f0..441b224 100644 --- a/src/main/java/eu/ztsh/wymiana/data/entity/CurrencyEntity.java +++ b/src/main/java/eu/ztsh/wymiana/data/entity/CurrencyEntity.java @@ -1,17 +1,12 @@ package eu.ztsh.wymiana.data.entity; -import eu.ztsh.wymiana.model.Symbol; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.math.BigDecimal; - @Data @NoArgsConstructor @AllArgsConstructor @@ -22,8 +17,7 @@ public class CurrencyEntity { @Id String pesel; @Id - @Enumerated(EnumType.STRING) - Symbol symbol; - BigDecimal amount; + String symbol; + Double amount; } diff --git a/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java b/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java deleted file mode 100644 index b32bb5e..0000000 --- a/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java +++ /dev/null @@ -1,13 +0,0 @@ -package eu.ztsh.wymiana.exception; - -public class ExchangeFailedException extends RuntimeException { - - public ExchangeFailedException() { - this("An exchange error has occurred"); - } - - public ExchangeFailedException(String message) { - super(message); - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java b/src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java deleted file mode 100644 index 4c8f314..0000000 --- a/src/main/java/eu/ztsh/wymiana/exception/InsufficientFundsException.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.ztsh.wymiana.exception; - -public class InsufficientFundsException extends RuntimeException { - - public InsufficientFundsException() { - super("Insufficient funds"); - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/exception/NoDataException.java b/src/main/java/eu/ztsh/wymiana/exception/NoDataException.java deleted file mode 100644 index 1472b0b..0000000 --- a/src/main/java/eu/ztsh/wymiana/exception/NoDataException.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.ztsh.wymiana.exception; - -import eu.ztsh.wymiana.model.Symbol; - -public class NoDataException extends RuntimeException { - - public NoDataException(Symbol code, String date) { - super("No data for code %s and date %s".formatted(code, date)); - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/exception/UserNotFoundException.java b/src/main/java/eu/ztsh/wymiana/exception/UserNotFoundException.java deleted file mode 100644 index 3cf38c2..0000000 --- a/src/main/java/eu/ztsh/wymiana/exception/UserNotFoundException.java +++ /dev/null @@ -1,15 +0,0 @@ -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/model/Currency.java b/src/main/java/eu/ztsh/wymiana/model/Currency.java index 05e7ab1..0cc6b16 100644 --- a/src/main/java/eu/ztsh/wymiana/model/Currency.java +++ b/src/main/java/eu/ztsh/wymiana/model/Currency.java @@ -1,7 +1,5 @@ package eu.ztsh.wymiana.model; -import java.math.BigDecimal; - -public record Currency(Symbol symbol, BigDecimal amount) { +public record Currency(String symbol, double amount) { } diff --git a/src/main/java/eu/ztsh/wymiana/model/Symbol.java b/src/main/java/eu/ztsh/wymiana/model/Symbol.java deleted file mode 100644 index 4d38bbd..0000000 --- a/src/main/java/eu/ztsh/wymiana/model/Symbol.java +++ /dev/null @@ -1,6 +0,0 @@ -package eu.ztsh.wymiana.model; - -public enum Symbol { - PLN, - USD -} diff --git a/src/main/java/eu/ztsh/wymiana/model/User.java b/src/main/java/eu/ztsh/wymiana/model/User.java index 7ae46a1..a1f28f1 100644 --- a/src/main/java/eu/ztsh/wymiana/model/User.java +++ b/src/main/java/eu/ztsh/wymiana/model/User.java @@ -2,6 +2,6 @@ package eu.ztsh.wymiana.model; import java.util.Map; -public record User(String name, String surname, String pesel, Map currencies) { +public record User(String name, String surname, String pesel, Map currencies) { } diff --git a/src/main/java/eu/ztsh/wymiana/model/UserCreateRequestConfiguredWrapper.java b/src/main/java/eu/ztsh/wymiana/model/UserCreateRequestConfiguredWrapper.java deleted file mode 100644 index b4a3619..0000000 --- a/src/main/java/eu/ztsh/wymiana/model/UserCreateRequestConfiguredWrapper.java +++ /dev/null @@ -1,65 +0,0 @@ -package eu.ztsh.wymiana.model; - -import eu.ztsh.wymiana.web.model.UserCreateRequest; - -import java.math.BigDecimal; - -public class UserCreateRequestConfiguredWrapper { - - private final UserCreateRequest request; - private final Symbol initialSymbol; - - public String name() { - return request.name(); - } - - public String surname() { - return request.surname(); - } - - public String pesel() { - return request.pesel(); - } - - public BigDecimal initial() { - return request.initial(); - } - - public Symbol initialSymbol() { - return initialSymbol; - } - - private UserCreateRequestConfiguredWrapper(Builder builder) { - this.request = builder.request; - this.initialSymbol = builder.initial; - } - - public static Builder wrap(UserCreateRequest request) { - return new Builder().withRequest(request); - } - - public static final class Builder { - - private UserCreateRequest request; - private Symbol initial; - - private Builder() { - } - - private Builder withRequest(UserCreateRequest request) { - this.request = request; - return this; - } - - public Builder withInitial(Symbol initial) { - this.initial = initial; - return this; - } - - public UserCreateRequestConfiguredWrapper build() { - return new UserCreateRequestConfiguredWrapper(this); - } - - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java deleted file mode 100644 index e9736d7..0000000 --- a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java +++ /dev/null @@ -1,84 +0,0 @@ -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.Symbol; -import eu.ztsh.wymiana.model.User; -import eu.ztsh.wymiana.validation.InstanceValidator; -import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@RequiredArgsConstructor -@Service -public class CurrencyService { - - private final UserService userService; - private final NbpService nbpService; - private final InstanceValidator validator; - - public User exchange(CurrencyExchangeRequest request) { - validator.validate(request); - return userService.get(request.pesel()).map(user -> { - if (!request.from().equals(Symbol.PLN) && !request.to().equals(Symbol.PLN)) { - throw new ExchangeFailedException("Either 'from' or 'to' has to be PLN"); - } - - var from = user.currencies().get(request.from()); - if (from == null) { - // There is no currency 'from' opened so no need to check if user has funds to exchange - throw new InsufficientFundsException(); - } - var exchanged = performExchange(from, - Optional.ofNullable(user.currencies().get(request.to())).orElse(create(request.to())), - Optional.ofNullable(request.toSell()).orElse(BigDecimal.ZERO), - Optional.ofNullable(request.toBuy()).orElse(BigDecimal.ZERO)); - user.currencies().putAll(exchanged); - return userService.update(user); - }) - .orElseThrow(() -> new UserNotFoundException(request)); - } - - private Currency create(Symbol symbol) { - return new Currency(symbol, BigDecimal.ZERO); - } - - private Map performExchange(Currency from, Currency to, BigDecimal toSell, BigDecimal toBuy) { - BigDecimal exchangeRate; - BigDecimal neededFromAmount; - BigDecimal requestedToAmount; - if (from.symbol().equals(Symbol.PLN)) { - exchangeRate = nbpService.getSellRate(to.symbol()); - neededFromAmount = round(toBuy.signum() != 0 ? toBuy.multiply(exchangeRate) : toSell); - requestedToAmount = round(toBuy.signum() != 0 ? toBuy : divide(toSell, exchangeRate)); - } else { - exchangeRate = nbpService.getBuyRate(from.symbol()); - neededFromAmount = round(toBuy.signum() != 0 ? divide(toBuy, exchangeRate) : toSell); - requestedToAmount = round(toBuy.signum() != 0 ? toBuy : toSell.multiply(exchangeRate)); - } - if (neededFromAmount.compareTo(from.amount()) > 0) { - throw new InsufficientFundsException(); - } - var newFrom = new Currency(from.symbol(), from.amount().subtract(neededFromAmount)); - var newTo = new Currency(to.symbol(), to.amount().add(requestedToAmount)); - return Stream.of(newFrom, newTo).collect(Collectors.toMap(Currency::symbol, currency -> currency)); - } - - private BigDecimal round(BigDecimal input) { - return input.setScale(2, RoundingMode.HALF_UP); - } - - private BigDecimal divide(BigDecimal input, BigDecimal division) { - return input.setScale(2, RoundingMode.HALF_UP).divide(division, RoundingMode.HALF_UP); - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/service/NbpService.java b/src/main/java/eu/ztsh/wymiana/service/NbpService.java deleted file mode 100644 index 3d5edd3..0000000 --- a/src/main/java/eu/ztsh/wymiana/service/NbpService.java +++ /dev/null @@ -1,91 +0,0 @@ -package eu.ztsh.wymiana.service; - -import eu.ztsh.wymiana.exception.NoDataException; -import eu.ztsh.wymiana.model.Rates; -import eu.ztsh.wymiana.model.Symbol; -import lombok.RequiredArgsConstructor; -import org.assertj.core.util.VisibleForTesting; -import org.springframework.http.HttpStatusCode; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; - -import java.math.BigDecimal; -import java.time.Clock; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAdjusters; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * NBP exchange rates service - */ -@Service -@RequiredArgsConstructor -public class NbpService { - - private final Clock clock; - private final RestClient restClient; - private static final String URI_PATTERN = "/api/exchangerates/rates/c/{code}/{date}/"; - private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - - private final ConcurrentMap cache = new ConcurrentHashMap<>(1); - - public BigDecimal getSellRate(Symbol currency) { - return getCurrency(currency).sell(); - } - - public BigDecimal getBuyRate(Symbol currency) { - return getCurrency(currency).buy(); - } - - private synchronized RatesCache getCurrency(Symbol currency) { - var today = getFetchDate(); - var cacheObject = cache.get(currency); - if (cacheObject == null || cacheObject.date().isBefore(today)) { - var fresh = fetchData(currency, dtf.format(today)); - var rate = fresh.getRates().getFirst(); - cacheObject = new RatesCache( - LocalDate.parse(rate.getEffectiveDate(), dtf), - rate.getBid(), - rate.getAsk() - ); - cache.put(Symbol.valueOf(fresh.getCode().toUpperCase()), cacheObject); - } - return cacheObject; - } - - /** - * Calculates date for data fetch. - *

- * Usually this would be today, but as rates are set only Mon-Fri, during weekends it is needed to fetch last friday rates. - * - * @return date for data fetch - */ - @VisibleForTesting - LocalDate getFetchDate() { - var today = LocalDate.now(clock); - return isWeekend(today) ? today.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)) : today; - } - - @VisibleForTesting - Rates fetchData(Symbol code, String date) { - return restClient.get().uri(URI_PATTERN, code.name().toLowerCase(), date) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, ((request, response) -> { - throw new NoDataException(code, date); - })) - .body(Rates.class); - } - - private static boolean isWeekend(LocalDate today) { - return today.getDayOfWeek() == DayOfWeek.SATURDAY - || today.getDayOfWeek() == DayOfWeek.SUNDAY; - } - - private record RatesCache(LocalDate date, BigDecimal buy, BigDecimal sell) { - - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/service/UserService.java b/src/main/java/eu/ztsh/wymiana/service/UserService.java index 2205d19..01cba7c 100644 --- a/src/main/java/eu/ztsh/wymiana/service/UserService.java +++ b/src/main/java/eu/ztsh/wymiana/service/UserService.java @@ -1,45 +1,31 @@ package eu.ztsh.wymiana.service; -import eu.ztsh.wymiana.config.CurrencyProperties; import eu.ztsh.wymiana.data.repository.UserRepository; import eu.ztsh.wymiana.exception.UserAlreadyExistsException; import eu.ztsh.wymiana.model.User; -import eu.ztsh.wymiana.model.UserCreateRequestConfiguredWrapper; import eu.ztsh.wymiana.util.UserMapper; -import eu.ztsh.wymiana.validation.InstanceValidator; import eu.ztsh.wymiana.web.model.UserCreateRequest; +import jakarta.validation.Valid; 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 { private final UserRepository userRepository; - private final InstanceValidator validator; - private final CurrencyProperties currencyProperties; - public User create(UserCreateRequest request) { - validator.validate(request); + public User create(@Valid UserCreateRequest request) { if (userRepository.findById(request.pesel()).isPresent()) { throw new UserAlreadyExistsException(request); } - return UserMapper.entityToPojo(userRepository.save(UserMapper.requestToEntity( - UserCreateRequestConfiguredWrapper.wrap(request).withInitial(currencyProperties.initial()).build() - ))); + return UserMapper.entityToPojo(userRepository.save(UserMapper.requestToEntity(request))); } - public Optional get(@PESEL String pesel) { + public Optional get(String pesel) { return userRepository.findById(pesel).map(UserMapper::entityToPojo); } - public User update(User user) { - return UserMapper.entityToPojo(userRepository.save(UserMapper.pojoToEntity(user))); - } - } diff --git a/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java b/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java index 697e956..7542187 100644 --- a/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java +++ b/src/main/java/eu/ztsh/wymiana/util/CurrencyMapper.java @@ -2,7 +2,6 @@ package eu.ztsh.wymiana.util; import eu.ztsh.wymiana.data.entity.CurrencyEntity; import eu.ztsh.wymiana.model.Currency; -import eu.ztsh.wymiana.model.Symbol; import java.util.List; import java.util.Map; @@ -14,19 +13,11 @@ public class CurrencyMapper { return new Currency(entity.getSymbol(), entity.getAmount()); } - public static CurrencyEntity pojoToEntity(Currency pojo, String pesel) { - return new CurrencyEntity(pesel, pojo.symbol(), pojo.amount()); - } - - public static Map entitiesToPojoMap(List values) { + public static Map entitiesToPojoMap(List values) { return values.stream().map(CurrencyMapper::entityToPojo) .collect(Collectors.toMap(Currency::symbol, pojo -> pojo)); } - public static List pojoMapToEntities(Map currencies, String pesel) { - return currencies.values().stream().map(entry -> pojoToEntity(entry, pesel)).toList(); - } - private CurrencyMapper() { } diff --git a/src/main/java/eu/ztsh/wymiana/util/UserMapper.java b/src/main/java/eu/ztsh/wymiana/util/UserMapper.java index 31c2a20..b4aff44 100644 --- a/src/main/java/eu/ztsh/wymiana/util/UserMapper.java +++ b/src/main/java/eu/ztsh/wymiana/util/UserMapper.java @@ -3,8 +3,7 @@ package eu.ztsh.wymiana.util; import eu.ztsh.wymiana.data.entity.CurrencyEntity; import eu.ztsh.wymiana.data.entity.UserEntity; import eu.ztsh.wymiana.model.User; -import eu.ztsh.wymiana.model.UserCreateRequestConfiguredWrapper; -import eu.ztsh.wymiana.web.model.UserResponse; +import eu.ztsh.wymiana.web.model.UserCreateRequest; import java.util.List; @@ -15,18 +14,9 @@ public class UserMapper { CurrencyMapper.entitiesToPojoMap(entity.getCurrencies())); } - public static UserEntity pojoToEntity(User pojo) { - return new UserEntity(pojo.pesel(), pojo.name(), pojo.surname(), - CurrencyMapper.pojoMapToEntities(pojo.currencies(), pojo.pesel())); - } - - public static UserResponse pojoToResponse(User pojo) { - return new UserResponse(pojo.name(), pojo.surname(), pojo.pesel(), pojo.currencies().values().stream().toList()); - } - - public static UserEntity requestToEntity(UserCreateRequestConfiguredWrapper request) { + public static UserEntity requestToEntity(UserCreateRequest request) { return new UserEntity(request.pesel(), request.name(), request.surname(), - List.of(new CurrencyEntity(request.pesel(), request.initialSymbol(), request.initial()))); + List.of(new CurrencyEntity(request.pesel(), "PLN", request.pln()))); } private UserMapper() { diff --git a/src/main/java/eu/ztsh/wymiana/validation/Adult.java b/src/main/java/eu/ztsh/wymiana/validation/Adult.java index 0d7448f..affba90 100644 --- a/src/main/java/eu/ztsh/wymiana/validation/Adult.java +++ b/src/main/java/eu/ztsh/wymiana/validation/Adult.java @@ -16,12 +16,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Constraint(validatedBy = AdultValidator.class) @Documented public @interface Adult { - String message() default MESSAGE; + String message() default "{jakarta.validation.constraints.Adult.message}"; Class[] groups() default { }; Class[] payload() default { }; - - String MESSAGE = "The person has not reached the age of 18"; - } diff --git a/src/main/java/eu/ztsh/wymiana/validation/InstanceValidator.java b/src/main/java/eu/ztsh/wymiana/validation/InstanceValidator.java deleted file mode 100644 index 664f5c4..0000000 --- a/src/main/java/eu/ztsh/wymiana/validation/InstanceValidator.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.ztsh.wymiana.validation; - -import jakarta.validation.Validator; - -public class InstanceValidator { - - private final Validator validator; - - public InstanceValidator(Validator validator) { - this.validator = validator; - } - - public void validate(T t, Class... classes) { - var violations = validator.validate(t, classes); - if (!violations.isEmpty()) { - throw new ValidationFailedException(violations); - } - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/validation/InstanceValidatorFactory.java b/src/main/java/eu/ztsh/wymiana/validation/InstanceValidatorFactory.java deleted file mode 100644 index 69289ab..0000000 --- a/src/main/java/eu/ztsh/wymiana/validation/InstanceValidatorFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package eu.ztsh.wymiana.validation; - -import jakarta.validation.Validation; -import jakarta.validation.ValidatorFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class InstanceValidatorFactory { - - @Bean - public InstanceValidator instanceValidator() { - try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { - return new InstanceValidator(factory.getValidator()); - } - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequest.java b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequest.java deleted file mode 100644 index 608bc3c..0000000 --- a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package eu.ztsh.wymiana.validation; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.TYPE_USE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - - -@Retention(RUNTIME) -@Target({ TYPE_USE }) -@Documented -@Constraint(validatedBy = {ValidExchangeRequestValidator.class }) -public @interface ValidExchangeRequest { - - String message() default "Exchange request is not valid"; - Class[] groups() default { }; - Class[] payload() default { }; - -} diff --git a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java deleted file mode 100644 index 9a01a0c..0000000 --- a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java +++ /dev/null @@ -1,25 +0,0 @@ -package eu.ztsh.wymiana.validation; - -import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; - -public class ValidExchangeRequestValidator implements - ConstraintValidator { - - @Override - public boolean isValid(CurrencyExchangeRequest request, - ConstraintValidatorContext constraintValidatorContext) { - if (request == null) { - return false; - } - - // Apart from @NotNull annotation we need to check if request.from() != null to avoid NPE in equals - return (request.from() != null && !request.from().equals(request.to())) - && !((request.toBuy() == null && request.toSell() == null) - || (request.toBuy() != null && request.toSell() != null)) - && ((request.toBuy() != null && request.toBuy().signum() >= 0) - || (request.toSell() != null && request.toSell().signum() >= 0)); - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/validation/ValidationFailedException.java b/src/main/java/eu/ztsh/wymiana/validation/ValidationFailedException.java deleted file mode 100644 index 7ed17fa..0000000 --- a/src/main/java/eu/ztsh/wymiana/validation/ValidationFailedException.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.ztsh.wymiana.validation; - -import jakarta.validation.ConstraintViolation; -import lombok.Getter; - -import java.util.Set; -import java.util.stream.Collectors; - -@Getter -public class ValidationFailedException extends RuntimeException { - - private final transient Set violations; - - public ValidationFailedException(Set> violations) { - super("Validation failed: %s".formatted(violations.stream() - .map(ConstraintViolation::getMessage).collect(Collectors.joining(System.lineSeparator())))); - this.violations = violations; - } - -} diff --git a/src/main/java/eu/ztsh/wymiana/web/controller/ExchangeController.java b/src/main/java/eu/ztsh/wymiana/web/controller/ExchangeController.java deleted file mode 100644 index 39c635d..0000000 --- a/src/main/java/eu/ztsh/wymiana/web/controller/ExchangeController.java +++ /dev/null @@ -1,62 +0,0 @@ -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 eu.ztsh.wymiana.web.model.UserResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -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; - - @Operation(summary = "Perform exchange") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", - description = "Exchange performed successfully", - content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = UserResponse.class))), - @ApiResponse(responseCode = "400", - description = "Insufficient funds", - content = @Content(mediaType = MediaType.TEXT_PLAIN_VALUE)), - @ApiResponse(responseCode = "404", - description = "User not found", - content = @Content(mediaType = MediaType.TEXT_PLAIN_VALUE)), - @ApiResponse(responseCode = "500", - description = "Another error has occurred", - content = @Content(mediaType = MediaType.TEXT_PLAIN_VALUE)) - }) - @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 deleted file mode 100644 index dfa40b6..0000000 --- a/src/main/java/eu/ztsh/wymiana/web/controller/UserController.java +++ /dev/null @@ -1,83 +0,0 @@ -package eu.ztsh.wymiana.web.controller; - -import eu.ztsh.wymiana.exception.UserAlreadyExistsException; -import eu.ztsh.wymiana.exception.UserNotFoundException; -import eu.ztsh.wymiana.service.UserService; -import eu.ztsh.wymiana.util.UserMapper; -import eu.ztsh.wymiana.validation.ValidationFailedException; -import eu.ztsh.wymiana.web.model.UserCreateRequest; -import eu.ztsh.wymiana.web.model.UserResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.ValidationException; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -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") -@Tag(name="User management", description = "Create or get user") -public class UserController { - - private final UserService userService; - - @Operation(summary = "Get user by PESEL") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User found"), - @ApiResponse(responseCode = "400", description = "Request not valid", content = @Content), - @ApiResponse(responseCode = "404", description = "User not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Another error has occurred", content = @Content) - }) - @GetMapping("{pesel}") - public ResponseEntity get(@PathVariable("pesel") String pesel) { - try { - return userService.get(pesel) - .map(UserMapper::pojoToResponse) - .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(); - } - } - - @Operation(summary = "Create user") - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "User created", content = @Content), - @ApiResponse(responseCode = "400", description = "Request not valid", content = @Content(mediaType = MediaType.TEXT_PLAIN_VALUE)), - @ApiResponse(responseCode = "409", description = "User already exists", content = @Content(mediaType = MediaType.TEXT_PLAIN_VALUE)), - @ApiResponse(responseCode = "500", description = "Another error has occurred", content = @Content(mediaType = MediaType.TEXT_PLAIN_VALUE)) - }) - @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/CurrencyExchangeRequest.java b/src/main/java/eu/ztsh/wymiana/web/model/CurrencyExchangeRequest.java deleted file mode 100644 index c999dc5..0000000 --- a/src/main/java/eu/ztsh/wymiana/web/model/CurrencyExchangeRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package eu.ztsh.wymiana.web.model; - -import eu.ztsh.wymiana.model.Symbol; -import eu.ztsh.wymiana.validation.ValidExchangeRequest; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.Builder; -import org.hibernate.validator.constraints.pl.PESEL; - -import java.math.BigDecimal; - -@Builder -@ValidExchangeRequest -public record CurrencyExchangeRequest( - @NotNull @PESEL String pesel, - @NotNull Symbol from, - @NotNull Symbol to, - @Min(0) BigDecimal toBuy, - @Min(0) BigDecimal toSell -) { - -} 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 0614f6b..35c7a94 100644 --- a/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java +++ b/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java @@ -3,16 +3,11 @@ package eu.ztsh.wymiana.web.model; 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; -import java.math.BigDecimal; - -@Builder public record UserCreateRequest( @NotNull String name, @NotNull String surname, - @PESEL @Adult @NotNull String pesel, - @NotNull @Min(0) BigDecimal initial) { + @Adult String pesel, + @Min(0) double pln) { } diff --git a/src/main/java/eu/ztsh/wymiana/web/model/UserResponse.java b/src/main/java/eu/ztsh/wymiana/web/model/UserResponse.java deleted file mode 100644 index 2e73267..0000000 --- a/src/main/java/eu/ztsh/wymiana/web/model/UserResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.ztsh.wymiana.web.model; - -import eu.ztsh.wymiana.model.Currency; - -import java.util.List; - -public record UserResponse(String name, String surname, String pesel, List currencies) { - -} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 84ae7b8..ae6306d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,11 +2,6 @@ hsqldb: name: db port: 9090 -nbp: - baseurl: "http://api.nbp.pl" -currency: - initial: PLN - spring: datasource: username: sa @@ -16,15 +11,3 @@ spring: jpa: hibernate: ddl-auto: create - jackson: - mapper: - ACCEPT_CASE_INSENSITIVE_ENUMS: true - -management: - endpoints: - jmx: - exposure: - exclude: '*' - web: - exposure: - include: health diff --git a/src/main/resources/schema/rates.json b/src/main/resources/schema/rates.json deleted file mode 100644 index 2ed42d0..0000000 --- a/src/main/resources/schema/rates.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "$id": "https://api.nbp.pl/c/rates.json", - "$schema": "http://json-schema.org/draft/2020-12/schema", - "type": "object", - "def": { - "rate": { - "type": "object", - "properties": { - "no": { - "type": "string" - }, - "effectiveDate": { - "type": "string" - }, - "bid": { - "type": "number" - }, - "ask": { - "type": "number" - } - }, - "required": [ - "no", - "effectiveDate", - "bid", - "ask" - ] - } - }, - "properties": { - "table": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "code": { - "type": "string" - }, - "rates": { - "type": "array", - "items": { - "$ref": "#/def/rate" - } - } - }, - "required": [ - "table", - "currency", - "code", - "rates" - ] -} diff --git a/src/test/java/eu/ztsh/wymiana/EntityCreator.java b/src/test/java/eu/ztsh/wymiana/EntityCreator.java index 0f3b662..77ae953 100644 --- a/src/test/java/eu/ztsh/wymiana/EntityCreator.java +++ b/src/test/java/eu/ztsh/wymiana/EntityCreator.java @@ -2,20 +2,10 @@ 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.Symbol; -import eu.ztsh.wymiana.model.User; -import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; import eu.ztsh.wymiana.web.model.UserCreateRequest; -import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; public class EntityCreator { @@ -23,64 +13,18 @@ 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 BigDecimal PLN = createScaled(20.10, 2); - public static BigDecimal USD_SELL = createScaled(5.18, 2); - public static BigDecimal USD_BUY = createScaled(5.08, 2); - public static BigDecimal BUY_RATE = createScaled(3.8804, 4); - public static BigDecimal SELL_RATE = createScaled(3.9572, 4); + public static double PLN = 20.10; } - public static UserEntityBuilder userEntity() { + public static UserEntityBuilder user() { return new UserEntityBuilder(); } - public static User user() { - return user(Constants.PLN, BigDecimal.ZERO); - } - - public static User user(BigDecimal pln, BigDecimal usd) { - Map currencies = new HashMap<>(); - if (pln.signum() > 0) { - currencies.put(Symbol.PLN, new Currency(Symbol.PLN, pln)); - } - if (usd.signum() > 0) { - currencies.put(Symbol.USD, new Currency(Symbol.USD, usd)); - } - return new User(Constants.NAME, Constants.SURNAME, Constants.PESEL, currencies); - } - - public static UserCreateRequest.UserCreateRequestBuilder userRequest() { - return userRequest(null); - } - - public static UserCreateRequest.UserCreateRequestBuilder userRequest(Double initial) { - return UserCreateRequest.builder().name(Constants.NAME) - .surname(Constants.SURNAME) - .pesel(Constants.PESEL) - .initial(initial != null ? createScaled(initial, 2) : Constants.PLN); - } - - public static CurrencyExchangeRequest.CurrencyExchangeRequestBuilder exchangeRequest() { - return CurrencyExchangeRequest.builder().pesel(Constants.PESEL); - } - - public static Rates rates(String date) { - var rates = new Rates(); - rates.setTable("C"); - rates.setCurrency("dolar amerykański"); - rates.setCode(Symbol.USD.toString()); - var rate = new Rate(); - rate.setNo("096/C/NBP/2024"); - rate.setEffectiveDate(date); - rate.setBid(Constants.BUY_RATE); - rate.setAsk(Constants.SELL_RATE); - rates.setRates(List.of(rate)); - return rates; + public static UserCreateRequest userRequest() { + return new UserCreateRequest(Constants.NAME, Constants.SURNAME, Constants.PESEL, Constants.PLN); } public static class UserEntityBuilder { @@ -88,8 +32,8 @@ public class EntityCreator { String name; String surname; String pesel; - BigDecimal pln = BigDecimal.valueOf(-1); - BigDecimal usd = BigDecimal.valueOf(-1); + double pln; + double usd; public UserEntityBuilder name(String name) { this.name = name; @@ -107,34 +51,26 @@ public class EntityCreator { } public UserEntityBuilder pln(double pln) { - return this.pln(BigDecimal.valueOf(pln)); - } - - public UserEntityBuilder pln(BigDecimal pln) { - this.pln = pln.setScale(2, RoundingMode.HALF_UP); + this.pln = pln; return this; } public UserEntityBuilder usd(double usd) { - return this.usd(BigDecimal.valueOf(usd)); - } - - public UserEntityBuilder usd(BigDecimal usd) { - this.usd = usd.setScale(2, RoundingMode.HALF_UP); + this.usd = usd; return this; } public UserEntity build() { var nonnulPesel = Optional.ofNullable(pesel).orElse(Constants.PESEL); List currencies = new ArrayList<>(); - if (pln.signum() > -1) { - currencies.add(new CurrencyEntity(nonnulPesel, Symbol.PLN, pln)); + if (pln > 0) { + currencies.add(new CurrencyEntity(nonnulPesel, "PLN", pln)); } - if (usd.signum() > -1) { - currencies.add(new CurrencyEntity(nonnulPesel, Symbol.USD, usd)); + if (usd > 0) { + currencies.add(new CurrencyEntity(nonnulPesel, "USD", usd)); } if (currencies.isEmpty()) { - currencies.add(new CurrencyEntity(nonnulPesel, Symbol.PLN, Constants.PLN)); + currencies.add(new CurrencyEntity(nonnulPesel, "PLN", Constants.PLN)); } return new UserEntity( nonnulPesel, @@ -146,8 +82,4 @@ public class EntityCreator { } - private static BigDecimal createScaled(double input, int scale) { - return BigDecimal.valueOf(input).setScale(scale, RoundingMode.HALF_UP); - } - } diff --git a/src/test/java/eu/ztsh/wymiana/RepositoryBasedTest.java b/src/test/java/eu/ztsh/wymiana/RepositoryBasedTest.java index 5e0bc54..cd0ffcf 100644 --- a/src/test/java/eu/ztsh/wymiana/RepositoryBasedTest.java +++ b/src/test/java/eu/ztsh/wymiana/RepositoryBasedTest.java @@ -29,7 +29,6 @@ public abstract class RepositoryBasedTest { .isNotEmpty() .get() .usingRecursiveComparison() - .ignoringCollectionOrder() .isEqualTo(entity); } diff --git a/src/test/java/eu/ztsh/wymiana/WireMockExtension.java b/src/test/java/eu/ztsh/wymiana/WireMockExtension.java deleted file mode 100644 index 99ebc73..0000000 --- a/src/test/java/eu/ztsh/wymiana/WireMockExtension.java +++ /dev/null @@ -1,50 +0,0 @@ -package eu.ztsh.wymiana; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.github.tomakehurst.wiremock.client.WireMock; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.ExtensionContext; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; - -public class WireMockExtension implements BeforeAllCallback, AfterEachCallback, ExtensionContext.Store.CloseableResource { - - public static final String baseUrl = "http://localhost:38080"; - - public static void response(String endpoint, int status, String body) { - configureFor(38080); - stubFor(get(urlEqualTo(endpoint)) - .willReturn(WireMock.status(status) - .withHeader("Content-Type", "application/json") - .withBody(body))); - } - - public static void verifyGet(int count, String url) { - verify(exactly(count), getRequestedFor(urlEqualTo(url))); - } - - private static final WireMockServer wireMockServer = new WireMockServer(38080); - private boolean started; - - @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { - if (!started) { - wireMockServer.start(); - started = true; - } - } - @Override - public void afterEach(ExtensionContext extensionContext) throws Exception { - wireMockServer.listAllStubMappings().getMappings().forEach(wireMockServer::removeStub); - wireMockServer.findAllUnmatchedRequests().forEach(System.out::println); - wireMockServer.resetRequests(); - } - - @Override - public void close() throws Throwable { - wireMockServer.stop(); - } - - -} 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 20b7868..63cd478 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.userEntity().build(); + var entity = EntityCreator.user().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 deleted file mode 100644 index 14f7ea3..0000000 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ /dev/null @@ -1,215 +0,0 @@ -package eu.ztsh.wymiana.service; - -import eu.ztsh.wymiana.EntityCreator; -import eu.ztsh.wymiana.RepositoryBasedTest; -import eu.ztsh.wymiana.config.CurrencyProperties; -import eu.ztsh.wymiana.data.repository.UserRepository; -import eu.ztsh.wymiana.exception.InsufficientFundsException; -import eu.ztsh.wymiana.exception.UserNotFoundException; -import eu.ztsh.wymiana.model.Symbol; -import eu.ztsh.wymiana.validation.InstanceValidator; -import eu.ztsh.wymiana.validation.ValidationFailedException; -import jakarta.transaction.Transactional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; - -import java.math.BigDecimal; -import java.util.Objects; -import java.util.stream.Stream; - -import static eu.ztsh.wymiana.EntityCreator.Constants.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class CurrencyServiceTest extends RepositoryBasedTest { - - private final CurrencyService currencyService; - - @Autowired - public CurrencyServiceTest(UserRepository userRepository, InstanceValidator instanceValidator, - CurrencyProperties currencyProperties) { - super(userRepository); - var nbp = Mockito.mock(NbpService.class); - Mockito.when(nbp.getSellRate(Symbol.USD)).thenReturn(SELL_RATE); - Mockito.when(nbp.getBuyRate(Symbol.USD)).thenReturn(BUY_RATE); - currencyService = new CurrencyService(new UserService(userRepository, instanceValidator, currencyProperties), - nbp, instanceValidator); - } - - @Transactional - @Test - void plnToUsdToSellSuccessTest() { - var entity = EntityCreator.userEntity().build(); - userRepository.save(entity); - var result = currencyService.exchange(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toSell(PLN) - .build()); - assertThat(result.currencies()) - .matches(map -> map.get(Symbol.PLN).amount().signum() == 0 && Objects.equals(map.get(Symbol.USD).amount(), USD_BUY), "USD 5.08"); - var expected = EntityCreator.userEntity().pln(0).usd(USD_BUY).build(); - expect(expected); - } - - @Transactional - @Test - void plnToUsdToBuySuccessTest() { - var entity = EntityCreator.userEntity().build(); - userRepository.save(entity); - var result = currencyService.exchange(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toBuy(USD_BUY) - .build()); - assertThat(result.currencies()) - .matches(map -> map.get(Symbol.PLN).amount().signum() == 0 && Objects.equals(map.get(Symbol.USD).amount(), USD_BUY)); - var expected = EntityCreator.userEntity().pln(0).usd(USD_BUY).build(); - expect(expected); - } - - @Transactional - @Test - void usdToPlnToSellSuccessTest() { - var entity = EntityCreator.userEntity().pln(-1).usd(USD_SELL).build(); - userRepository.save(entity); - var result = currencyService.exchange(EntityCreator.exchangeRequest() - .from(Symbol.USD) - .to(Symbol.PLN) - .toSell(USD_SELL) - .build()); - assertThat(result.currencies()) - .matches(map -> Objects.equals(map.get(Symbol.PLN).amount(), PLN) && map.get(Symbol.USD).amount().signum() == 0, "PLN 20.10"); - var expected = EntityCreator.userEntity().pln(PLN).usd(0).build(); - expect(expected); - } - - @Transactional - @Test - void usdToPlnToBuySuccessTest() { - var entity = EntityCreator.userEntity().pln(-1).usd(USD_SELL).build(); - userRepository.save(entity); - var result = currencyService.exchange(EntityCreator.exchangeRequest() - .from(Symbol.USD) - .to(Symbol.PLN) - .toBuy(PLN) - .build()); - assertThat(result.currencies()) - .matches(map -> Objects.equals(map.get(Symbol.PLN).amount(), PLN) && map.get(Symbol.USD).amount().signum() == 0, "PLN 20.10"); - var expected = EntityCreator.userEntity().pln(PLN).usd(0).build(); - expect(expected); - } - - @Transactional - @Test - void usdToPlnNoUsdCurrencyTest() { - var entity = EntityCreator.userEntity().build(); - userRepository.save(entity); - var request = EntityCreator.exchangeRequest() - .from(Symbol.USD) - .to(Symbol.PLN) - .toBuy(PLN) - .build(); - assertThatThrownBy(() -> currencyService.exchange(request)) - .isInstanceOf(InsufficientFundsException.class); - } - - @Transactional - @Test - void doubleExchangeTest() { - var initialValue = BigDecimal.valueOf(100); - var entity = EntityCreator.userEntity().pln(initialValue).build(); - userRepository.save(entity); - currencyService.exchange(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toBuy(USD_BUY) - .build()); - currencyService.exchange(EntityCreator.exchangeRequest() - .from(Symbol.USD) - .to(Symbol.PLN) - .toSell(USD_BUY) - .build()); - var resultOptional = userRepository.findById(entity.getPesel()); - assertThat(resultOptional) - .isNotEmpty(); - var resultEntity = resultOptional.get(); - assertThat(resultEntity.getCurrencies() - .stream() - .filter(c -> c.getSymbol().equals(Symbol.PLN)) - .findFirst()).isNotEmpty().get().matches(currencyEntity -> currencyEntity.getAmount().compareTo(initialValue) < 0); - } - - @Transactional - @Test - void insufficientFundsTest() { - var entity = EntityCreator.userEntity().build(); - userRepository.save(entity); - var request = EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toBuy(PLN) - .build(); - assertThatThrownBy(() -> currencyService.exchange(request)) - .isInstanceOf(InsufficientFundsException.class); - } - - @DisplayName("Invalid PESEL value") - @ParameterizedTest - @MethodSource - void invalidPeselTest(String pesel) { - var entity = EntityCreator.exchangeRequest() - .pesel(pesel) - .from(Symbol.PLN) - .to(Symbol.USD) - .toSell(USD_SELL) - .build(); - assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class) - .hasMessageContaining("PESEL"); - } - - @Test - void notExistingUserTest() { - var entity = EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toSell(USD_SELL) - .build(); - assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(UserNotFoundException.class); - } - - @Test - @DisplayName("Empty 'from' value") - void emptyFromTest() { - var entity = EntityCreator.exchangeRequest() - .to(Symbol.USD) - .toSell(USD_SELL) - .build(); - assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class) - .hasMessageContaining("null"); - } - - @Test - @DisplayName("Empty 'to' value") - void emptyToTest() { - var entity = EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .toSell(USD_SELL) - .build(); - assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class) - .hasMessageContaining("null"); - } - - private static Stream invalidPeselTest() { - return Stream.of("INVALID", INVALID_PESEL); - } - -} diff --git a/src/test/java/eu/ztsh/wymiana/service/NbpServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/NbpServiceTest.java deleted file mode 100644 index 0ec2838..0000000 --- a/src/test/java/eu/ztsh/wymiana/service/NbpServiceTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package eu.ztsh.wymiana.service; - -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.exception.NoDataException; -import eu.ztsh.wymiana.model.Symbol; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.mockito.Mockito; -import org.springframework.web.client.RestClient; - -import java.time.Clock; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.Month; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAdjusters; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -@ExtendWith(WireMockExtension.class) -class NbpServiceTest { - - private static final ZoneId zone = ZoneId.of("Europe/Warsaw"); - private static final LocalDate today = LocalDate.of(2024, Month.MAY, 12); // Sunday - private static Clock clock; - private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - private final RestClient restClient = RestClient.builder().baseUrl(WireMockExtension.baseUrl).build(); - private NbpService nbpService; - - @BeforeAll - static void prepare() { - clock = Mockito.mock(Clock.class); - Mockito.when(clock.getZone()).thenReturn(zone); - } - - @BeforeEach - void prepareTest() { - nbpService = new NbpService(clock, restClient); - } - - @DisplayName("Check if fetch date is calculated properly: weekdays") - @ParameterizedTest - @EnumSource(value = DayOfWeek.class, names = {"SATURDAY", "SUNDAY"}, mode = EnumSource.Mode.EXCLUDE) - void getFetchDateOnWorkingDayTest(DayOfWeek dayOfWeek) { - updateClock(dayOfWeek); - assertThat(nbpService.getFetchDate()).isEqualTo( - switch (dayOfWeek) { - case MONDAY -> LocalDate.of(2024, Month.MAY, 6); - case TUESDAY -> LocalDate.of(2024, Month.MAY, 7); - case WEDNESDAY -> LocalDate.of(2024, Month.MAY, 8); - case THURSDAY -> LocalDate.of(2024, Month.MAY, 9); - case FRIDAY -> LocalDate.of(2024, Month.MAY, 10); - default -> null; - } - ); - } - - @DisplayName("Check if fetch date is calculated properly: weekends") - @ParameterizedTest - @EnumSource(value = DayOfWeek.class, names = {"SATURDAY", "SUNDAY"}) - void getFetchDateOnWeekendTest(DayOfWeek dayOfWeek) { - updateClock(dayOfWeek); - assertThat(nbpService.getFetchDate()).isEqualTo(LocalDate.of(2024, Month.MAY, 10)); - } - - @DisplayName("Fetch rates straight from server") - @Test - void getWithoutCacheTest() throws JsonProcessingException { - var date = dtf.format(updateClock(DayOfWeek.FRIDAY)); - var url = "/api/exchangerates/rates/c/usd/%s/".formatted(date); - WireMockExtension.response(url, 200, new ObjectMapper().writeValueAsString(EntityCreator.rates(date))); - try { - assertThat(nbpService.getSellRate(Symbol.USD)).isEqualTo(EntityCreator.Constants.SELL_RATE); - } finally { - WireMockExtension.verifyGet(1, url); - } - } - - @DisplayName("Fetch rates from cache") - @Test - void getWithCacheTest() throws JsonProcessingException { - var date = dtf.format(updateClock(DayOfWeek.FRIDAY)); - var url = "/api/exchangerates/rates/c/usd/%s/".formatted(date); - WireMockExtension.response(url, 200, new ObjectMapper().writeValueAsString(EntityCreator.rates(date))); - // save to cache - assertThat(nbpService.getSellRate(Symbol.USD)).isEqualTo(EntityCreator.Constants.SELL_RATE); - // get from cache - assertThat(nbpService.getBuyRate(Symbol.USD)).isEqualTo(EntityCreator.Constants.BUY_RATE); - WireMockExtension.verifyGet(1, url); - } - - @DisplayName("Support 404: invalid currency or no data") - @Test - void getInvalidCurrencyTest() { - var date = dtf.format(updateClock(DayOfWeek.FRIDAY)); - var url = "/api/exchangerates/rates/c/usd/%s/".formatted(date); - WireMockExtension.response(url, 404, "404 NotFound - Not Found - Brak danych"); - assertThatThrownBy(() -> nbpService.getSellRate(Symbol.USD)).isInstanceOf(NoDataException.class); - WireMockExtension.verifyGet(1, url); - } - - private LocalDate updateClock(DayOfWeek dayOfWeek) { - var date = today.with(TemporalAdjusters.previousOrSame(dayOfWeek)); - Mockito.when(clock.instant()).thenReturn(date.atStartOfDay(zone).toInstant()); - return LocalDate.from(date); - } - -} diff --git a/src/test/java/eu/ztsh/wymiana/service/UserServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/UserServiceTest.java index 33ea330..4a9b94b 100644 --- a/src/test/java/eu/ztsh/wymiana/service/UserServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/UserServiceTest.java @@ -2,15 +2,10 @@ package eu.ztsh.wymiana.service; import eu.ztsh.wymiana.EntityCreator; import eu.ztsh.wymiana.RepositoryBasedTest; -import eu.ztsh.wymiana.config.CurrencyProperties; import eu.ztsh.wymiana.data.repository.UserRepository; import eu.ztsh.wymiana.exception.UserAlreadyExistsException; import eu.ztsh.wymiana.util.UserMapper; -import eu.ztsh.wymiana.validation.Adult; -import eu.ztsh.wymiana.validation.InstanceValidator; -import eu.ztsh.wymiana.validation.ValidationFailedException; import jakarta.transaction.Transactional; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,60 +17,33 @@ class UserServiceTest extends RepositoryBasedTest { private final UserService userService; @Autowired - public UserServiceTest(UserRepository userRepository, InstanceValidator instanceValidator, - CurrencyProperties currencyProperties) { + public UserServiceTest(UserRepository userRepository) { super(userRepository); - userService = new UserService(userRepository, instanceValidator, currencyProperties); + userService = new UserService(userRepository); } @Test @Transactional - @DisplayName("Create new user") void createNewUserTest() { - userService.create(EntityCreator.userRequest().build()); - var entity = EntityCreator.userEntity().build(); + userService.create(EntityCreator.userRequest()); + var entity = EntityCreator.user().build(); expect(entity); } @Test - @DisplayName("Try to create user that already exists") - void createDuplicatedUserTest() { - var first = EntityCreator.userRequest().build(); - var second = EntityCreator.userRequest().build(); + void createDuplicatedUser() { + var first = EntityCreator.userRequest(); + var second = EntityCreator.userRequest(); userService.create(first); assertThatThrownBy(() -> userService.create(second)) .isInstanceOf(UserAlreadyExistsException.class) .hasMessage("User with PESEL %s already exists".formatted(EntityCreator.Constants.PESEL)); } - @Test - @DisplayName("Try to create another user with same PESEL") - void createDuplicatedPeselTest() { - var first = EntityCreator.userRequest().build(); - var second = EntityCreator.userRequest(30.30) - .name("Jan") - .surname("Kowalski") - .build(); - userService.create(first); - assertThatThrownBy(() -> userService.create(second)) - .isInstanceOf(UserAlreadyExistsException.class) - .hasMessage("User with PESEL %s already exists".formatted(EntityCreator.Constants.PESEL)); - } - - @Test - @DisplayName("Try to create too young user") - void youngUserTest() { - var request = EntityCreator.userRequest().pesel("").build(); - assertThatThrownBy(() -> userService.create(request)) - .isInstanceOf(ValidationFailedException.class) - .hasMessageContaining(Adult.MESSAGE); - } - @Test @Transactional - @DisplayName("Get existing user") void getExistingUserTest() { - var entity = EntityCreator.userEntity().build(); + var entity = EntityCreator.user().build(); userRepository.save(entity); var userOptional = userService.get(EntityCreator.Constants.PESEL); var expected = UserMapper.entityToPojo(entity); @@ -87,7 +55,6 @@ class UserServiceTest extends RepositoryBasedTest { } @Test - @DisplayName("Try get non-existing user") void getNonExistingUserTest() { var userOptional = userService.get(EntityCreator.Constants.PESEL); assertThat(userOptional).isEmpty(); diff --git a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java index 121c03d..f3fc91d 100644 --- a/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java +++ b/src/test/java/eu/ztsh/wymiana/util/UserMapperTest.java @@ -1,38 +1,34 @@ package eu.ztsh.wymiana.util; import eu.ztsh.wymiana.EntityCreator; -import eu.ztsh.wymiana.model.Symbol; -import eu.ztsh.wymiana.model.UserCreateRequestConfiguredWrapper; +import eu.ztsh.wymiana.model.Currency; +import eu.ztsh.wymiana.model.User; import org.junit.jupiter.api.Test; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; class UserMapperTest { @Test void entityToPojoTest() { - var entity = EntityCreator.userEntity().build(); - var expected = EntityCreator.user(); + 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)) + ); assertThat(UserMapper.entityToPojo(entity)) .usingRecursiveComparison() .isEqualTo(expected); } - @Test - void pojoToEntityTest() { - var entity = EntityCreator.user(); - var expected = EntityCreator.userEntity().build(); - assertThat(UserMapper.pojoToEntity(entity)) - .usingRecursiveComparison() - .isEqualTo(expected); - } - @Test void requestToEntityTest() { - var request = UserCreateRequestConfiguredWrapper.wrap(EntityCreator.userRequest().build()) - .withInitial(Symbol.PLN) - .build() ; - var expected = EntityCreator.userEntity().build(); + var request = EntityCreator.userRequest(); + var expected = EntityCreator.user().build(); assertThat(UserMapper.requestToEntity(request)) .usingRecursiveComparison() .isEqualTo(expected); diff --git a/src/test/java/eu/ztsh/wymiana/validation/AdultValidatorTest.java b/src/test/java/eu/ztsh/wymiana/validation/AdultValidatorTest.java index d4a2ed1..ca24718 100644 --- a/src/test/java/eu/ztsh/wymiana/validation/AdultValidatorTest.java +++ b/src/test/java/eu/ztsh/wymiana/validation/AdultValidatorTest.java @@ -1,42 +1,58 @@ package eu.ztsh.wymiana.validation; +import jakarta.validation.ConstraintValidatorContext; +import org.hibernate.validator.internal.engine.DefaultClockProvider; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; -class AdultValidatorTest extends ValidatorTest { +import static org.assertj.core.api.Assertions.assertThat; - protected AdultValidatorTest() { - super(new AdultValidator()); +class AdultValidatorTest { + + private static AdultValidator validator; + private static ConstraintValidatorContext validatorContext; + + @BeforeAll + static void prepare() { + validator = new AdultValidator(); + validatorContext = Mockito.mock(ConstraintValidatorContext.class); + Mockito.when(validatorContext.getClockProvider()).thenReturn(DefaultClockProvider.INSTANCE); } @Test @DisplayName("No digits in PESEL") void invalidPatternTest() { - assertThatValidation("notAPesel").isFalse(); + assertThat(call("notAPesel")).isFalse(); } @Test @DisplayName("Not an adult") void notAnAdultTest() { - assertThatValidation("24242400000").isFalse(); + assertThat(call("24242400000")).isFalse(); } @Test @DisplayName("Adult") void adultTest() { - assertThatValidation("88010100000").isTrue(); + assertThat(call("88010100000")).isTrue(); } @Test @DisplayName("Elderly person") void seniorTest() { - assertThatValidation("00010100000").isTrue(); + assertThat(call("00010100000")).isTrue(); } @Test @DisplayName("Invalid date") void notAValidDateTest() { - assertThatValidation("00919100000").isFalse(); + assertThat(call("00919100000")).isFalse(); + } + + private boolean call(String value) { + return validator.isValid(value, validatorContext); } } diff --git a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java deleted file mode 100644 index 4edd903..0000000 --- a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package eu.ztsh.wymiana.validation; - -import eu.ztsh.wymiana.EntityCreator; -import eu.ztsh.wymiana.model.Symbol; -import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; - -import static eu.ztsh.wymiana.EntityCreator.Constants.USD_BUY; -import static eu.ztsh.wymiana.EntityCreator.Constants.USD_SELL; - -class ValidExchangeRequestValidatorTest extends ValidatorTest { - - protected ValidExchangeRequestValidatorTest() { - super(new ValidExchangeRequestValidator()); - } - - @Test - @DisplayName("Valid request with buy value specified") - void validRequestWithBuyTest() { - assertThatValidation(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toBuy(USD_BUY) - .build()).isTrue(); - } - - @Test - @DisplayName("Valid request with sell value specified") - void validRequestWithSellTest() { - assertThatValidation(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toSell(USD_SELL) - .build()).isTrue(); - } - - @Test - @DisplayName("From and To have same value") - void sameFromToTest() { - assertThatValidation(EntityCreator.exchangeRequest() - .from(Symbol.USD) - .to(Symbol.USD) - .toSell(USD_SELL) - .build()).isFalse(); - } - - @Test - @DisplayName("Empty amounts") - void emptyBuySellTest() { - assertThatValidation(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .build()).isFalse(); - } - - @Test - @DisplayName("Both Buy and Sell params filled in") - void bothFilledBuySellTest() { - assertThatValidation(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toBuy(USD_BUY) - .toSell(USD_SELL) - .build()).isFalse(); - } - - @Test - @DisplayName("Negative buy amount value") - void negativeBuyAmountTest() { - assertThatValidation(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toBuy(BigDecimal.valueOf(-1.0)) - .build()).isFalse(); - } - - @Test - @DisplayName("Negative sell amount value") - void negativeSellAmountTest() { - assertThatValidation(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toSell(BigDecimal.valueOf(-1.0)) - .build()).isFalse(); - } - -} diff --git a/src/test/java/eu/ztsh/wymiana/validation/ValidatorTest.java b/src/test/java/eu/ztsh/wymiana/validation/ValidatorTest.java deleted file mode 100644 index 7dc4bd8..0000000 --- a/src/test/java/eu/ztsh/wymiana/validation/ValidatorTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package eu.ztsh.wymiana.validation; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import org.assertj.core.api.AbstractBooleanAssert; -import org.hibernate.validator.internal.engine.DefaultClockProvider; -import org.junit.jupiter.api.BeforeAll; -import org.mockito.Mockito; - -import static org.assertj.core.api.Assertions.assertThat; - -public abstract class ValidatorTest, C> { - - private final V validator; - private static ConstraintValidatorContext validatorContext; - - protected ValidatorTest(V validator) { - this.validator = validator; - } - - @BeforeAll - static void prepare() { - validatorContext = Mockito.mock(ConstraintValidatorContext.class); - Mockito.when(validatorContext.getClockProvider()).thenReturn(DefaultClockProvider.INSTANCE); - } - - protected AbstractBooleanAssert assertThatValidation(C value) { - return assertThat(validator.isValid(value, validatorContext)); - } - -} diff --git a/src/test/java/eu/ztsh/wymiana/web/ApplicationIntegrationTests.java b/src/test/java/eu/ztsh/wymiana/web/ApplicationIntegrationTests.java deleted file mode 100644 index 9a14ef5..0000000 --- a/src/test/java/eu/ztsh/wymiana/web/ApplicationIntegrationTests.java +++ /dev/null @@ -1,264 +0,0 @@ -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.Symbol; -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.math.BigDecimal; -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(100D).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.pojoToResponse(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(Symbol.PLN) - .to(Symbol.USD) - .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(BigDecimal.valueOf(100).subtract(PLN), USD_BUY)); - webTestClient.post() - .uri(endpoint) - .bodyValue(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .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(Symbol.PLN) - .to(Symbol.USD) - .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(Symbol.PLN) - .to(Symbol.USD) - .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(Symbol.USD) - .to(Symbol.PLN) - .toBuy(PLN) - .build()) - .exchange() - .expectStatus().isBadRequest(); - } - - @Test - @DisplayName("03.6: Perform valid money exchange with lower case currency symbols") - void ignoreCaseTest() { - var date = getTodayOrLastFriday(); - var expected = asJson(EntityCreator.user(BigDecimal.valueOf(100).subtract(PLN.multiply(BigDecimal.TWO)), USD_BUY.multiply(BigDecimal.TWO))); - webTestClient.post() - .uri(endpoint) - .header("Content-Type", "application/json") - .bodyValue(asJson(EntityCreator.exchangeRequest() - .from(Symbol.PLN) - .to(Symbol.USD) - .toSell(PLN) - .build()).replace("USD", "usd").replace("PLN", "pln")) - .exchange() - .expectStatus().isOk() - .expectBody().json(expected); - // cache - WireMockExtension.verifyGet(0, URI_PATTERN.formatted(date)); - } - - 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 deleted file mode 100644 index 198e366..0000000 --- a/src/test/resources/application-it.yaml +++ /dev/null @@ -1,2 +0,0 @@ -nbp: - baseurl: "http://localhost:38080"