From c87bcc8b54af6c9658c61b252a8adee7a0a40203 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Fri, 24 May 2024 16:25:19 +0200 Subject: [PATCH] test: CurrencyServiceTest completed? --- .../exception/ExchangeFailedException.java | 13 ++++ .../ztsh/wymiana/service/CurrencyService.java | 50 ++++++++++++++- .../eu/ztsh/wymiana/service/NbpService.java | 4 +- .../eu/ztsh/wymiana/service/UserService.java | 4 ++ .../ValidExchangeRequestValidator.java | 3 +- .../wymiana/service/CurrencyServiceTest.java | 63 +++++++++++++++++-- 6 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java diff --git a/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java b/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java new file mode 100644 index 0000000..481d330 --- /dev/null +++ b/src/main/java/eu/ztsh/wymiana/exception/ExchangeFailedException.java @@ -0,0 +1,13 @@ +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/service/CurrencyService.java b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java index 53d401e..a26b0bc 100644 --- a/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java +++ b/src/main/java/eu/ztsh/wymiana/service/CurrencyService.java @@ -1,11 +1,19 @@ package eu.ztsh.wymiana.service; +import eu.ztsh.wymiana.exception.ExchangeFailedException; +import eu.ztsh.wymiana.exception.InsufficientFundsException; +import eu.ztsh.wymiana.model.Currency; 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.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + @RequiredArgsConstructor @Service public class CurrencyService { @@ -15,7 +23,47 @@ public class CurrencyService { private final InstanceValidator validator; public User exchange(CurrencyExchangeRequest request) { - throw new UnsupportedOperationException("Not implemented yet"); + validator.validate(request); + return userService.get(request.pesel()).map(user -> { + if (!request.from().equalsIgnoreCase("PLN") && !request.to().equalsIgnoreCase("PLN")) { + throw new ExchangeFailedException("Either 'from' or 'to' has to be PLN"); + } + // As we support only USD now, we need to limit second parameter too + // Begin: unlock other currencies + if (!request.from().equalsIgnoreCase("USD") && !request.to().equalsIgnoreCase("USD")) { + throw new ExchangeFailedException("Either 'from' or 'to' has to be USD"); + } + // End: unlock other currencies + + var exchanged = performExchange(user.currencies().get(request.from().toUpperCase()), + user.currencies().get(request.to().toUpperCase()), + Optional.ofNullable(request.toSell()).orElse(0D), + Optional.ofNullable(request.toBuy()).orElse(0D)); + user.currencies().putAll(exchanged); + return userService.update(user); + }) + .orElseThrow(ExchangeFailedException::new); + } + + private Map performExchange(Currency from, Currency to, double toSell, double toBuy) { + double exchangeRate; + double neededFromAmount; + double requestedToAmount; + if (from.symbol().equalsIgnoreCase("PLN")) { + exchangeRate = nbpService.getSellRate(to.symbol()); + neededFromAmount = toBuy != 0 ? toBuy * exchangeRate : toSell; + requestedToAmount = toBuy != 0 ? toBuy : toSell / exchangeRate; + } else { + exchangeRate = nbpService.getSellRate(from.symbol()); + neededFromAmount = toBuy != 0 ? toBuy / exchangeRate : toSell; + requestedToAmount = toBuy != 0 ? toBuy : toSell * exchangeRate; + } + if (neededFromAmount > from.amount()) { + throw new InsufficientFundsException(); + } + var newFrom = new Currency(from.symbol(), from.amount() - neededFromAmount); + var newTo = new Currency(to.symbol(), to.amount() + requestedToAmount); + return Stream.of(newFrom, newTo).collect(Collectors.toMap(Currency::symbol, currency -> currency)); } } diff --git a/src/main/java/eu/ztsh/wymiana/service/NbpService.java b/src/main/java/eu/ztsh/wymiana/service/NbpService.java index da26238..e612e0a 100644 --- a/src/main/java/eu/ztsh/wymiana/service/NbpService.java +++ b/src/main/java/eu/ztsh/wymiana/service/NbpService.java @@ -31,11 +31,11 @@ public class NbpService { private final ConcurrentMap cache = new ConcurrentHashMap<>(1); public double getSellRate(String currency) { - return getCurrency(currency).sell(); + return getCurrency(currency.toUpperCase()).sell(); } public double getBuyRate(String currency) { - return getCurrency(currency).buy(); + return getCurrency(currency.toUpperCase()).buy(); } private synchronized RatesCache getCurrency(String currency) { diff --git a/src/main/java/eu/ztsh/wymiana/service/UserService.java b/src/main/java/eu/ztsh/wymiana/service/UserService.java index d23dc2c..e5b26f6 100644 --- a/src/main/java/eu/ztsh/wymiana/service/UserService.java +++ b/src/main/java/eu/ztsh/wymiana/service/UserService.java @@ -30,4 +30,8 @@ public class UserService { return userRepository.findById(pesel).map(UserMapper::entityToPojo); } + public User update(User user) { + throw new UnsupportedOperationException("Not implemented yet"); + } + } diff --git a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java index 0895d92..143876b 100644 --- a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java +++ b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java @@ -14,7 +14,8 @@ public class ValidExchangeRequestValidator implements return false; } - return !request.from().equals(request.to()) + // 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() >= 0) diff --git a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java index c26f5ad..13ae4ec 100644 --- a/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java +++ b/src/test/java/eu/ztsh/wymiana/service/CurrencyServiceTest.java @@ -3,9 +3,11 @@ package eu.ztsh.wymiana.service; import eu.ztsh.wymiana.EntityCreator; import eu.ztsh.wymiana.RepositoryBasedTest; import eu.ztsh.wymiana.data.repository.UserRepository; +import eu.ztsh.wymiana.exception.ExchangeFailedException; import eu.ztsh.wymiana.exception.InsufficientFundsException; import eu.ztsh.wymiana.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; @@ -32,8 +34,11 @@ class CurrencyServiceTest extends RepositoryBasedTest { currencyService = new CurrencyService(new UserService(userRepository, instanceValidator), nbp, instanceValidator); } + @Transactional @Test - void toSellSuccessTest() { + void plnToUsdToSellSuccessTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(PLN_SYMBOL) .to(USD_SYMBOL) @@ -45,8 +50,11 @@ class CurrencyServiceTest extends RepositoryBasedTest { expect(expected); } + @Transactional @Test - void toBuySuccessTest() { + void plnToUsdToBuySuccessTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); var result = currencyService.exchange(EntityCreator.exchangeRequest() .from(PLN_SYMBOL) .to(USD_SYMBOL) @@ -57,6 +65,37 @@ class CurrencyServiceTest extends RepositoryBasedTest { var expected = EntityCreator.user().pln(0).usd(USD_BUY).build(); expect(expected); } + @Transactional + @Test + void usdToPlnToSellSuccessTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); + var result = currencyService.exchange(EntityCreator.exchangeRequest() + .from(USD_SYMBOL) + .to(PLN_SYMBOL) + .toSell(USD_SELL) + .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(); + expect(expected); + } + + @Transactional + @Test + void usdToPlnToBuySuccessTest() { + var entity = EntityCreator.user().build(); + userRepository.save(entity); + var result = currencyService.exchange(EntityCreator.exchangeRequest() + .from(USD_SYMBOL) + .to(PLN_SYMBOL) + .toBuy(PLN) + .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(); + expect(expected); + } @Test void insufficientFundsTest() { @@ -80,7 +119,19 @@ class CurrencyServiceTest extends RepositoryBasedTest { .toSell(USD_SELL) .build(); assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class); + .isInstanceOf(ValidationFailedException.class) + .hasMessageContaining("PESEL"); + } + + @Test + void notExistingUserTest() { + var entity = EntityCreator.exchangeRequest() + .from(PLN_SYMBOL) + .to(USD_SYMBOL) + .toSell(USD_SELL) + .build(); + assertThatThrownBy(() -> currencyService.exchange(entity)) + .isInstanceOf(ExchangeFailedException.class); } @Test @@ -91,7 +142,8 @@ class CurrencyServiceTest extends RepositoryBasedTest { .toSell(USD_SELL) .build(); assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class); + .isInstanceOf(ValidationFailedException.class) + .hasMessageContaining("null"); } @Test @@ -102,7 +154,8 @@ class CurrencyServiceTest extends RepositoryBasedTest { .toSell(USD_SELL) .build(); assertThatThrownBy(() -> currencyService.exchange(entity)) - .isInstanceOf(ValidationFailedException.class); + .isInstanceOf(ValidationFailedException.class) + .hasMessageContaining("null"); } private static Stream invalidPeselTest() {