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/pom.xml b/pom.xml index 3756469..5cbeab4 100644 --- a/pom.xml +++ b/pom.xml @@ -27,10 +27,8 @@ ${java.version} - 3.5.4 - 1.2.1 @@ -79,12 +77,6 @@ org.assertj assertj-core - - org.wiremock - wiremock-standalone - ${wiremock.version} - test - @@ -93,22 +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 - - - - - generate - - - - 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/RestClientConfiguration.java b/src/main/java/eu/ztsh/wymiana/config/RestClientConfiguration.java deleted file mode 100644 index 74ee47b..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() { - return RestClient.builder() - .baseUrl("http://api.nbp.pl") - .defaultHeader("Accept", "application/json") - .build(); - } - -} 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 8da9ad6..0000000 --- a/src/main/java/eu/ztsh/wymiana/exception/NoDataException.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.ztsh.wymiana.exception; - -public class NoDataException extends RuntimeException { - - public NoDataException(String code, String date) { - super("No data for code %s and date %s".formatted(code, date)); - } - -} 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 da26238..0000000 --- a/src/main/java/eu/ztsh/wymiana/service/NbpService.java +++ /dev/null @@ -1,89 +0,0 @@ -package eu.ztsh.wymiana.service; - -import eu.ztsh.wymiana.exception.NoDataException; -import eu.ztsh.wymiana.model.Rates; -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.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 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/ValidExchangeRequestValidator.java b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java index 0895d92..43d4f1f 100644 --- a/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java +++ b/src/main/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidator.java @@ -4,21 +4,12 @@ import eu.ztsh.wymiana.web.model.CurrencyExchangeRequest; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -public class ValidExchangeRequestValidator implements - ConstraintValidator { +public class ValidExchangeRequestValidator implements ConstraintValidator { @Override - public boolean isValid(CurrencyExchangeRequest request, + public boolean isValid(CurrencyExchangeRequest currencyExchangeRequest, 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)); + return false; } } 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/ValidExchangeRequestValidatorTest.java b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java index 85161e6..e990658 100644 --- a/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java +++ b/src/test/java/eu/ztsh/wymiana/validation/ValidExchangeRequestValidatorTest.java @@ -1,16 +1,8 @@ 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 { @@ -21,108 +13,42 @@ class ValidExchangeRequestValidatorTest extends ValidatorTest invalidPeselTest() { - return Stream.of("INVALID", PESEL.replace('6', '7')); } }