diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/das/DasTilerService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/das/DasTilerService.java index 07bca18d..d357d79c 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/das/DasTilerService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/das/DasTilerService.java @@ -2,8 +2,6 @@ import au.org.aodn.ogcapi.server.core.configuration.Config; import au.org.aodn.ogcapi.server.core.exception.DasUpstreamException; -import au.org.aodn.ogcapi.server.core.service.Search; -import au.org.aodn.stac.model.StacCollectionModel; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @@ -24,7 +22,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; /** @@ -45,7 +42,6 @@ public class DasTilerService { private static final String DATA_TILES_BASE = "/api/v1/das/tiler/data_tiles"; protected final DasProperties dasProperties; - protected final Search search; /** * The shared DAS client — short timeouts, and it attaches the DAS API key to every request @@ -55,11 +51,10 @@ public class DasTilerService { private final ObjectMapper mapper; public DasTilerService( - DasProperties dasProperties, Search search, + DasProperties dasProperties, @Qualifier(Config.DAS_REST_TEMPLATE) RestTemplate httpClient, ObjectMapper mapper) { this.dasProperties = dasProperties; - this.search = search; this.httpClient = httpClient; this.mapper = mapper; } @@ -217,19 +212,6 @@ public List productsForCollection(String collectionId) { return result; } - public boolean isDatasetInCollection(String collectionId, String dataset) { - var result = search.searchCollections(collectionId); - if (result == null || result.getCollections() == null) { - return false; - } - return result.getCollections().stream() - .map(StacCollectionModel::getAssets) - .filter(Objects::nonNull) - .flatMap(assets -> assets.keySet().stream()) - .map(key -> key.contains(".") ? key.substring(0, key.indexOf('.')) : key) - .anyMatch(dataset::equals); - } - private DasTileResult exchangeForImage(UriComponentsBuilder builder, Map params, String fallbackContentType) { String url = builder.encode().toUriString(); try { diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java index e8a45e73..8ca1edc3 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java @@ -105,7 +105,8 @@ public ResponseEntity collectionMapGetTile(String tileMatrix, Integer ti content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "404", description = "`tileMatrixSetId` is not `WebMercatorQuad`, " + - "`dataset` is not in the collection, or there is no data for that date.", + "or DAS reported an unknown product (`{dataset}:{variable}`) or an unavailable date. " + + "The DAS cases are forwarded from DAS, which owns the product catalogue.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "422", description = "The product cannot be rendered as a visual " + @@ -211,10 +212,6 @@ public ResponseEntity getCollectionVisualMapTile( if (!"png".equals(f) && !"webp".equals(f)) { throw new InvalidParameterException("f must be 'png' or 'webp'"); } - if (!dasTilerService.isDatasetInCollection(collectionId, dataset)) { - throw new ResourceNotFoundException( - "dataset '" + dataset + "' not found in collection '" + collectionId + "'"); - } // DAS identifies a renderable product by the combined {dataset}:{variable} id. String product = dataset + ":" + variable; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java index 7d0e8c83..807abe53 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java @@ -1,7 +1,6 @@ package au.org.aodn.ogcapi.server.tile; import au.org.aodn.ogcapi.server.core.exception.InvalidParameterException; -import au.org.aodn.ogcapi.server.core.exception.ResourceNotFoundException; import au.org.aodn.ogcapi.server.core.model.ErrorResponse; import au.org.aodn.ogcapi.server.core.service.das.DasTilerService; import com.fasterxml.jackson.databind.JsonNode; @@ -43,8 +42,12 @@ public class RestExtApi { description = "The discovery call a map client makes before requesting tiles: it supplies every " + "value the tile routes need, including ready-to-use URL templates.\n\n" + "`tile_types` is a capability list — what this service can serve today, not a property of " + - "the data. Single-variable products give `[\"visual\", \"data\"]`; two-variable products " + - "(e.g. `ucur+vcur`) give `[\"data\"]` (they cannot be colourised as visual tiles). Each " + + "the data. It reflects the capability DAS reports per product: a product DAS marks as " + + "visual-capable gives `[\"visual\", \"data\"]`, one it does not gives `[\"data\"]`. " + + "Two-variable products (e.g. `ucur+vcur`) are always `[\"data\"]` — they cannot be " + + "colourised — and a single-variable product may be `[\"data\"]` too when its variable is " + + "not renderable. Variable arity is used only as a fallback against an older DAS that does " + + "not report capability. Each " + "capability carries its own template(s): `visual_tile_url_template` + `legend_url` when " + "`\"visual\"` is present, `data_tile_url_template` + `data_manifest_url_template` when " + "`\"data\"` is present. For two-variable products the `variable` array order (e.g. " + @@ -77,6 +80,15 @@ public class RestExtApi { "full_date_range": {"start": "2020-01-01", "end": "2024-01-02"}, "data_tile_url_template": "/api/v1/ogc/ext/tiles/collections/0c9eb39c-9cbe-4c6a-8a10-5867087e703a/data_tiles/{lod}/{x}/{y}?dataset=model_sea_level_anomaly_gridded_realtime&variable=ucur%2Bvcur&datetime={datetime}", "data_manifest_url_template": "/api/v1/ogc/ext/tiles/collections/0c9eb39c-9cbe-4c6a-8a10-5867087e703a/data_tiles/manifest?dataset=model_sea_level_anomaly_gridded_realtime&variable=ucur%2Bvcur&datetime={datetime}" + }, + { + "id": "model_sea_level_anomaly_gridded_realtime:wdir", + "variable": "WDIR", + "tile_types": ["data"], + "available_dates": ["2024-01-01", "2024-01-02"], + "full_date_range": {"start": "2020-01-01", "end": "2024-01-02"}, + "data_tile_url_template": "/api/v1/ogc/ext/tiles/collections/0c9eb39c-9cbe-4c6a-8a10-5867087e703a/data_tiles/{lod}/{x}/{y}?dataset=model_sea_level_anomaly_gridded_realtime&variable=wdir&datetime={datetime}", + "data_manifest_url_template": "/api/v1/ogc/ext/tiles/collections/0c9eb39c-9cbe-4c6a-8a10-5867087e703a/data_tiles/manifest?dataset=model_sea_level_anomaly_gridded_realtime&variable=wdir&datetime={datetime}" } ] }"""))), @@ -113,10 +125,15 @@ public ResponseEntity getCollectionProducts( entry.set("variable", variable); // tile_types is a capability list: what THIS service can serve today, not a property of - // the data. Visual tiles are colourised scalars only; DAS rejects multi-variable products - // there. Data tiles support one or exactly two variables (the shader packs one or two - // channels); three or more is unsupported by the protocol, so nothing is servable. - boolean canVisual = variableCount == 1; + // the data. Visual capability now comes from DAS, which knows whether a variable is + // actually renderable — arity cannot tell a colourisable scalar from one the renderer + // has no sensible colouring for. The arity rule survives only as a fallback for a DAS + // old enough to have no `visual` field, which the OGC-first deployment order requires. + // Data tiles still follow arity: the shader packs one or two channels, and DAS config + // validation guarantees nothing longer reaches here. + boolean canVisual = product.has("visual") + ? product.path("visual").asBoolean() + : variableCount == 1; boolean canData = variableCount == 1 || variableCount == 2; ArrayNode tileTypes = mapper.createArrayNode(); if (canVisual) { @@ -133,8 +150,8 @@ public ResponseEntity getCollectionProducts( entry.set("full_date_range", availability != null && !availability.isMissingNode() ? availability.path("full_date_range") : mapper.createObjectNode()); - // The tile routes take dataset and variable separately (they check dataset membership - // against the collection's assets), so split the product id on its first ':'. The + // The tile routes take dataset and variable separately, so split the product id on its + // first ':'. That split is the only place the id is treated as anything but opaque. The // variable half of a two-variable product contains '+' (e.g. ucur+vcur), which URLEncoder // renders as %2B — without that a query string would decode it back to a space. int sep = id.indexOf(':'); @@ -204,8 +221,9 @@ public ResponseEntity getCollectionProducts( "negative `x`/`y`.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "404", description = "`dataset` is not in the collection, or DAS has no " + - "such product, LOD or chunk, or no data for that date.", + @ApiResponse(responseCode = "404", description = "DAS reported an unknown product " + + "(`{dataset}:{variable}`), an unavailable date, or an out-of-range LOD or chunk. " + + "Forwarded from DAS, which owns the product catalogue.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "422", description = "DAS could not process the request (e.g. a " + @@ -278,10 +296,6 @@ public ResponseEntity getCollectionDataTile( throw new InvalidParameterException("x and y (chunk column/row) must be >= 0"); } validateProductParams(dataset, variable, datetime); - if (!dasTilerService.isDatasetInCollection(collectionId, dataset)) { - throw new ResourceNotFoundException( - "dataset '" + dataset + "' not found in collection '" + collectionId + "'"); - } // DAS identifies a product by the combined {dataset}:{variable} id. String product = dataset + ":" + variable; @@ -314,8 +328,9 @@ public ResponseEntity getCollectionDataTile( "containing a space (an unencoded `+`), or `datetime` not `YYYY-MM-DD`.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "404", description = "`dataset` is not in the collection, or DAS has " + - "no such product, or no data for that date.", + @ApiResponse(responseCode = "404", description = "DAS reported an unknown product " + + "(`{dataset}:{variable}`) or an unavailable date. Forwarded from DAS, which owns " + + "the product catalogue.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "422", description = "DAS could not process the request (e.g. a " + @@ -361,10 +376,6 @@ public ResponseEntity getCollectionDataManifest( @RequestParam(required = false) String datetime) { validateProductParams(dataset, variable, datetime); - if (!dasTilerService.isDatasetInCollection(collectionId, dataset)) { - throw new ResourceNotFoundException( - "dataset '" + dataset + "' not found in collection '" + collectionId + "'"); - } String product = dataset + ":" + variable; DasTilerService.DasJsonResult manifest = dasTilerService.getDataManifest(product, datetime); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/das/DasTilerServiceTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/das/DasTilerServiceTest.java index ea708db8..f9ef856c 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/das/DasTilerServiceTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/das/DasTilerServiceTest.java @@ -1,10 +1,6 @@ package au.org.aodn.ogcapi.server.core.service.das; import au.org.aodn.ogcapi.server.core.exception.DasUpstreamException; -import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; -import au.org.aodn.ogcapi.server.core.service.Search; -import au.org.aodn.stac.model.AssetModel; -import au.org.aodn.stac.model.StacCollectionModel; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -45,20 +41,18 @@ public class DasTilerServiceTest { private static final String PRODUCT_ID = "model_sea_level_anomaly_gridded_realtime:gsla"; private RestTemplate httpClient; - private Search search; private DasTilerService service; @BeforeEach public void setUp() { httpClient = mock(RestTemplate.class); - search = mock(Search.class); DasProperties config = new DasProperties( HOST, null,"test-secret", "internal-secret", Duration.ofSeconds(5), Duration.ofSeconds(30) ); - service = new DasTilerService(config, search, httpClient, new ObjectMapper()); + service = new DasTilerService(config, httpClient, new ObjectMapper()); } private HttpHeaders imageHeaders() { @@ -374,45 +368,6 @@ public void testProductsForCollectionFiltersByMetadataUuid() { assertTrue(service.productsForCollection("unknown-uuid").isEmpty()); } - @Test - public void testIsDatasetInCollectionChecksAssetKeys() { - // es-indexer keys assets by the cloud-optimised file name, which carries a format extension, - // while DAS product ids use the bare stem — so everything from the first dot on is dropped - // before matching. Membership is a stem lookup against the cached searchCollections result. - // The extension is not enumerated, so an unknown/future format works the same way. - StacCollectionModel model = StacCollectionModel.builder() - .uuid("uuid-a") - .assets(Map.of( - "satellite_austemp_heatwave_8day.zarr", - AssetModel.builder().role(AssetModel.Role.SUMMARY).build(), - "mooring_temperature_logger_delayed.parquet", - AssetModel.builder().role(AssetModel.Role.SUMMARY).build(), - "some_future_format_dataset.nc4", - AssetModel.builder().role(AssetModel.Role.SUMMARY).build())) - .build(); - ElasticSearchBase.SearchResult found = new ElasticSearchBase.SearchResult<>(); - found.setCollections(List.of(model)); - when(search.searchCollections("uuid-a")).thenReturn(found); - - assertTrue(service.isDatasetInCollection("uuid-a", "satellite_austemp_heatwave_8day"), - "the .zarr extension on the asset key must be ignored when matching the dataset"); - assertTrue(service.isDatasetInCollection("uuid-a", "mooring_temperature_logger_delayed"), - "the .parquet extension on the asset key must be ignored when matching the dataset"); - assertTrue(service.isDatasetInCollection("uuid-a", "some_future_format_dataset"), - "the extension is not hard-coded, so an unknown format is stripped the same way"); - assertFalse(service.isDatasetInCollection("uuid-a", "some_other_dataset"), - "a dataset that is not an asset key is not in the collection"); - } - - @Test - public void testIsDatasetInCollectionFalseWhenCollectionMissing() { - ElasticSearchBase.SearchResult empty = new ElasticSearchBase.SearchResult<>(); - empty.setCollections(List.of()); - when(search.searchCollections("uuid-missing")).thenReturn(empty); - - assertFalse(service.isDatasetInCollection("uuid-missing", "model_sea_level_anomaly_gridded_realtime")); - } - private record CapturedRequest(String url, Map params) { } } diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestApiTest.java index dc658899..7704ca88 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestApiTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestApiTest.java @@ -17,6 +17,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -209,7 +210,6 @@ public void verifyVisualMapTileRowColOutOfRangeForZoomReturns400() { @Test public void verifyVisualMapTileMaxZoomBoundaryIsAccepted() { - when(dasTilerService.isDatasetInCollection("some-uuid", "model_sla")).thenReturn(true); when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(24), eq(0), eq(0), eq("png"), isNull(), isNull())) .thenReturn(new DasTilerService.DasTileResult("tile-bytes".getBytes(), "image/png", null)); @@ -221,18 +221,24 @@ public void verifyVisualMapTileMaxZoomBoundaryIsAccepted() { } @Test - public void verifyVisualMapTileDatasetNotInCollectionReturns404() { - // isDatasetInCollection defaults to false on the mock (unstubbed boolean). + public void verifyVisualMapTileUnknownProductIsForwardedToDas() { + // DAS owns the product catalogue, so an unknown dataset is its answer to give. + // Previously this 404'd locally from an Elasticsearch membership check that could + // disagree with what DAS actually publishes. + when(dasTilerService.getVisualTile(eq("wrong:gsla"), eq("2024-01-01"), eq(2), eq(1), eq(1), eq("png"), isNull(), isNull())) + .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "Unknown product: wrong:gsla")); + ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?dataset=model_sla&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?dataset=wrong&variable=gsla&datetime=2024-01-01", String.class ); + Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + verify(dasTilerService).getVisualTile(eq("wrong:gsla"), eq("2024-01-01"), eq(2), eq(1), eq(1), eq("png"), isNull(), isNull()); } @Test public void verifyVisualMapTileForwardsZXYAndReturnsImage() { - when(dasTilerService.isDatasetInCollection("some-uuid", "model_sla")).thenReturn(true); when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(1), eq(3), eq("png"), isNull(), isNull())) .thenReturn(new DasTilerService.DasTileResult("tile-bytes".getBytes(), "image/png", "public, max-age=31536000, immutable")); @@ -249,7 +255,6 @@ public void verifyVisualMapTileForwardsZXYAndReturnsImage() { @Test public void verifyVisualMapTileRebuildsProductAndMapsWebpExt() { // dataset + variable are recombined into the DAS product id `model_sla:gsla`. - when(dasTilerService.isDatasetInCollection("some-uuid", "model_sla")).thenReturn(true); when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(1), eq(3), eq("webp"), isNull(), isNull())) .thenReturn(new DasTilerService.DasTileResult("webp-bytes".getBytes(), "image/webp", null)); @@ -264,7 +269,6 @@ public void verifyVisualMapTileRebuildsProductAndMapsWebpExt() { @Test public void verifyVisualMapTileUpstreamErrorMirrored() { - when(dasTilerService.isDatasetInCollection("some-uuid", "model_sla")).thenReturn(true); when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(1), eq(3), eq("png"), isNull(), isNull())) .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "no such date")); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestExtApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestExtApiTest.java index 9b1adde2..de1b1aa1 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestExtApiTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestExtApiTest.java @@ -21,6 +21,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import java.util.ArrayList; import java.util.List; import static org.mockito.ArgumentMatchers.anyInt; @@ -65,6 +66,13 @@ private JsonNode multiVariableProduct(String id, String metadataUuid, List response = testRestTemplate.getForEntity( + getExternalBasePath() + "/tiles/collections/" + collectionId + "/products", JsonNode.class + ); + Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); + return response.getBody().get("products"); + } + + private List tileTypesOf(JsonNode entry) { + List types = new ArrayList<>(); + entry.get("tile_types").forEach(node -> types.add(node.asText())); + return types; + } + + // --- Data-tile route: value-encoded PNG passthrough, floor-only validation, forwarded DAS errors --- @Test public void verifyDataTileReturnsImageWithCacheControl() { - when(dasTilerService.isDatasetInCollection("uuid-a", "model_sla")).thenReturn(true); when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01", 1, 0, 0)).thenReturn( new DasTilerService.DasTileResult( "data-bytes".getBytes(), "image/png", "public, max-age=31536000, immutable")); @@ -242,7 +352,6 @@ public void verifyDataTileRejectsMissingOrMalformedParams() { @Test public void verifyDataTileRejectsUnencodedPlusInVariable() { - when(dasTilerService.isDatasetInCollection("uuid-a", "model_sla")).thenReturn(true); // A raw '+' decodes to a space, so the product id would be 'model_sla:ucur vcur' — caught // here rather than forwarded to DAS as an unresolvable id. @@ -257,21 +366,24 @@ public void verifyDataTileRejectsUnencodedPlusInVariable() { } @Test - public void verifyDataTileNotFoundWhenDatasetNotInCollection() { - when(dasTilerService.isDatasetInCollection("uuid-a", "wrong")).thenReturn(false); + public void verifyDataTileUnknownProductIsForwardedToDas() { + // DAS owns the product catalogue, so an unknown dataset is its answer to give. + // Previously this 404'd locally from an Elasticsearch membership check that could + // disagree with what DAS actually publishes. + when(dasTilerService.getDataTile("wrong:gsla", "2024-01-01", 1, 0, 0)) + .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "Unknown product: wrong:gsla")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/0/0" + "?dataset=wrong&variable=gsla&datetime=2024-01-01", ErrorResponse.class); Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); - // Membership fails locally, so DAS is never called for the tile. - verify(dasTilerService, never()).getDataTile(anyString(), anyString(), anyInt(), anyInt(), anyInt()); + Assertions.assertEquals("Unknown product: wrong:gsla", response.getBody().getMessage()); + verify(dasTilerService).getDataTile("wrong:gsla", "2024-01-01", 1, 0, 0); } @Test public void verifyDataTileMirrorsUpstreamNotFound() { - when(dasTilerService.isDatasetInCollection("uuid-a", "model_sla")).thenReturn(true); when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01", 9, 0, 0)) .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "LOD 9 not in grid")); @@ -285,7 +397,6 @@ public void verifyDataTileMirrorsUpstreamNotFound() { @Test public void verifyDataTileMirrorsUpstreamServiceUnavailable() { - when(dasTilerService.isDatasetInCollection("uuid-a", "model_sla")).thenReturn(true); when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01", 1, 0, 0)) .thenThrow(new DasUpstreamException(HttpStatus.SERVICE_UNAVAILABLE, "Service Unavailable")); @@ -315,7 +426,6 @@ public void verifyDataTileMirrorsUpstreamServiceUnavailable() { public void verifyDataManifestReturnsJsonWithCacheControl() { ObjectNode manifestBody = mapper.createObjectNode(); manifestBody.putArray("bounds").add(0).add(0).add(1).add(1); - when(dasTilerService.isDatasetInCollection("uuid-a", "model_sla")).thenReturn(true); when(dasTilerService.getDataManifest("model_sla:gsla", "2024-01-01")) .thenReturn(new DasTilerService.DasJsonResult(manifestBody, "public, max-age=31536000, immutable")); @@ -343,15 +453,17 @@ public void verifyDataManifestRejectsMissingOrMalformedParams() { } @Test - public void verifyDataManifestNotFoundWhenDatasetNotInCollection() { - when(dasTilerService.isDatasetInCollection("uuid-a", "wrong")).thenReturn(false); + public void verifyDataManifestUnknownProductIsForwardedToDas() { + when(dasTilerService.getDataManifest("wrong:gsla", "2024-01-01")) + .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "Unknown product: wrong:gsla")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/manifest" + "?dataset=wrong&variable=gsla&datetime=2024-01-01", ErrorResponse.class); Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); - verify(dasTilerService, never()).getDataManifest(anyString(), anyString()); + Assertions.assertEquals("Unknown product: wrong:gsla", response.getBody().getMessage()); + verify(dasTilerService).getDataManifest("wrong:gsla", "2024-01-01"); } @Test