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 13fabb63..e525bc54 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 @@ -82,7 +82,8 @@ public record DasJsonResult(JsonNode body, String cacheControl) { public DasTileResult getVisualTile(String productId, String date, int zoom, int tileX, int tileY, String ext, String colormap, String rescale) { UriComponentsBuilder builder = UriComponentsBuilder - .fromUriString(dasProperties.host() + VISUAL_TILES_BASE + "/{product}/{date}/{z}/{x}/{y}.{ext}"); + .fromUriString(dasProperties.host() + VISUAL_TILES_BASE + "/{product}/{z}/{x}/{y}.{ext}") + .queryParam("date", "{date}"); Map params = new HashMap<>(); params.put("product", productId); params.put("date", date); @@ -111,7 +112,8 @@ public DasTileResult getVisualTile(String productId, String date, int zoom, int */ public DasTileResult getDataTile(String productId, String date, int lod, int x, int y) { UriComponentsBuilder builder = UriComponentsBuilder - .fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/{date}/{z}/{x}/{y}.png"); + .fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/{z}/{x}/{y}.png") + .queryParam("date", "{date}"); Map params = new HashMap<>(); params.put("product", productId); params.put("date", date); @@ -128,7 +130,8 @@ public DasTileResult getDataTile(String productId, String date, int lod, int x, */ public DasJsonResult getPoint(String productId, String date, double lat, double lon) { UriComponentsBuilder builder = UriComponentsBuilder - .fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/{date}/point") + .fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/point") + .queryParam("date", "{date}") .queryParam("lat", "{lat}") .queryParam("lon", "{lon}"); Map params = new HashMap<>(); @@ -148,7 +151,8 @@ public DasJsonResult getPoint(String productId, String date, double lat, double */ public DasJsonResult getDataManifest(String productId, String date) { UriComponentsBuilder builder = UriComponentsBuilder - .fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/{date}/manifest.json"); + .fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/manifest.json") + .queryParam("date", "{date}"); Map params = new HashMap<>(); params.put("product", productId); params.put("date", date); @@ -176,10 +180,17 @@ public List getProducts() { * Fetches the product/date manifest, forwarding its {@code Cache-Control} rather than this * service inventing a freshness of its own. */ - public DasJsonResult getManifest() { + public DasJsonResult getManifest(String collectionId) { UriComponentsBuilder builder = UriComponentsBuilder .fromUriString(dasProperties.host() + VISUAL_TILES_BASE + "/manifest"); - return exchangeForJson(builder, Map.of()); + Map params = new HashMap<>(); + + if (collectionId != null) { + builder.queryParam("metadata_uuid", "{metadata_uuid}"); + params.put("metadata_uuid", collectionId); + } + + return exchangeForJson(builder, params); } public JsonNode getColormaps() { diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java index ffec5bfc..f0f5924b 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java @@ -148,11 +148,12 @@ public static String validateAndFormatDate(String dateInput, boolean isStartDate } /** - * Parse an ISO-8601 date-time and verify it is in UTC (offset 'Z'). + * Parse an ISO-8601 date-time and verify it is at zero offset from UTC — accepts both the 'Z' + * designator and an explicit "+00:00"/"-00:00" offset, since they are equivalent instants. * - * @param dateTime ISO-8601 date-time string, e.g. "2026-06-16T00:00:00Z" + * @param dateTime ISO-8601 date-time string, e.g. "2026-06-16T00:00:00Z" or "2026-06-16T00:00:00+00:00" * @return the parsed date-time - * @throws IllegalArgumentException if the value is not a valid ISO-8601 date-time or is not in UTC + * @throws IllegalArgumentException if the value is not a valid ISO-8601 date-time or is not at zero UTC offset */ public static java.time.OffsetDateTime parseUtcDateTime(String dateTime) { java.time.OffsetDateTime parsed; @@ -163,7 +164,7 @@ public static java.time.OffsetDateTime parseUtcDateTime(String dateTime) { } if (!parsed.getOffset().equals(java.time.ZoneOffset.UTC)) { - throw new IllegalArgumentException("Date-time must be in UTC (offset 'Z'): " + dateTime); + throw new IllegalArgumentException("Date-time must be at zero UTC offset (e.g. 'Z' or '+00:00'): " + dateTime); } return parsed; } 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 a2a26e44..5931fbaa 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 @@ -7,6 +7,7 @@ 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 au.org.aodn.ogcapi.server.core.util.DatetimeUtils; import au.org.aodn.ogcapi.tile.api.*; import au.org.aodn.ogcapi.tile.model.*; import io.swagger.v3.oas.annotations.Hidden; @@ -100,7 +101,7 @@ public ResponseEntity collectionMapGetTile(String tileMatrix, Integer ti @Content(mediaType = "image/png", schema = @Schema(type = "string", format = "binary")), @Content(mediaType = "image/webp", schema = @Schema(type = "string", format = "binary"))}), @ApiResponse(responseCode = "400", description = "`dataset`, `variable` or `datetime` missing, " + - "`datetime` not `YYYY-MM-DD`, `f` neither `png` nor `webp`, or z/x/y out of range.", + "`datetime` not a full UTC ISO-8601 timestamp, `f` neither `png` nor `webp`, or z/x/y out of range.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "404", description = "`tileMatrixSetId` is not `WebMercatorQuad`, " + @@ -165,9 +166,9 @@ public ResponseEntity getCollectionVisualMapTile( @RequestParam(required = false) String variable, @Parameter(in = ParameterIn.QUERY, required = true, - description = "Date to render, strict `YYYY-MM-DD` — not an RFC 3339 date-time. Must be " + + description = "Date to render, a full UTC ISO-8601 timestamp (offset `Z`). Must be " + "one of the product's `available_dates`.", - example = "2024-01-01") + example = "2024-01-01T00:00:00Z") @RequestParam(required = false) String datetime, @Parameter(in = ParameterIn.QUERY, @@ -205,8 +206,13 @@ public ResponseEntity getCollectionVisualMapTile( if (variable == null || variable.isBlank()) { throw new InvalidParameterException("variable is required"); } - if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2}$")) { - throw new InvalidParameterException("datetime is required and must be YYYY-MM-DD"); + if (datetime == null) { + throw new InvalidParameterException("datetime is required"); + } + try { + DatetimeUtils.parseUtcDateTime(datetime); + } catch (IllegalArgumentException e) { + throw new InvalidParameterException(e.getMessage()); } if (!"png".equals(f) && !"webp".equals(f)) { throw new InvalidParameterException("f must be 'png' or 'webp'"); 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 df832981..38efe2c3 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 @@ -3,6 +3,7 @@ import au.org.aodn.ogcapi.server.core.exception.InvalidParameterException; import au.org.aodn.ogcapi.server.core.model.ErrorResponse; import au.org.aodn.ogcapi.server.core.service.das.DasTilerService; +import au.org.aodn.ogcapi.server.core.util.DatetimeUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -111,7 +112,7 @@ public ResponseEntity getCollectionProducts( example = "0c9eb39c-9cbe-4c6a-8a10-5867087e703a") @PathVariable String collectionId) { List products = dasTilerService.productsForCollection(collectionId); - DasTilerService.DasJsonResult manifest = dasTilerService.getManifest(); + DasTilerService.DasJsonResult manifest = dasTilerService.getManifest(collectionId); JsonNode manifestProducts = manifest.body() != null ? manifest.body().path("products") : null; ArrayNode result = mapper.createArrayNode(); @@ -220,7 +221,7 @@ public ResponseEntity getCollectionProducts( content = @Content(mediaType = "image/png", schema = @Schema(type = "string", format = "binary"))), @ApiResponse(responseCode = "400", description = "`dataset` or `variable` missing, `variable` " + - "containing a space (an unencoded `+`), `datetime` not `YYYY-MM-DD`, `lod` below 1, or " + + "containing a space (an unencoded `+`), `datetime` not a full UTC ISO-8601 timestamp, `lod` below 1, or " + "negative `x`/`y`.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @@ -283,9 +284,9 @@ public ResponseEntity getCollectionDataTile( @RequestParam(required = false) String variable, @Parameter(in = ParameterIn.QUERY, required = true, - description = "Date to decode, strict `YYYY-MM-DD` — not an RFC 3339 date-time. Must be " + + description = "Date to decode, a full UTC ISO-8601 timestamp (offset `Z`). Must be " + "one of the product's `available_dates`.", - example = "2024-01-01") + example = "2024-01-01T00:00:00Z") @RequestParam(required = false) String datetime) { // Unlike the visual route's z (bounded 0..24 by WebMercatorQuad), the LOD grid is computed @@ -336,7 +337,7 @@ public ResponseEntity getCollectionDataTile( } }"""))), @ApiResponse(responseCode = "400", description = "`dataset` or `variable` missing, `variable` " + - "containing a space (an unencoded `+`), `datetime` not `YYYY-MM-DD`, or `lat`/`lon` " + + "containing a space (an unencoded `+`), `datetime` not a full UTC ISO-8601 timestamp, or `lat`/`lon` " + "missing or out of range.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @@ -382,9 +383,9 @@ public ResponseEntity getCollectionDataPoint( @RequestParam(required = false) String variable, @Parameter(in = ParameterIn.QUERY, required = true, - description = "Date to decode, strict `YYYY-MM-DD`. Must be one of the product's " + - "`available_dates`.", - example = "2024-01-01") + description = "Date to decode, a full UTC ISO-8601 timestamp (offset `Z`). Must be one of " + + "the product's `available_dates`.", + example = "2024-01-01T00:00:00Z") @RequestParam(required = false) String datetime, @Parameter(in = ParameterIn.QUERY, required = true, @@ -426,7 +427,7 @@ public ResponseEntity getCollectionDataPoint( @ApiResponse(responseCode = "200", description = "The data-tile decode manifest.", content = @Content(mediaType = "application/json")), @ApiResponse(responseCode = "400", description = "`dataset` or `variable` missing, `variable` " + - "containing a space (an unencoded `+`), or `datetime` not `YYYY-MM-DD`.", + "containing a space (an unencoded `+`), or `datetime` not a full UTC ISO-8601 timestamp.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "404", description = "DAS reported an unknown product " + @@ -471,9 +472,9 @@ public ResponseEntity getCollectionDataManifest( @RequestParam(required = false) String variable, @Parameter(in = ParameterIn.QUERY, required = true, - description = "Date to decode, strict `YYYY-MM-DD`. Must be one of the product's " + - "`available_dates`.", - example = "2024-01-01") + description = "Date to decode, a full UTC ISO-8601 timestamp (offset `Z`). Must be one of " + + "the product's `available_dates`.", + example = "2024-01-01T00:00:00Z") @RequestParam(required = false) String datetime) { validateProductParams(dataset, variable, datetime); @@ -502,8 +503,13 @@ private void validateProductParams(String dataset, String variable, String datet "variable must not contain spaces; percent-encode the '+' of a two-variable " + "product as %2B (e.g. variable=ucur%2Bvcur)"); } - if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2}$")) { - throw new InvalidParameterException("datetime is required and must be YYYY-MM-DD"); + if (datetime == null) { + throw new InvalidParameterException("datetime is required"); + } + try { + DatetimeUtils.parseUtcDateTime(datetime); + } catch (IllegalArgumentException e) { + throw new InvalidParameterException(e.getMessage()); } } 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 75ec366e..44980514 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 @@ -86,8 +86,10 @@ public void testGetVisualTileSendsProductAsPathVariable() { service.getVisualTile(PRODUCT_ID, "2024-01-01", 2, 1, 1, "png", null, null); CapturedRequest captured = captureImageRequest(); - assertTrue(captured.url.contains("/{product}/{date}/{z}/{x}/{y}.{ext}"), "product must be a path variable, got: " + captured.url); + assertTrue(captured.url.contains("/{product}/{z}/{x}/{y}.{ext}"), "product must be a path variable, got: " + captured.url); + assertTrue(captured.url.contains("date={date}"), "date must be a query param, got: " + captured.url); assertEquals(PRODUCT_ID, captured.params.get("product"), "product id with ':' must be passed raw as a path variable"); + assertEquals("2024-01-01", captured.params.get("date")); } @Test @@ -128,20 +130,21 @@ public void testGetVisualTileForwardsContentTypeAndCacheControl() { assertEquals("public, max-age=31536000, immutable", result.cacheControl()); } - // --- Data tiles: value-encoded PNGs, product-local LOD, no query params --- + // --- Data tiles: value-encoded PNGs, product-local LOD, date as the only query param --- @Test - public void testGetDataTileSendsProductAndPngPathWithoutQuery() { + public void testGetDataTileSendsProductAsPathVariableAndDateAsQuery() { when(httpClient.getForEntity(anyString(), eq(byte[].class), anyMap())) .thenReturn(new ResponseEntity<>("data-bytes".getBytes(), imageHeaders(), HttpStatus.OK)); service.getDataTile(PRODUCT_ID, "2024-01-01", 1, 0, 0); CapturedRequest captured = captureImageRequest(); - assertTrue(captured.url.contains("/data_tiles/{product}/{date}/{z}/{x}/{y}.png"), - "data tile must expand product/date/lod/x/y as path variables and end in .png, got: " + captured.url); - assertFalse(captured.url.contains("?"), "data tiles take no query params, got: " + captured.url); + assertTrue(captured.url.contains("/data_tiles/{product}/{z}/{x}/{y}.png"), + "data tile must expand product/lod/x/y as path variables and end in .png, got: " + captured.url); + assertTrue(captured.url.contains("date={date}"), "date must be a query param, got: " + captured.url); assertEquals(PRODUCT_ID, captured.params.get("product"), "product id with ':' must be a raw path variable"); + assertEquals("2024-01-01", captured.params.get("date")); assertEquals(1, captured.params.get("z")); assertEquals(0, captured.params.get("x")); assertEquals(0, captured.params.get("y")); @@ -191,7 +194,7 @@ public void testGetDataTileNotFoundMirrored() { // --- Point: decoded value(s) at a lat/lon, JSON body with query params --- @Test - public void testGetPointSendsProductAndDateAsPathVariablesWithLatLonQuery() { + public void testGetPointSendsProductAsPathVariableWithDateLatLonQuery() { ObjectNode pointBody = new ObjectMapper().createObjectNode(); pointBody.put("lat", -44.27813720703125); HttpHeaders headers = new HttpHeaders(); @@ -203,10 +206,10 @@ public void testGetPointSendsProductAndDateAsPathVariablesWithLatLonQuery() { service.getPoint(PRODUCT_ID, "2024-01-01", -44.27, 132.00); CapturedRequest captured = captureJsonRequest(); - assertTrue(captured.url.contains("/data_tiles/{product}/{date}/point"), - "point must expand product/date as path variables, got: " + captured.url); - assertTrue(captured.url.contains("lat={lat}") && captured.url.contains("lon={lon}"), - "lat/lon must be query params, got: " + captured.url); + assertTrue(captured.url.contains("/data_tiles/{product}/point"), + "point must expand product as a path variable, got: " + captured.url); + assertTrue(captured.url.contains("date={date}") && captured.url.contains("lat={lat}") && captured.url.contains("lon={lon}"), + "date/lat/lon must be query params, got: " + captured.url); assertEquals(PRODUCT_ID, captured.params.get("product"), "product id with ':' must be a raw path variable"); assertEquals("2024-01-01", captured.params.get("date")); assertEquals(-44.27, captured.params.get("lat")); @@ -259,8 +262,9 @@ public void testGetDataManifestBuildsUrlAndForwardsCacheControl() { DasTilerService.DasJsonResult result = service.getDataManifest(PRODUCT_ID, "2024-01-01"); CapturedRequest captured = captureJsonRequest(); - assertTrue(captured.url.contains("/data_tiles/{product}/{date}/manifest.json"), - "manifest must expand product/date as path variables and end in manifest.json, got: " + captured.url); + assertTrue(captured.url.contains("/data_tiles/{product}/manifest.json"), + "manifest must expand product as a path variable and end in manifest.json, got: " + captured.url); + assertTrue(captured.url.contains("date={date}"), "date must be a query param, got: " + captured.url); assertEquals(PRODUCT_ID, captured.params.get("product")); assertEquals("2024-01-01", captured.params.get("date")); assertEquals(manifestBody, result.body()); @@ -421,7 +425,7 @@ public void testGetManifestBuildsUrlAndForwardsCacheControl() { when(httpClient.getForEntity(anyString(), eq(JsonNode.class), anyMap())) .thenReturn(new ResponseEntity<>(manifestBody, headers, HttpStatus.OK)); - DasTilerService.DasJsonResult result = service.getManifest(); + DasTilerService.DasJsonResult result = service.getManifest(null); CapturedRequest captured = captureJsonRequest(); assertTrue(captured.url.contains("/visual_tiles/manifest"), "got: " + captured.url); @@ -437,7 +441,7 @@ public void testGetManifestServerErrorMappedTo502() { .thenThrow(HttpServerErrorException.create( HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", HttpHeaders.EMPTY, new byte[0], null)); - DasUpstreamException ex = assertThrows(DasUpstreamException.class, () -> service.getManifest()); + DasUpstreamException ex = assertThrows(DasUpstreamException.class, () -> service.getManifest(null)); assertEquals(HttpStatus.BAD_GATEWAY, ex.getStatus()); } 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 a9937d3d..1aa25ea5 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 @@ -118,7 +118,7 @@ public void verifyTilesMatrixSetXYZ() throws IOException { @Test public void verifyVisualMapTileMissingDatasetReturns400() { ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?variable=gsla&datetime=2024-01-01T00:00:00Z", String.class ); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -127,7 +127,7 @@ public void verifyVisualMapTileMissingDatasetReturns400() { @Test public void verifyVisualMapTileMissingVariableReturns400() { ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?dataset=model_sla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?dataset=model_sla&datetime=2024-01-01T00:00:00Z", String.class ); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -140,7 +140,7 @@ public void verifyVisualMapTileMissingVariableReturns400() { @Test public void verifyVisualMapTileRejectionUsesErrorResponseEnvelope() { ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class ); @@ -174,7 +174,7 @@ public void verifyVisualMapTileBadDatetimeReturns400() { @Test public void verifyVisualMapTileWrongTileMatrixSetReturns404() { ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WorldCRS84Quad/2/1/1?dataset=model_sla&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WorldCRS84Quad/2/1/1?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", String.class ); Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); @@ -183,7 +183,7 @@ public void verifyVisualMapTileWrongTileMatrixSetReturns404() { @Test public void verifyVisualMapTileNegativeZoomReturns400() { ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/-1/0/0?dataset=model_sla&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/-1/0/0?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", String.class ); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -192,7 +192,7 @@ public void verifyVisualMapTileNegativeZoomReturns400() { @Test public void verifyVisualMapTileZoomAboveMaxReturns400() { ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/25/0/0?dataset=model_sla&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/25/0/0?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", String.class ); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -202,7 +202,7 @@ public void verifyVisualMapTileZoomAboveMaxReturns400() { public void verifyVisualMapTileRowColOutOfRangeForZoomReturns400() { // At z=2 valid row/col range is 0-3; 4 is one past the edge. ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/4/0?dataset=model_sla&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/4/0?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", String.class ); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -210,11 +210,11 @@ public void verifyVisualMapTileRowColOutOfRangeForZoomReturns400() { @Test public void verifyVisualMapTileMaxZoomBoundaryIsAccepted() { - when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(24), eq(0), eq(0), eq("png"), isNull(), isNull())) + when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01T00:00:00Z"), eq(24), eq(0), eq(0), eq("png"), isNull(), isNull())) .thenReturn(new DasTilerService.DasTileResult("tile-bytes".getBytes(), "image/png", null)); ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/24/0/0?dataset=model_sla&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/24/0/0?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", byte[].class ); Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -225,26 +225,26 @@ 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())) + when(dasTilerService.getVisualTile(eq("wrong:gsla"), eq("2024-01-01T00:00:00Z"), 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=wrong&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/1?dataset=wrong&variable=gsla&datetime=2024-01-01T00:00:00Z", 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()); + verify(dasTilerService).getVisualTile(eq("wrong:gsla"), eq("2024-01-01T00:00:00Z"), eq(2), eq(1), eq(1), eq("png"), isNull(), isNull()); } @Test public void verifyVisualMapTileForwardsZXYAndReturnsImage() { // path .../2/1/3 -> tileMatrix=2, tileRow(y)=1, tileCol(x)=3; getVisualTile takes x before y. - when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(3), eq(1), eq("png"), isNull(), isNull())) + when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01T00:00:00Z"), eq(2), eq(3), eq(1), eq("png"), isNull(), isNull())) .thenReturn(new DasTilerService.DasTileResult("tile-bytes".getBytes(), "image/png", "public, max-age=31536000, immutable")); ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/3?dataset=model_sla&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/3?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", byte[].class ); @@ -257,11 +257,11 @@ public void verifyVisualMapTileForwardsZXYAndReturnsImage() { public void verifyVisualMapTileRebuildsProductAndMapsWebpExt() { // dataset + variable are recombined into the DAS product id `model_sla:gsla`. // path .../2/1/3 -> tileMatrix=2, tileRow(y)=1, tileCol(x)=3; getVisualTile takes x before y. - when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(3), eq(1), eq("webp"), isNull(), isNull())) + when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01T00:00:00Z"), eq(2), eq(3), eq(1), eq("webp"), isNull(), isNull())) .thenReturn(new DasTilerService.DasTileResult("webp-bytes".getBytes(), "image/webp", null)); ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/3?dataset=model_sla&variable=gsla&datetime=2024-01-01&f=webp", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/3?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z&f=webp", byte[].class ); @@ -272,11 +272,11 @@ public void verifyVisualMapTileRebuildsProductAndMapsWebpExt() { @Test public void verifyVisualMapTileUpstreamErrorMirrored() { // path .../2/1/3 -> tileMatrix=2, tileRow(y)=1, tileCol(x)=3; getVisualTile takes x before y. - when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(3), eq(1), eq("png"), isNull(), isNull())) + when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01T00:00:00Z"), eq(2), eq(3), eq(1), eq("png"), isNull(), isNull())) .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "no such date")); ResponseEntity response = testRestTemplate.getForEntity( - getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/3?dataset=model_sla&variable=gsla&datetime=2024-01-01", + getBasePath() + "/collections/some-uuid/map/tiles/WebMercatorQuad/2/1/3?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", String.class ); 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 558db85c..a17690c7 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 @@ -97,7 +97,7 @@ public void verifyCollectionProductsListsMatchingProducts() { when(dasTilerService.productsForCollection("uuid-a")).thenReturn( List.of(singleVariableProduct("model_sla:gsla", "uuid-a", "GSLA")) ); - when(dasTilerService.getManifest()).thenReturn(manifestWith("model_sla:gsla")); + when(dasTilerService.getManifest(anyString())).thenReturn(manifestWith("model_sla:gsla")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/products", JsonNode.class @@ -124,7 +124,7 @@ public void verifyScalarProductAdvertisesVisualAndDataTileTypes() { when(dasTilerService.productsForCollection("uuid-a")).thenReturn( List.of(singleVariableProduct("model_sla:gsla", "uuid-a", "GSLA")) ); - when(dasTilerService.getManifest()).thenReturn(manifestWith("model_sla:gsla")); + when(dasTilerService.getManifest(anyString())).thenReturn(manifestWith("model_sla:gsla")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/products", JsonNode.class @@ -154,7 +154,7 @@ public void verifyScalarProductAdvertisesVisualAndDataTileTypes() { @Test public void verifyCollectionProductsEmptyWhenNoneMatch() { when(dasTilerService.productsForCollection("uuid-none")).thenReturn(List.of()); - when(dasTilerService.getManifest()).thenReturn(emptyManifest()); + when(dasTilerService.getManifest(anyString())).thenReturn(emptyManifest()); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-none/products", JsonNode.class @@ -169,7 +169,7 @@ public void verifyMultiVariableProductAdvertisesDataTileTypeOnly() { when(dasTilerService.productsForCollection("uuid-a")).thenReturn( List.of(multiVariableProduct("model_currents:ucur+vcur", "uuid-a", List.of("UCUR", "VCUR"))) ); - when(dasTilerService.getManifest()).thenReturn(emptyManifest()); + when(dasTilerService.getManifest(anyString())).thenReturn(emptyManifest()); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/products", JsonNode.class @@ -205,7 +205,7 @@ public void verifyScalarWithVisualTrueAdvertisesVisualAndData() { when(dasTilerService.productsForCollection("uuid-a")).thenReturn( List.of(scalarProductWithVisual("model_sla:gsla", "uuid-a", "GSLA", true)) ); - when(dasTilerService.getManifest()).thenReturn(manifestWith("model_sla:gsla")); + when(dasTilerService.getManifest(anyString())).thenReturn(manifestWith("model_sla:gsla")); JsonNode entry = getProducts("uuid-a").get(0); @@ -222,7 +222,7 @@ public void verifyScalarWithVisualFalseAdvertisesDataOnly() { when(dasTilerService.productsForCollection("uuid-a")).thenReturn( List.of(scalarProductWithVisual("model_sla:wdir", "uuid-a", "WDIR", false)) ); - when(dasTilerService.getManifest()).thenReturn(manifestWith("model_sla:wdir")); + when(dasTilerService.getManifest(anyString())).thenReturn(manifestWith("model_sla:wdir")); JsonNode entry = getProducts("uuid-a").get(0); @@ -240,7 +240,7 @@ public void verifyPairIsDataOnlyEvenWhenDasReportsVisualExplicitly() { JsonNode product = multiVariableProduct("model_currents:ucur+vcur", "uuid-a", List.of("UCUR", "VCUR")); ((ObjectNode) product).put("visual", false); when(dasTilerService.productsForCollection("uuid-a")).thenReturn(List.of(product)); - when(dasTilerService.getManifest()).thenReturn(emptyManifest()); + when(dasTilerService.getManifest(anyString())).thenReturn(emptyManifest()); JsonNode entry = getProducts("uuid-a").get(0); @@ -262,7 +262,7 @@ public void verifyOldDasWithoutVisualFieldFallsBackToArity() { multiVariableProduct("model_currents:ucur+vcur", "uuid-a", List.of("UCUR", "VCUR")) ) ); - when(dasTilerService.getManifest()).thenReturn(manifestWith("model_sla:gsla")); + when(dasTilerService.getManifest(anyString())).thenReturn(manifestWith("model_sla:gsla")); JsonNode products = getProducts("uuid-a"); @@ -279,7 +279,7 @@ public void verifyDataCapabilityStillFollowsArityNotTheVisualField() { when(dasTilerService.productsForCollection("uuid-a")).thenReturn( List.of(scalarProductWithVisual("model_sla:wdir", "uuid-a", "WDIR", false)) ); - when(dasTilerService.getManifest()).thenReturn(emptyManifest()); + when(dasTilerService.getManifest(anyString())).thenReturn(emptyManifest()); JsonNode entry = getProducts("uuid-a").get(0); @@ -305,13 +305,13 @@ private List tileTypesOf(JsonNode entry) { @Test public void verifyDataTileReturnsImageWithCacheControl() { - when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01", 1, 0, 0)).thenReturn( + when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01T00:00:00Z", 1, 0, 0)).thenReturn( new DasTilerService.DasTileResult( "data-bytes".getBytes(), "image/png", "public, max-age=31536000, immutable")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/0/0" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01", byte[].class); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", byte[].class); Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); Assertions.assertArrayEquals("data-bytes".getBytes(), response.getBody()); @@ -323,7 +323,7 @@ public void verifyDataTileReturnsImageWithCacheControl() { public void verifyDataTileRejectsLodBelowOne() { ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/0/0/0" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01", ErrorResponse.class); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); verify(dasTilerService, never()).getDataTile(anyString(), anyString(), anyInt(), anyInt(), anyInt()); @@ -333,10 +333,10 @@ public void verifyDataTileRejectsLodBelowOne() { public void verifyDataTileRejectsNegativeXorY() { Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/-1/0" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01", ErrorResponse.class).getStatusCode()); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class).getStatusCode()); Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/0/-1" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01", ErrorResponse.class).getStatusCode()); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class).getStatusCode()); } @Test @@ -344,12 +344,12 @@ public void verifyDataTileRejectsMissingOrMalformedParams() { // missing dataset Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/0/0" - + "?variable=gsla&datetime=2024-01-01", ErrorResponse.class).getStatusCode()); + + "?variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class).getStatusCode()); // missing variable Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/0/0" - + "?dataset=model_sla&datetime=2024-01-01", ErrorResponse.class).getStatusCode()); - // datetime not YYYY-MM-DD + + "?dataset=model_sla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class).getStatusCode()); + // datetime not a full UTC ISO-8601 timestamp Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/0/0" + "?dataset=model_sla&variable=gsla&datetime=2024-1-1", ErrorResponse.class).getStatusCode()); @@ -362,7 +362,7 @@ public void verifyDataTileRejectsUnencodedPlusInVariable() { // here rather than forwarded to DAS as an unresolvable id. ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/0/0" - + "?dataset=model_sla&variable=ucur+vcur&datetime=2024-01-01", ErrorResponse.class); + + "?dataset=model_sla&variable=ucur+vcur&datetime=2024-01-01T00:00:00Z", ErrorResponse.class); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); Assertions.assertTrue(response.getBody().getMessage().contains("%2B"), @@ -375,26 +375,26 @@ 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)) + when(dasTilerService.getDataTile("wrong:gsla", "2024-01-01T00:00:00Z", 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); + + "?dataset=wrong&variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class); Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); Assertions.assertEquals("Unknown product: wrong:gsla", response.getBody().getMessage()); - verify(dasTilerService).getDataTile("wrong:gsla", "2024-01-01", 1, 0, 0); + verify(dasTilerService).getDataTile("wrong:gsla", "2024-01-01T00:00:00Z", 1, 0, 0); } @Test public void verifyDataTileMirrorsUpstreamNotFound() { - when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01", 9, 0, 0)) + when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01T00:00:00Z", 9, 0, 0)) .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "LOD 9 not in grid")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/9/0/0" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01", ErrorResponse.class); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class); Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); Assertions.assertEquals("LOD 9 not in grid", response.getBody().getMessage()); @@ -402,7 +402,7 @@ public void verifyDataTileMirrorsUpstreamNotFound() { @Test public void verifyDataTileMirrorsUpstreamServiceUnavailable() { - when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01", 1, 0, 0)) + when(dasTilerService.getDataTile("model_sla:gsla", "2024-01-01T00:00:00Z", 1, 0, 0)) .thenThrow(new DasUpstreamException(HttpStatus.SERVICE_UNAVAILABLE, "Service Unavailable")); // The Accept headers real callers send — a map client (e.g. Mapbox) that names both image formats @@ -417,7 +417,7 @@ public void verifyDataTileMirrorsUpstreamServiceUnavailable() { ResponseEntity response = testRestTemplate.exchange( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/1/0/0" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01", + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", HttpMethod.GET, new HttpEntity<>(headers), ErrorResponse.class); Assertions.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.getStatusCode(), @@ -434,12 +434,12 @@ public void verifyDataPointReturnsJsonWithCacheControl() { ObjectNode pointBody = mapper.createObjectNode(); pointBody.put("lat", -44.27813720703125); pointBody.put("lon", 132.0092315673828); - when(dasTilerService.getPoint("model_sla:gsla", "2024-01-01", -44.27, 132.00)) + when(dasTilerService.getPoint("model_sla:gsla", "2024-01-01T00:00:00Z", -44.27, 132.00)) .thenReturn(new DasTilerService.DasJsonResult(pointBody, "public, max-age=31536000, immutable")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01&lat=-44.27&lon=132.00", JsonNode.class); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z&lat=-44.27&lon=132.00", JsonNode.class); Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); Assertions.assertTrue(response.getBody().has("lat")); @@ -451,12 +451,12 @@ public void verifyDataPointRejectsMissingOrMalformedParams() { // missing dataset Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?variable=gsla&datetime=2024-01-01&lat=-44.27&lon=132.00", ErrorResponse.class).getStatusCode()); + + "?variable=gsla&datetime=2024-01-01T00:00:00Z&lat=-44.27&lon=132.00", ErrorResponse.class).getStatusCode()); // missing variable Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?dataset=model_sla&datetime=2024-01-01&lat=-44.27&lon=132.00", ErrorResponse.class).getStatusCode()); - // datetime not YYYY-MM-DD + + "?dataset=model_sla&datetime=2024-01-01T00:00:00Z&lat=-44.27&lon=132.00", ErrorResponse.class).getStatusCode()); + // datetime not a full UTC ISO-8601 timestamp Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" + "?dataset=model_sla&variable=gsla&datetime=2024-1-1&lat=-44.27&lon=132.00", ErrorResponse.class).getStatusCode()); @@ -467,19 +467,19 @@ public void verifyDataPointRejectsMissingOrOutOfRangeLatLon() { // missing lat Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01&lon=132.00", ErrorResponse.class).getStatusCode()); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z&lon=132.00", ErrorResponse.class).getStatusCode()); // missing lon Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01&lat=-44.27", ErrorResponse.class).getStatusCode()); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z&lat=-44.27", ErrorResponse.class).getStatusCode()); // lat out of range Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01&lat=91&lon=132.00", ErrorResponse.class).getStatusCode()); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z&lat=91&lon=132.00", ErrorResponse.class).getStatusCode()); // lon out of range Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01&lat=-44.27&lon=181", ErrorResponse.class).getStatusCode()); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z&lat=-44.27&lon=181", ErrorResponse.class).getStatusCode()); verify(dasTilerService, never()).getPoint(anyString(), anyString(), anyDouble(), anyDouble()); } @@ -489,7 +489,7 @@ public void verifyDataPointRejectsUnencodedPlusInVariable() { // here rather than forwarded to DAS as an unresolvable id. ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?dataset=model_sla&variable=ucur+vcur&datetime=2024-01-01&lat=-44.27&lon=132.00", ErrorResponse.class); + + "?dataset=model_sla&variable=ucur+vcur&datetime=2024-01-01T00:00:00Z&lat=-44.27&lon=132.00", ErrorResponse.class); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); Assertions.assertTrue(response.getBody().getMessage().contains("%2B"), @@ -500,28 +500,28 @@ public void verifyDataPointRejectsUnencodedPlusInVariable() { @Test public void verifyDataPointUnknownProductIsForwardedToDas() { // DAS owns the product catalogue, so an unknown dataset is its answer to give. - when(dasTilerService.getPoint("wrong:gsla", "2024-01-01", -44.27, 132.00)) + when(dasTilerService.getPoint("wrong:gsla", "2024-01-01T00:00:00Z", -44.27, 132.00)) .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "Unknown product: wrong:gsla")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/point" - + "?dataset=wrong&variable=gsla&datetime=2024-01-01&lat=-44.27&lon=132.00", ErrorResponse.class); + + "?dataset=wrong&variable=gsla&datetime=2024-01-01T00:00:00Z&lat=-44.27&lon=132.00", ErrorResponse.class); Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); Assertions.assertEquals("Unknown product: wrong:gsla", response.getBody().getMessage()); - verify(dasTilerService).getPoint("wrong:gsla", "2024-01-01", -44.27, 132.00); + verify(dasTilerService).getPoint("wrong:gsla", "2024-01-01T00:00:00Z", -44.27, 132.00); } @Test public void verifyDataManifestReturnsJsonWithCacheControl() { ObjectNode manifestBody = mapper.createObjectNode(); manifestBody.putArray("bounds").add(0).add(0).add(1).add(1); - when(dasTilerService.getDataManifest("model_sla:gsla", "2024-01-01")) + when(dasTilerService.getDataManifest("model_sla:gsla", "2024-01-01T00:00:00Z")) .thenReturn(new DasTilerService.DasJsonResult(manifestBody, "public, max-age=31536000, immutable")); ResponseEntity response = testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/manifest" - + "?dataset=model_sla&variable=gsla&datetime=2024-01-01", JsonNode.class); + + "?dataset=model_sla&variable=gsla&datetime=2024-01-01T00:00:00Z", JsonNode.class); Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); Assertions.assertTrue(response.getBody().has("bounds")); @@ -532,28 +532,28 @@ public void verifyDataManifestReturnsJsonWithCacheControl() { public void verifyDataManifestRejectsMissingOrMalformedParams() { Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/manifest" - + "?variable=gsla&datetime=2024-01-01", ErrorResponse.class).getStatusCode()); + + "?variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class).getStatusCode()); Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/manifest" + "?dataset=model_sla&variable=gsla&datetime=not-a-date", ErrorResponse.class).getStatusCode()); // an unencoded '+' in variable decodes to a space Assertions.assertEquals(HttpStatus.BAD_REQUEST, testRestTemplate.getForEntity( getExternalBasePath() + "/tiles/collections/uuid-a/data_tiles/manifest" - + "?dataset=model_sla&variable=ucur+vcur&datetime=2024-01-01", ErrorResponse.class).getStatusCode()); + + "?dataset=model_sla&variable=ucur+vcur&datetime=2024-01-01T00:00:00Z", ErrorResponse.class).getStatusCode()); } @Test public void verifyDataManifestUnknownProductIsForwardedToDas() { - when(dasTilerService.getDataManifest("wrong:gsla", "2024-01-01")) + when(dasTilerService.getDataManifest("wrong:gsla", "2024-01-01T00:00:00Z")) .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); + + "?dataset=wrong&variable=gsla&datetime=2024-01-01T00:00:00Z", ErrorResponse.class); Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); Assertions.assertEquals("Unknown product: wrong:gsla", response.getBody().getMessage()); - verify(dasTilerService).getDataManifest("wrong:gsla", "2024-01-01"); + verify(dasTilerService).getDataManifest("wrong:gsla", "2024-01-01T00:00:00Z"); } @Test