From 07cf95eb19e9a48179ff55b7f8e4efdf4f20439d Mon Sep 17 00:00:00 2001 From: Lyn Long Date: Tue, 28 Oct 2025 11:34:01 +1100 Subject: [PATCH 1/3] handle non-specified case in validation date format --- .../service/wfs/DownloadWfsDataService.java | 5 +- .../server/core/util/DatetimeUtils.java | 7 +- .../wfs/DownloadWfsDataServiceTest.java | 285 ++++++++++++++++++ .../wfs/DownloadableFieldsServiceTest.java | 13 - .../server/core/util/DatetimeUtilsTest.java | 134 ++++++++ 5 files changed, 428 insertions(+), 16 deletions(-) create mode 100644 server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java create mode 100644 server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java index 79ee9aa9..cf69c197 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java @@ -49,8 +49,9 @@ private String buildCqlFilter(String startDate, String endDate, Object multiPoly .filter(field -> "dateTime".equals(field.getType()) || "date".equals(field.getType())) .findFirst(); - // Add temporal filter - if (temporalField.isPresent() && startDate != null && endDate != null) { + // Add temporal filter only if both dates are specified + if (temporalField.isPresent() && startDate != null && !startDate.isEmpty() + && endDate != null && !endDate.isEmpty()) { String fieldName = temporalField.get().getName(); cqlFilter.append(fieldName) .append(" DURING ") 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 820ee25a..2083082e 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 @@ -18,10 +18,15 @@ private DatetimeUtils() { * * @param dateInput Input date string (supports MM-YYYY or YYYY-MM-DD formats) * @param isStartDate true for start date (first day of month), false for end date (last day of month) - * @return Formatted date string in YYYY-MM-DD format + * @return Formatted date string in YYYY-MM-DD format, or null if date is not specified * @throws IllegalArgumentException if date format is invalid */ public static String validateAndFormatDate(String dateInput, boolean isStartDate) { + // Handle null, empty, or "non-specified" dates + if (dateInput == null || dateInput.trim().isEmpty() || "non-specified".equalsIgnoreCase(dateInput.trim())) { + return null; + } + if (MM_YYYY_PATTERN.matcher(dateInput).matches()) { String[] parts = dateInput.split("-"); int month = Integer.parseInt(parts[0]); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java new file mode 100644 index 00000000..b8b61c0c --- /dev/null +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java @@ -0,0 +1,285 @@ +package au.org.aodn.ogcapi.server.core.service.wfs; + +import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.DownloadableFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wms.DescribeLayerResponse; +import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit tests for DownloadWfsDataService + */ +@ExtendWith(MockitoExtension.class) +public class DownloadWfsDataServiceTest { + + @Mock + private WmsServer wmsServer; + + @Mock + private WfsServer wfsServer; + + @Mock + private RestTemplate restTemplate; + + @Mock + private WfsDefaultParam wfsDefaultParam; + + private DownloadWfsDataService downloadWfsDataService; + + @BeforeEach + public void setUp() { + downloadWfsDataService = new DownloadWfsDataService( + wmsServer, wfsServer, restTemplate, wfsDefaultParam + ); + } + + /** + * Helper method to create a list of downloadable fields for testing + */ + private List createTestDownloadableFields() { + List fields = new ArrayList<>(); + + // Add geometry field + fields.add(DownloadableFieldModel.builder() + .name("geom") + .label("geom") + .type("geometrypropertytype") + .build()); + + // Add datetime field + fields.add(DownloadableFieldModel.builder() + .name("timestamp") + .label("timestamp") + .type("dateTime") + .build()); + + return fields; + } + + @Test + public void testPrepareWfsRequestUrl_WithNullDates() { + // Setup + String uuid = "test-uuid"; + String layerName = "test:layer"; + List fields = createTestDownloadableFields(); + + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); + DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); + DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); + + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); + when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); + when(layerDescription.getQuery()).thenReturn(query); + when(query.getTypeName()).thenReturn(layerName); + + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + + Map defaultParams = new HashMap<>(); + defaultParams.put("service", "WFS"); + defaultParams.put("version", "2.0.0"); + defaultParams.put("request", "GetFeature"); + when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); + + // Test with null dates (non-specified dates from frontend) + String result = downloadWfsDataService.prepareWfsRequestUrl( + uuid, null, null, null, null, layerName + ); + + // Verify URL doesn't contain temporal filter when dates are null + assertNotNull(result); + assertTrue(result.contains("typeName=" + layerName)); + assertFalse(result.contains("cql_filter"), "URL should not contain cql_filter when dates are null"); + } + + @Test + public void testPrepareWfsRequestUrl_WithEmptyDates() { + // Setup + String uuid = "test-uuid"; + String layerName = "test:layer"; + List fields = createTestDownloadableFields(); + + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); + DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); + DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); + + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); + when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); + when(layerDescription.getQuery()).thenReturn(query); + when(query.getTypeName()).thenReturn(layerName); + + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + + Map defaultParams = new HashMap<>(); + defaultParams.put("service", "WFS"); + defaultParams.put("version", "2.0.0"); + defaultParams.put("request", "GetFeature"); + when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); + + // Test with empty string dates + String result = downloadWfsDataService.prepareWfsRequestUrl( + uuid, "", "", null, null, layerName + ); + + // Verify URL doesn't contain temporal filter when dates are empty + assertNotNull(result); + assertTrue(result.contains("typeName=" + layerName)); + assertFalse(result.contains("cql_filter"), "URL should not contain cql_filter when dates are empty"); + } + + @Test + public void testPrepareWfsRequestUrl_WithValidDates() { + // Setup + String uuid = "test-uuid"; + String layerName = "test:layer"; + String startDate = "2023-01-01"; + String endDate = "2023-12-31"; + List fields = createTestDownloadableFields(); + + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); + DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); + DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); + + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); + when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); + when(layerDescription.getQuery()).thenReturn(query); + when(query.getTypeName()).thenReturn(layerName); + + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + + Map defaultParams = new HashMap<>(); + defaultParams.put("service", "WFS"); + defaultParams.put("version", "2.0.0"); + defaultParams.put("request", "GetFeature"); + when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); + + // Test with valid dates + String result = downloadWfsDataService.prepareWfsRequestUrl( + uuid, startDate, endDate, null, null, layerName + ); + + // Verify URL contains temporal filter when valid dates are provided + assertNotNull(result); + assertTrue(result.contains("typeName=" + layerName)); + assertTrue(result.contains("cql_filter"), "URL should contain cql_filter with valid dates"); + assertTrue(result.contains("DURING"), "CQL filter should contain DURING operator"); + assertTrue(result.contains("2023-01-01T00:00:00Z"), "CQL filter should contain start date"); + assertTrue(result.contains("2023-12-31T23:59:59Z"), "CQL filter should contain end date"); + } + + @Test + public void testPrepareWfsRequestUrl_WithOnlyStartDate() { + // Setup + String uuid = "test-uuid"; + String layerName = "test:layer"; + String startDate = "2023-01-01"; + List fields = createTestDownloadableFields(); + + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); + DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); + DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); + + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); + when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); + when(layerDescription.getQuery()).thenReturn(query); + when(query.getTypeName()).thenReturn(layerName); + + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + + Map defaultParams = new HashMap<>(); + defaultParams.put("service", "WFS"); + defaultParams.put("version", "2.0.0"); + defaultParams.put("request", "GetFeature"); + when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); + + // Test with only start date (end date is null) + String result = downloadWfsDataService.prepareWfsRequestUrl( + uuid, startDate, null, null, null, layerName + ); + + // Verify URL doesn't contain temporal filter when only one date is provided + assertNotNull(result); + assertTrue(result.contains("typeName=" + layerName)); + assertFalse(result.contains("cql_filter"), "URL should not contain cql_filter when only start date is provided"); + } + + @Test + public void testPrepareWfsRequestUrl_WithMMYYYYFormat() { + // Setup + String uuid = "test-uuid"; + String layerName = "test:layer"; + String startDate = "01-2023"; // MM-YYYY format + String endDate = "12-2023"; // MM-YYYY format + List fields = createTestDownloadableFields(); + + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); + DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); + DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); + + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); + when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); + when(layerDescription.getQuery()).thenReturn(query); + when(query.getTypeName()).thenReturn(layerName); + + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + + Map defaultParams = new HashMap<>(); + defaultParams.put("service", "WFS"); + defaultParams.put("version", "2.0.0"); + defaultParams.put("request", "GetFeature"); + when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); + + // Test with MM-YYYY format dates + String result = downloadWfsDataService.prepareWfsRequestUrl( + uuid, startDate, endDate, null, null, layerName + ); + + // Verify URL contains temporal filter with converted dates + assertNotNull(result); + assertTrue(result.contains("typeName=" + layerName)); + assertTrue(result.contains("cql_filter"), "URL should contain cql_filter"); + assertTrue(result.contains("DURING"), "CQL filter should contain DURING operator"); + // Start date should be first day of January 2023 + assertTrue(result.contains("2023-01-01T00:00:00Z"), "Start date should be converted to first day of month"); + // End date should be last day of December 2023 + assertTrue(result.contains("2023-12-31T23:59:59Z"), "End date should be converted to last day of month"); + } + + @Test + public void testPrepareWfsRequestUrl_NoWfsServerUrl() { + // Setup + String uuid = "test-uuid"; + String layerName = "test:layer"; + + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(null); + when(wfsServer.getFeatureServerUrl(eq(uuid), eq(layerName))).thenReturn(java.util.Optional.empty()); + + // Test with no WFS server URL available + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + downloadWfsDataService.prepareWfsRequestUrl( + uuid, null, null, null, null, layerName + ); + }); + + assertTrue(exception.getMessage().contains("No WFS server URL found")); + } +} + diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsServiceTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsServiceTest.java index 1cb9b909..c7444b6a 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsServiceTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsServiceTest.java @@ -219,19 +219,6 @@ public void testGetDownloadableFieldsEmptyResponse() { ); } - // @Test -// public void testGetDownloadableFieldsUnauthorizedServer() { -// when(wfsServerConfig.validateAndGetApprovedServerUrl(UNAUTHORIZED_SERVER)) -// .thenThrow(new UnauthorizedServerException("Access to WFS server '" + UNAUTHORIZED_SERVER + "' is not authorized")); -// UnauthorizedServerException exception = assertThrows( -// UnauthorizedServerException.class, -// () -> downloadableFieldsService.getDownloadableFields(UNAUTHORIZED_SERVER, "test:layer") -// ); -// -// assertTrue(exception.getMessage().contains("not authorized")); -// assertTrue(exception.getMessage().contains(UNAUTHORIZED_SERVER)); -// } -// @Test public void testGetDownloadableFieldsWfsError() { FeatureRequest request = FeatureRequest.builder().layerName("invalid:layer").build(); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java new file mode 100644 index 00000000..766b6911 --- /dev/null +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java @@ -0,0 +1,134 @@ +package au.org.aodn.ogcapi.server.core.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for DatetimeUtils + */ +public class DatetimeUtilsTest { + + @Test + public void testValidateAndFormatDate_ValidYYYY_MM_DD() { + // Test valid YYYY-MM-DD format + String result = DatetimeUtils.validateAndFormatDate("2023-01-15", true); + assertEquals("2023-01-15", result); + } + + @Test + public void testValidateAndFormatDate_ValidMM_YYYY_StartDate() { + // Test MM-YYYY format for start date (should return first day of month) + String result = DatetimeUtils.validateAndFormatDate("01-2023", true); + assertEquals("2023-01-01", result); + } + + @Test + public void testValidateAndFormatDate_ValidMM_YYYY_EndDate() { + // Test MM-YYYY format for end date (should return last day of month) + String result = DatetimeUtils.validateAndFormatDate("02-2023", false); + assertEquals("2023-02-28", result); + } + + @Test + public void testValidateAndFormatDate_LeapYear() { + // Test February in leap year + String result = DatetimeUtils.validateAndFormatDate("02-2024", false); + assertEquals("2024-02-29", result); + } + + @Test + public void testValidateAndFormatDate_Month31Days() { + // Test month with 31 days + String result = DatetimeUtils.validateAndFormatDate("01-2023", false); + assertEquals("2023-01-31", result); + } + + @Test + public void testValidateAndFormatDate_Month30Days() { + // Test month with 30 days + String result = DatetimeUtils.validateAndFormatDate("04-2023", false); + assertEquals("2023-04-30", result); + } + + @Test + public void testValidateAndFormatDate_NullDate() { + // Test null date returns null + String result = DatetimeUtils.validateAndFormatDate(null, true); + assertNull(result, "Null date should return null"); + } + + @Test + public void testValidateAndFormatDate_EmptyString() { + // Test empty string returns null + String result = DatetimeUtils.validateAndFormatDate("", true); + assertNull(result, "Empty string should return null"); + } + + @Test + public void testValidateAndFormatDate_WhitespaceString() { + // Test whitespace string returns null + String result = DatetimeUtils.validateAndFormatDate(" ", false); + assertNull(result, "Whitespace string should return null"); + } + + @Test + public void testValidateAndFormatDate_NonSpecified() { + // Test "non-specified" returns null (case-insensitive) + String result1 = DatetimeUtils.validateAndFormatDate("non-specified", true); + assertNull(result1, "non-specified should return null"); + + String result2 = DatetimeUtils.validateAndFormatDate("NON-SPECIFIED", false); + assertNull(result2, "NON-SPECIFIED should return null"); + + String result3 = DatetimeUtils.validateAndFormatDate("Non-Specified", true); + assertNull(result3, "Non-Specified should return null"); + } + + @Test + public void testValidateAndFormatDate_InvalidFormat() { + // Test invalid format throws exception + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + DatetimeUtils.validateAndFormatDate("2023/01/15", true); + }); + assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format")); + } + + @Test + public void testValidateAndFormatDate_InvalidMonth() { + // Test invalid month throws exception + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + DatetimeUtils.validateAndFormatDate("13-2023", true); + }); + assertTrue(exception.getMessage().contains("Invalid month in date")); + } + + + @Test + public void testValidateAndFormatDate_InvalidFormat_Slashes() { + // Test date with slashes instead of dashes + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + DatetimeUtils.validateAndFormatDate("2023/02/15", true); + }); + assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format")); + } + + @Test + public void testValidateAndFormatDate_IncompleteDate() { + // Test incomplete date format + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + DatetimeUtils.validateAndFormatDate("2023-01", true); + }); + assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format")); + } + + @Test + public void testValidateAndFormatDate_RandomString() { + // Test random string throws exception + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + DatetimeUtils.validateAndFormatDate("random-text", true); + }); + assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format")); + } +} + From 631b6bc1abc026194066a9b3fa435bbab3832906 Mon Sep 17 00:00:00 2001 From: Lyn Long Date: Tue, 28 Oct 2025 11:50:04 +1100 Subject: [PATCH 2/3] fix format --- .../service/wfs/DownloadWfsDataService.java | 3 +- .../server/core/util/DatetimeUtils.java | 2 +- .../wfs/DownloadWfsDataServiceTest.java | 73 +++++++++---------- .../server/core/util/DatetimeUtilsTest.java | 2 - 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java index cf69c197..1f32c405 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java @@ -50,8 +50,7 @@ private String buildCqlFilter(String startDate, String endDate, Object multiPoly .findFirst(); // Add temporal filter only if both dates are specified - if (temporalField.isPresent() && startDate != null && !startDate.isEmpty() - && endDate != null && !endDate.isEmpty()) { + if (temporalField.isPresent() && startDate != null && !startDate.isEmpty() && endDate != null && !endDate.isEmpty()) { String fieldName = temporalField.get().getName(); cqlFilter.append(fieldName) .append(" DURING ") 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 2083082e..2480f85a 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 @@ -26,7 +26,7 @@ public static String validateAndFormatDate(String dateInput, boolean isStartDate if (dateInput == null || dateInput.trim().isEmpty() || "non-specified".equalsIgnoreCase(dateInput.trim())) { return null; } - + if (MM_YYYY_PATTERN.matcher(dateInput).matches()) { String[] parts = dateInput.split("-"); int month = Integer.parseInt(parts[0]); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java index b8b61c0c..b444c1bd 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java @@ -52,21 +52,21 @@ public void setUp() { */ private List createTestDownloadableFields() { List fields = new ArrayList<>(); - + // Add geometry field fields.add(DownloadableFieldModel.builder() .name("geom") .label("geom") .type("geometrypropertytype") .build()); - + // Add datetime field fields.add(DownloadableFieldModel.builder() .name("timestamp") .label("timestamp") .type("dateTime") .build()); - + return fields; } @@ -76,30 +76,30 @@ public void testPrepareWfsRequestUrl_WithNullDates() { String uuid = "test-uuid"; String layerName = "test:layer"; List fields = createTestDownloadableFields(); - + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); when(layerDescription.getQuery()).thenReturn(query); when(query.getTypeName()).thenReturn(layerName); - + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); - + Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); defaultParams.put("version", "2.0.0"); defaultParams.put("request", "GetFeature"); when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); - + // Test with null dates (non-specified dates from frontend) String result = downloadWfsDataService.prepareWfsRequestUrl( uuid, null, null, null, null, layerName ); - + // Verify URL doesn't contain temporal filter when dates are null assertNotNull(result); assertTrue(result.contains("typeName=" + layerName)); @@ -112,30 +112,30 @@ public void testPrepareWfsRequestUrl_WithEmptyDates() { String uuid = "test-uuid"; String layerName = "test:layer"; List fields = createTestDownloadableFields(); - + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); when(layerDescription.getQuery()).thenReturn(query); when(query.getTypeName()).thenReturn(layerName); - + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); - + Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); defaultParams.put("version", "2.0.0"); defaultParams.put("request", "GetFeature"); when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); - + // Test with empty string dates String result = downloadWfsDataService.prepareWfsRequestUrl( uuid, "", "", null, null, layerName ); - + // Verify URL doesn't contain temporal filter when dates are empty assertNotNull(result); assertTrue(result.contains("typeName=" + layerName)); @@ -150,30 +150,30 @@ public void testPrepareWfsRequestUrl_WithValidDates() { String startDate = "2023-01-01"; String endDate = "2023-12-31"; List fields = createTestDownloadableFields(); - + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); when(layerDescription.getQuery()).thenReturn(query); when(query.getTypeName()).thenReturn(layerName); - + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); - + Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); defaultParams.put("version", "2.0.0"); defaultParams.put("request", "GetFeature"); when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); - + // Test with valid dates String result = downloadWfsDataService.prepareWfsRequestUrl( uuid, startDate, endDate, null, null, layerName ); - + // Verify URL contains temporal filter when valid dates are provided assertNotNull(result); assertTrue(result.contains("typeName=" + layerName)); @@ -190,30 +190,30 @@ public void testPrepareWfsRequestUrl_WithOnlyStartDate() { String layerName = "test:layer"; String startDate = "2023-01-01"; List fields = createTestDownloadableFields(); - + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); when(layerDescription.getQuery()).thenReturn(query); when(query.getTypeName()).thenReturn(layerName); - + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); - + Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); defaultParams.put("version", "2.0.0"); defaultParams.put("request", "GetFeature"); when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); - + // Test with only start date (end date is null) String result = downloadWfsDataService.prepareWfsRequestUrl( uuid, startDate, null, null, null, layerName ); - + // Verify URL doesn't contain temporal filter when only one date is provided assertNotNull(result); assertTrue(result.contains("typeName=" + layerName)); @@ -228,30 +228,30 @@ public void testPrepareWfsRequestUrl_WithMMYYYYFormat() { String startDate = "01-2023"; // MM-YYYY format String endDate = "12-2023"; // MM-YYYY format List fields = createTestDownloadableFields(); - + DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - + when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); when(layerDescription.getQuery()).thenReturn(query); when(query.getTypeName()).thenReturn(layerName); - + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); - + Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); defaultParams.put("version", "2.0.0"); defaultParams.put("request", "GetFeature"); when(wfsDefaultParam.getDownload()).thenReturn(defaultParams); - + // Test with MM-YYYY format dates String result = downloadWfsDataService.prepareWfsRequestUrl( uuid, startDate, endDate, null, null, layerName ); - + // Verify URL contains temporal filter with converted dates assertNotNull(result); assertTrue(result.contains("typeName=" + layerName)); @@ -268,18 +268,17 @@ public void testPrepareWfsRequestUrl_NoWfsServerUrl() { // Setup String uuid = "test-uuid"; String layerName = "test:layer"; - + when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(null); when(wfsServer.getFeatureServerUrl(eq(uuid), eq(layerName))).thenReturn(java.util.Optional.empty()); - + // Test with no WFS server URL available Exception exception = assertThrows(IllegalArgumentException.class, () -> { downloadWfsDataService.prepareWfsRequestUrl( uuid, null, null, null, null, layerName ); }); - + assertTrue(exception.getMessage().contains("No WFS server URL found")); } } - diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java index 766b6911..27d3af05 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java @@ -103,7 +103,6 @@ public void testValidateAndFormatDate_InvalidMonth() { assertTrue(exception.getMessage().contains("Invalid month in date")); } - @Test public void testValidateAndFormatDate_InvalidFormat_Slashes() { // Test date with slashes instead of dashes @@ -131,4 +130,3 @@ public void testValidateAndFormatDate_RandomString() { assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format")); } } - From efd0b4d7d0c2efec645cdbe9e5a465a46a042b16 Mon Sep 17 00:00:00 2001 From: Lyn Long Date: Tue, 28 Oct 2025 17:20:07 +1100 Subject: [PATCH 3/3] refactor code --- .../au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java | 3 ++- .../au/org/aodn/ogcapi/server/processes/RestServices.java | 5 +++-- .../org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) 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 2480f85a..8837bd35 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 @@ -7,6 +7,7 @@ public class DatetimeUtils { private static final Pattern MM_YYYY_PATTERN = Pattern.compile("^(\\d{2})-(\\d{4})$"); private static final Pattern YYYY_MM_DD_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); private static final DateTimeFormatter ISO_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + public static final String NON_SPECIFIED_DATE = "non-specified"; private DatetimeUtils() { } @@ -23,7 +24,7 @@ private DatetimeUtils() { */ public static String validateAndFormatDate(String dateInput, boolean isStartDate) { // Handle null, empty, or "non-specified" dates - if (dateInput == null || dateInput.trim().isEmpty() || "non-specified".equalsIgnoreCase(dateInput.trim())) { + if (dateInput == null || dateInput.trim().isEmpty() || NON_SPECIFIED_DATE.equalsIgnoreCase(dateInput.trim())) { return null; } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java index 30b50071..0321ec48 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java @@ -3,6 +3,7 @@ import au.org.aodn.ogcapi.server.core.exception.wfs.WfsErrorHandler; import au.org.aodn.ogcapi.server.core.model.enumeration.DatasetDownloadEnums; import au.org.aodn.ogcapi.server.core.service.wfs.DownloadWfsDataService; +import au.org.aodn.ogcapi.server.core.util.DatetimeUtils; import au.org.aodn.ogcapi.server.core.util.EmailUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -121,8 +122,8 @@ private String generateStartedEmailContent(String uuid, String startDate, String String template = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); // Handle dates - only show if not "non-specified" - String displayStartDate = (startDate != null && !startDate.equals("non-specified")) ? startDate.replace("-", "/") : ""; - String displayEndDate = (endDate != null && !endDate.equals("non-specified")) ? endDate.replace("-", "/") : ""; + String displayStartDate = (startDate != null && !startDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) ? startDate.replace("-", "/") : ""; + String displayEndDate = (endDate != null && !endDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) ? endDate.replace("-", "/") : ""; // Generate dynamic bbox HTML String bboxHtml = EmailUtils.generateBboxHtml(multipolygon, objectMapper); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java index 27d3af05..c72c0abb 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java @@ -75,14 +75,14 @@ public void testValidateAndFormatDate_WhitespaceString() { @Test public void testValidateAndFormatDate_NonSpecified() { // Test "non-specified" returns null (case-insensitive) - String result1 = DatetimeUtils.validateAndFormatDate("non-specified", true); + String result1 = DatetimeUtils.validateAndFormatDate(DatetimeUtils.NON_SPECIFIED_DATE, true); assertNull(result1, "non-specified should return null"); - String result2 = DatetimeUtils.validateAndFormatDate("NON-SPECIFIED", false); + String result2 = DatetimeUtils.validateAndFormatDate(DatetimeUtils.NON_SPECIFIED_DATE.toUpperCase(), false); assertNull(result2, "NON-SPECIFIED should return null"); String result3 = DatetimeUtils.validateAndFormatDate("Non-Specified", true); - assertNull(result3, "Non-Specified should return null"); + assertNull(result3, "Non-Specified (mixed case) should return null"); } @Test