diff --git a/.coverage_history b/.coverage_history index 1215f0c..7770227 100644 --- a/.coverage_history +++ b/.coverage_history @@ -1 +1 @@ -0.9937006618292002 +0.9937021683673469 diff --git a/src/main/java/dev/sorn/fmp4j/services/FmpService.java b/src/main/java/dev/sorn/fmp4j/services/FmpService.java index 8509766..6dcd271 100644 --- a/src/main/java/dev/sorn/fmp4j/services/FmpService.java +++ b/src/main/java/dev/sorn/fmp4j/services/FmpService.java @@ -76,11 +76,14 @@ protected Map headers() { public final R download() { final var required = requiredParams(); - for (final var req : required.keySet()) { - if (!params.containsKey(req)) { - throw new FmpServiceException("'%s' is a required query param for endpoint [%s]", req, url()); - } + final var missing = required.keySet().stream() + .filter(req -> !params.containsKey(req)) + .toList(); + + if (!missing.isEmpty()) { + throw new FmpServiceException("%s are required query params for endpoint [%s]", missing, url()); } + return filter(http.get(typeRef, url(), headers(), params)); } } diff --git a/src/test/java/dev/sorn/fmp4j/services/FmpFullQuoteServiceTest.java b/src/test/java/dev/sorn/fmp4j/services/FmpFullQuoteServiceTest.java index 1c84fe5..4fcf8e2 100644 --- a/src/test/java/dev/sorn/fmp4j/services/FmpFullQuoteServiceTest.java +++ b/src/test/java/dev/sorn/fmp4j/services/FmpFullQuoteServiceTest.java @@ -76,7 +76,7 @@ void missing_symbol_throws() { // then var e = assertThrows(FmpServiceException.class, () -> f.accept(service)); - assertEquals(format("'symbol' is a required query param for endpoint [%s]", service.url()), e.getMessage()); + assertEquals(format("[symbol] are required query params for endpoint [%s]", service.url()), e.getMessage()); } @Test diff --git a/src/test/java/dev/sorn/fmp4j/services/FmpPartialQuoteServiceTest.java b/src/test/java/dev/sorn/fmp4j/services/FmpPartialQuoteServiceTest.java index 43edb95..fcee105 100644 --- a/src/test/java/dev/sorn/fmp4j/services/FmpPartialQuoteServiceTest.java +++ b/src/test/java/dev/sorn/fmp4j/services/FmpPartialQuoteServiceTest.java @@ -76,7 +76,7 @@ void missing_symbol_throws() { // then var e = assertThrows(FmpServiceException.class, () -> f.accept(service)); - assertEquals(format("'symbol' is a required query param for endpoint [%s]", service.url()), e.getMessage()); + assertEquals(format("[symbol] are required query params for endpoint [%s]", service.url()), e.getMessage()); } @Test diff --git a/src/test/java/dev/sorn/fmp4j/services/FmpServiceTest.java b/src/test/java/dev/sorn/fmp4j/services/FmpServiceTest.java index 83b7fb6..e58f5da 100644 --- a/src/test/java/dev/sorn/fmp4j/services/FmpServiceTest.java +++ b/src/test/java/dev/sorn/fmp4j/services/FmpServiceTest.java @@ -12,6 +12,8 @@ import dev.sorn.fmp4j.cfg.FmpConfig; import dev.sorn.fmp4j.http.FmpHttpClient; import dev.sorn.fmp4j.types.FmpApiKey; +import dev.sorn.fmp4j.types.FmpLimit; +import dev.sorn.fmp4j.types.FmpPage; import dev.sorn.fmp4j.types.FmpSymbol; import java.time.LocalDate; import java.util.Arrays; @@ -20,7 +22,6 @@ import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -34,15 +35,19 @@ class FmpServiceTest { private ConcreteFmpService service; + private MultiRequiredFmpService multiRequiredService; + @BeforeEach void setup() { openMocks(this); when(mockConfig.fmpApiKey()).thenReturn(new FmpApiKey("abcdefghij1234567890abcdefghij12")); service = new ConcreteFmpService(mockConfig, mockHttpClient); + + multiRequiredService = new MultiRequiredFmpService(mockConfig, mockHttpClient); } @Test - void accepts_list_with_correct_type() { + void should_accept_list_with_correct_type() { // given List symbols = List.of(symbol("AAPL"), symbol("GOOGL"), symbol("MSFT")); @@ -84,7 +89,6 @@ void handles_empty_collection() { } @Test - @DisplayName("Should accept Optional with correct type") void accepts_optional_with_correct_type() { // given Optional optionalSymbol = Optional.of(symbol("AAPL")); @@ -114,7 +118,7 @@ void handles_empty_optional() { // given Optional emptyOptional = Optional.empty(); - // when then + // when // then assertDoesNotThrow(() -> service.param("symbol", emptyOptional)); assertEquals(emptyOptional, service.params.get("symbol")); } @@ -130,15 +134,37 @@ void handles_null_key() { } @Test - @DisplayName("Should reject nested Collection") void validates_nested_collections() { - // given: a nested list + // given List> nestedList = Arrays.asList(Arrays.asList("AAPL", "GOOGL")); // when // then assertThrows(FmpServiceException.class, () -> service.param("symbol", nestedList)); } + @Test + void should_show_all_missing_required_params() { + // given // when + FmpServiceException thrownEx = assertThrows(FmpServiceException.class, multiRequiredService::download); + var msg = thrownEx.getMessage(); + // then + assertTrue( + msg.contains("limit") || msg.contains("page"), + "Expected exception message to mention either 'limit' and 'page' as the required param, but got: " + + msg); + } + + @Test + void should_not_throw_missing_required_params() { + // given + multiRequiredService.param("page", FmpPage.page(10)); + multiRequiredService.param("limit", FmpLimit.limit(10)); + + // when // then + assertDoesNotThrow(multiRequiredService::download); + } + + // Concrete implementation for testing private static class ConcreteFmpService extends FmpService { public ConcreteFmpService(FmpConfig cfg, FmpHttpClient http) { super(cfg, http, new TypeReference() {}); @@ -159,4 +185,25 @@ protected String relativeUrl() { return "/test/endpoint"; } } + + private static class MultiRequiredFmpService extends FmpService { + public MultiRequiredFmpService(FmpConfig cfg, FmpHttpClient http) { + super(cfg, http, new TypeReference() {}); + } + + @Override + protected Map> requiredParams() { + return Map.of("limit", FmpLimit.class, "page", FmpPage.class); + } + + @Override + protected Map> optionalParams() { + return Map.of(); + } + + @Override + protected String relativeUrl() { + return "/test/endpoint"; + } + } }