cache = new ConcurrentHashMap<>(1);
-
- public double getSellRate(String currency) {
- return getCurrency(currency).sell();
- }
-
- public double getBuyRate(String currency) {
- return getCurrency(currency).buy();
- }
-
- private synchronized RatesCache getCurrency(String 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().get(0);
- cacheObject = new RatesCache(
- LocalDate.parse(rate.getEffectiveDate(), dtf),
- rate.getBid(),
- rate.getAsk()
- );
- cache.put(fresh.getCode(), 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(String code, String date) {
- return restClient.get().uri(URI_PATTERN, code.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, double buy, double sell) {
-
- }
-
-}
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 extends Payload>[] 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 0895d92..0000000
--- a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java
+++ /dev/null
@@ -1,24 +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;
- }
-
- return !request.from().equals(request.to())
- && !((request.toBuy() == null && request.toSell() == null)
- || (request.toBuy() != null && request.toSell() != null))
- && ((request.toBuy() != null && request.toBuy() >= 0)
- || (request.toSell() != null && request.toSell() >= 0));
- }
-
-}
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 54f192e..0000000
--- a/src/main/java/eu/ztsh/wymiana/web/model/CurrencyExchangeRequest.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package eu.ztsh.wymiana.web.model;
-
-import eu.ztsh.wymiana.validation.ValidExchangeRequest;
-import jakarta.validation.constraints.NotNull;
-import lombok.Builder;
-import org.hibernate.validator.constraints.pl.PESEL;
-
-@Builder
-@ValidExchangeRequest
-public record CurrencyExchangeRequest(
- @PESEL String pesel,
- @NotNull String from,
- @NotNull String to,
- Double toBuy,
- Double toSell
-) {
-
-}
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 df78b20..4f7fcd7 100644
--- a/src/test/java/eu/ztsh/wymiana/EntityCreator.java
+++ b/src/test/java/eu/ztsh/wymiana/EntityCreator.java
@@ -2,9 +2,6 @@ package eu.ztsh.wymiana;
import eu.ztsh.wymiana.data.entity.CurrencyEntity;
import eu.ztsh.wymiana.data.entity.UserEntity;
-import eu.ztsh.wymiana.model.Rate;
-import eu.ztsh.wymiana.model.Rates;
-import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest;
import eu.ztsh.wymiana.web.model.UserCreateRequest;
import java.util.ArrayList;
@@ -19,12 +16,6 @@ public class EntityCreator {
public static String NAME = "Janina";
public static String SURNAME = "Kowalska";
public static double PLN = 20.10;
- public static double USD_SELL = 5.18;
- public static double USD_BUY = 5.08;
- public static String PLN_SYMBOL = "PLN";
- public static String USD_SYMBOL = "USD";
- public static double BUY_RATE = 3.8804;
- public static double SELL_RATE = 3.9572;
}
@@ -39,24 +30,6 @@ public class EntityCreator {
.pln(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("USD");
- 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 class UserEntityBuilder {
String name;
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/service/NbpServiceTest.java b/src/test/java/eu/ztsh/wymiana/service/NbpServiceTest.java
deleted file mode 100644
index 7424fbe..0000000
--- a/src/test/java/eu/ztsh/wymiana/service/NbpServiceTest.java
+++ /dev/null
@@ -1,117 +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 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(EntityCreator.Constants.USD_SYMBOL)).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(EntityCreator.Constants.USD_SYMBOL)).isEqualTo(EntityCreator.Constants.SELL_RATE);
- // get from cache
- assertThat(nbpService.getBuyRate(EntityCreator.Constants.USD_SYMBOL)).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/usb/%s/".formatted(date);
- WireMockExtension.response(url, 404, "404 NotFound - Not Found - Brak danych");
- assertThatThrownBy(() -> nbpService.getSellRate("usb")).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/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 85161e6..0000000
--- a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package eu.ztsh.wymiana.validation;
-
-import eu.ztsh.wymiana.EntityCreator;
-import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest;
-import org.junit.jupiter.api.Disabled;
-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 java.util.stream.Stream;
-
-import static eu.ztsh.wymiana.EntityCreator.Constants.*;
-
-class ValidExchangeRequestValidatorTest extends ValidatorTest {
-
- protected ValidExchangeRequestValidatorTest() {
- super(new ValidExchangeRequestValidator());
- }
-
- @Test
- @DisplayName("Valid request with buy value specified")
- void validRequestWithBuyTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .from(PLN_SYMBOL)
- .to(USD_SYMBOL)
- .toBuy(USD_BUY)
- .build()).isTrue();
- }
-
- @Test
- @DisplayName("Valid request with sell value specified")
- void validRequestWithSellTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .from(PLN_SYMBOL)
- .to(USD_SYMBOL)
- .toSell(USD_SELL)
- .build()).isTrue();
- }
-
- @Disabled("Already validated (has field annotation)")
- @DisplayName("Invalid PESEL value")
- @ParameterizedTest
- @MethodSource
- void invalidPeselTest(String pesel) {
- assertThatValidation(EntityCreator.exchangeRequest()
- .pesel(pesel)
- .from(PLN_SYMBOL)
- .to(USD_SYMBOL)
- .toSell(USD_SELL)
- .build()).isFalse();
- }
-
- @Test
- @DisplayName("From and To have same value")
- void sameFromToTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .from(USD_SYMBOL)
- .to(USD_SYMBOL)
- .toSell(USD_SELL)
- .build()).isFalse();
- }
-
- @Test
- @DisplayName("Empty amounts")
- void emptyBuySellTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .from(PLN_SYMBOL)
- .to(USD_SYMBOL)
- .build()).isFalse();
- }
-
- @Disabled("Already validated (has field annotation)")
- @Test
- @DisplayName("Empty 'from' value")
- void emptyFromTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .to(USD_SYMBOL)
- .toSell(USD_SELL)
- .build()).isFalse();
- }
-
- @Disabled("Already validated (has field annotation)")
- @Test
- @DisplayName("Empty 'to' value")
- void emptyToTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .from(PLN_SYMBOL)
- .toSell(USD_SELL)
- .build()).isFalse();
- }
-
- @Test
- @DisplayName("Both Buy and Sell params filled in")
- void bothFilledBuySellTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .from(PLN_SYMBOL)
- .to(USD_SYMBOL)
- .toBuy(USD_BUY)
- .toSell(USD_SELL)
- .build()).isFalse();
- }
-
- @Test
- @DisplayName("Negative buy amount value")
- void negativeBuyAmountTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .from(PLN_SYMBOL)
- .to(USD_SYMBOL)
- .toBuy(-1.0)
- .build()).isFalse();
- }
-
- @Test
- @DisplayName("Negative sell amount value")
- void negativeSellAmountTest() {
- assertThatValidation(EntityCreator.exchangeRequest()
- .from(PLN_SYMBOL)
- .to(USD_SYMBOL)
- .toSell(-1.0)
- .build()).isFalse();
- }
-
- private static Stream invalidPeselTest() {
- return Stream.of("INVALID", PESEL.replace('6', '7'));
- }
-
-}
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));
- }
-
-}