diff --git a/src/main/java/org/opengis/cite/wfs30/SuiteAttribute.java b/src/main/java/org/opengis/cite/wfs30/SuiteAttribute.java index 456d5e38..9937515b 100644 --- a/src/main/java/org/opengis/cite/wfs30/SuiteAttribute.java +++ b/src/main/java/org/opengis/cite/wfs30/SuiteAttribute.java @@ -2,6 +2,7 @@ import java.io.File; import java.net.URI; +import java.util.List; import com.reprezen.kaizen.oasparser.model3.OpenApi3; import com.sun.jersey.api.client.Client; @@ -27,7 +28,15 @@ public enum SuiteAttribute { */ TEST_SUBJ_FILE( "testSubjectFile", File.class ), - API_MODEL( "apiModel", OpenApi3.class ); + /** + * Parsed OpenApi3 document resource /api; Added during execution. + */ + API_MODEL( "apiModel", OpenApi3.class ), + + /** + * Parsed collections from resource /collections; Added during execution. + */ + COLLECTIONS( "collections", List.class ); private final Class attrType; diff --git a/src/main/java/org/opengis/cite/wfs30/WFS3.java b/src/main/java/org/opengis/cite/wfs30/WFS3.java index 656843c9..c1f23e54 100644 --- a/src/main/java/org/opengis/cite/wfs30/WFS3.java +++ b/src/main/java/org/opengis/cite/wfs30/WFS3.java @@ -12,6 +12,8 @@ private WFS3() { public static final String OPEN_API_MIME_TYPE = "application/openapi+json;version=3.0"; + public static final String GEOJSON_MIME_TYPE = "application/geo+json"; + public enum PATH { API( "api" ), CONFORMANCE( "conformance" ), COLLECTIONS( "collections" ); diff --git a/src/main/java/org/opengis/cite/wfs30/collections/FeatureCollectionsMetadataOperation.java b/src/main/java/org/opengis/cite/wfs30/collections/FeatureCollectionsMetadataOperation.java index e2c72519..3e35dbd5 100644 --- a/src/main/java/org/opengis/cite/wfs30/collections/FeatureCollectionsMetadataOperation.java +++ b/src/main/java/org/opengis/cite/wfs30/collections/FeatureCollectionsMetadataOperation.java @@ -5,6 +5,11 @@ import static org.opengis.cite.wfs30.SuiteAttribute.API_MODEL; import static org.opengis.cite.wfs30.WFS3.PATH.COLLECTIONS; import static org.opengis.cite.wfs30.openapi3.OpenApiUtils.retrieveTestPoints; +import static org.opengis.cite.wfs30.util.JsonUtils.findLinkByRel; +import static org.opengis.cite.wfs30.util.JsonUtils.findLinksWithSupportedMediaTypeByRel; +import static org.opengis.cite.wfs30.util.JsonUtils.findLinksWithoutRelOrType; +import static org.opengis.cite.wfs30.util.JsonUtils.findUnsupportedTypes; +import static org.opengis.cite.wfs30.util.JsonUtils.linkIncludesRelAndType; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; @@ -15,10 +20,12 @@ import java.util.Map; import org.opengis.cite.wfs30.CommonFixture; +import org.opengis.cite.wfs30.SuiteAttribute; import org.opengis.cite.wfs30.openapi3.OpenApiUtils; import org.opengis.cite.wfs30.openapi3.TestPoint; import org.testng.ITestContext; import org.testng.SkipException; +import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -86,6 +93,15 @@ public void openApiDocument( ITestContext testContext ) { this.apiModel = (OpenApi3) testContext.getSuite().getAttribute( API_MODEL.getName() ); } + @AfterClass + public void storeCollectionsInTestContext( ITestContext testContext ) { + List> collections = new ArrayList<>(); + for ( List> testPointAndCollection : testPointAndCollections.values() ) { + collections.addAll( testPointAndCollection ); + } + testContext.getSuite().setAttribute( SuiteAttribute.COLLECTIONS.getName(), collections ); + } + /** * A.4.4.4. Validate the Feature Collections Metadata Operation * @@ -104,8 +120,8 @@ public void openApiDocument( ITestContext testContext ) { * @param testPoint * the test point to test, never null */ - @Test(description = "Implements A.4.4.4. Validate the Feature Collections Metadata Operation (Requirement 9, 10)", dataProvider = "collectionsUris", dependsOnGroups = "apidefinition") - public void validateFeatureCollectionsMetadataOperation(TestPoint testPoint ) { + @Test(description = "Implements A.4.4.4. Validate the Feature Collections Metadata Operation (Requirement 9, 10)", groups = "collections", dataProvider = "collectionsUris", dependsOnGroups = "apidefinition") + public void validateFeatureCollectionsMetadataOperation( TestPoint testPoint ) { String testPointUri = testPoint.createUri(); Response response = init().baseUri( testPointUri ).accept( JSON ).when().request( GET ); response.then().statusCode( 200 ); @@ -131,26 +147,26 @@ public void validateFeatureCollectionsMetadataOperation(TestPoint testPoint ) { * @param testPoint * the test point to test, never null */ - @Test(description = "Implements A.4.4.5. Validate the Feature Collections Metadata Operation Response (Requirement 11)", dataProvider = "collectionsUris", dependsOnMethods = "validateFeatureCollectionsMetadataOperation") + @Test(description = "Implements A.4.4.5. Validate the Feature Collections Metadata Operation Response (Requirement 11)", groups = "collections", dataProvider = "collectionsUris", dependsOnMethods = "validateFeatureCollectionsMetadataOperation") public void validateFeatureCollectionsMetadataOperationResponse_Links( TestPoint testPoint ) { Response response = testPointAndResponses.get( testPoint ); if ( response == null ) throw new SkipException( "Could not find a response for test point " + testPoint ); JsonPath jsonPath = response.jsonPath(); + List> links = jsonPath.getList( "links" ); // Validate that the retrieved document includes links for: Itself, - Map linkToSelf = findLinkToItself( jsonPath ); + Map linkToSelf = findLinkByRel( links, "self" ); assertNotNull( linkToSelf, "Feature Collection Metadata document must include a link for itself" ); assertTrue( linkIncludesRelAndType( linkToSelf ), "Link to itself must include a rel and type parameter" ); // Validate that the retrieved document includes links for: Itself, Alternate encodings of this document in // every other media type as identified by the compliance classes for this server. List mediaTypesToSupport = createListOfMediaTypesToSupport( testPoint, linkToSelf ); - List> alternateLinks = findLinksWithSupportedMediaTypeByRel( jsonPath.getList( "links" ), - mediaTypesToSupport, + List> alternateLinks = findLinksWithSupportedMediaTypeByRel( links, mediaTypesToSupport, "alternate" ); - List typesWithoutLink = findLinksWithoutTypes( alternateLinks, mediaTypesToSupport ); + List typesWithoutLink = findUnsupportedTypes( alternateLinks, mediaTypesToSupport ); assertTrue( typesWithoutLink.isEmpty(), "Feature Collection Metadata document must include links for alternate encodings. Missing links for types " + typesWithoutLink ); @@ -177,7 +193,7 @@ public void validateFeatureCollectionsMetadataOperationResponse_Links( TestPoint * @param testPoint * the test point to test, never null */ - @Test(description = "Implements A.4.4.5. Validate the Feature Collections Metadata Operation Response (Requirement 12)", dataProvider = "collectionsUris", dependsOnMethods = "validateFeatureCollectionsMetadataOperation") + @Test(description = "Implements A.4.4.5. Validate the Feature Collections Metadata Operation Response (Requirement 12)", groups = "collections", dataProvider = "collectionsUris", dependsOnMethods = "validateFeatureCollectionsMetadataOperation") public void validateFeatureCollectionsMetadataOperationResponse_Collections( TestPoint testPoint ) { Response response = testPointAndResponses.get( testPoint ); if ( response == null ) @@ -215,8 +231,8 @@ public void validateFeatureCollectionsMetadataOperationResponse_Collections( Tes * @param collection * the collection to test, never null */ - @Test(description = "Implements A.4.4.6. Validate a Collections Metadata document (Requirement 13)", dataProvider = "collections", dependsOnMethods = "validateFeatureCollectionsMetadataOperationResponse_Collections") - public void validateCollectionsMetadataDocument_Links(TestPoint testPoint, Map collection ) { + @Test(description = "Implements A.4.4.6. Validate a Collections Metadata document (Requirement 13)", groups = "collections", dataProvider = "collections", dependsOnMethods = "validateFeatureCollectionsMetadataOperationResponse_Collections") + public void validateCollectionsMetadataDocument_Links( TestPoint testPoint, Map collection ) { String collectionName = (String) collection.get( "name" ); List testPointsForNamedCollection = OpenApiUtils.retrieveTestPoints( apiModel, COLLECTIONS, collectionName ); @@ -225,11 +241,11 @@ public void validateCollectionsMetadataDocument_Links(TestPoint testPoint, Map mediaTypesToSupport = createListOfMediaTypesToSupport( testPointsForNamedCollection.get( 0 ), null ); - List links = (List) collection.get( "links" ); + List> links = (List>) collection.get( "links" ); List> alternateLinks = findLinksWithSupportedMediaTypeByRel( links, mediaTypesToSupport, "item" ); - List typesWithoutLink = findLinksWithoutTypes( alternateLinks, mediaTypesToSupport ); + List typesWithoutLink = findUnsupportedTypes( alternateLinks, mediaTypesToSupport ); assertTrue( typesWithoutLink.isEmpty(), "Collections Metadata document must include links with relation 'item' for each supported encodings. Missing links for types " + typesWithoutLink ); @@ -257,8 +273,8 @@ public void validateCollectionsMetadataDocument_Links(TestPoint testPoint, Mapnull */ - @Test(description = "Implements A.4.4.6. Validate a Collections Metadata document (Requirement 14)", dataProvider = "collections", dependsOnMethods = "validateFeatureCollectionsMetadataOperationResponse_Collections") - public void validateCollectionsMetadataDocument_Extent(TestPoint testPoint, Map collection ) { + @Test(description = "Implements A.4.4.6. Validate a Collections Metadata document (Requirement 14)", groups = "collections", dataProvider = "collections", dependsOnMethods = "validateFeatureCollectionsMetadataOperationResponse_Collections") + public void validateCollectionsMetadataDocument_Extent( TestPoint testPoint, Map collection ) { // TODO: validate the extent property } @@ -271,8 +287,9 @@ public void validateCollectionsMetadataDocument_Extent(TestPoint testPoint, Map< * @param collection * the collection to test, never null */ - @Test(description = "Implements A.4.4.7. Validate the Feature Collection Metadata Operation (Requirement 15) and A.4.4.8. Validate the Feature Collection Metadata Operation Response (Requirement 16)", dataProvider = "collections", dependsOnMethods = "validateFeatureCollectionsMetadataOperationResponse_Collections") - public void validateTheFeatureCollectionMetadataOperationAndResponse(TestPoint testPoint, Map collection ) { + @Test(description = "Implements A.4.4.7. Validate the Feature Collection Metadata Operation (Requirement 15) and A.4.4.8. Validate the Feature Collection Metadata Operation Response (Requirement 16)", groups = "collections", dataProvider = "collections", dependsOnMethods = "validateFeatureCollectionsMetadataOperationResponse_Collections") + public void validateTheFeatureCollectionMetadataOperationAndResponse( TestPoint testPoint, + Map collection ) { String collectionName = (String) collection.get( "name" ); List testPointsForNamedCollection = OpenApiUtils.retrieveTestPoints( apiModel, COLLECTIONS, collectionName ); @@ -308,7 +325,7 @@ public void validateTheFeatureCollectionMetadataOperationAndResponse(TestPoint t * @param testPoint * to test, never null */ - private Response validateTheFeatureCollectionMetadataOperationAndResponse(TestPoint testPoint ) { + private Response validateTheFeatureCollectionMetadataOperationAndResponse( TestPoint testPoint ) { String testPointUri = testPoint.createUri(); Response response = init().baseUri( testPointUri ).accept( JSON ).when().request( GET ); response.then().statusCode( 200 ); @@ -349,68 +366,6 @@ private List findMissingCollectionNames( List collections ) { return missingCollectionNames; } - private Map findLinkToItself( JsonPath jsonPath ) { - List links = jsonPath.getList( "links" ); - for ( Object link : links ) { - Map linkItem = (Map) link; - Object rel = linkItem.get( "rel" ); - if ( "self".equals( rel ) ) - return linkItem; - } - return null; - } - - private List> findLinksWithSupportedMediaTypeByRel( List links, - List mediaTypesToSupport, - String expectedRel ) { - List> alternateLinks = new ArrayList<>(); - for ( Object link : links ) { - Map linkItem = (Map) link; - Object type = linkItem.get( "type" ); - Object rel = linkItem.get( "rel" ); - if ( expectedRel.equals( rel ) && isSupportedMediaType( type, mediaTypesToSupport ) ) - alternateLinks.add( linkItem ); - } - return alternateLinks; - } - - private List findLinksWithoutTypes( List> alternateLinks, - List mediaTypesToSuppport ) { - List missingLinksForType = new ArrayList<>(); - for ( String contentMediaType : mediaTypesToSuppport ) { - boolean hasLinkForContentType = hasLinkForContentType( alternateLinks, contentMediaType ); - if ( !hasLinkForContentType ) - missingLinksForType.add( contentMediaType ); - } - return missingLinksForType; - } - - private boolean hasLinkForContentType( List> alternateLinks, String mediaType ) { - for ( Map alternateLink : alternateLinks ) { - Object type = alternateLink.get( "type" ); - if ( mediaType.equals( type ) ) - return true; - } - return false; - } - - private List findLinksWithoutRelOrType( List> alternateLinks ) { - List linksWithoutRelOrType = new ArrayList<>(); - for ( Map alternateLink : alternateLinks ) { - if ( !linkIncludesRelAndType( alternateLink ) ) - linksWithoutRelOrType.add( (String) alternateLink.get( "href" ) ); - } - return linksWithoutRelOrType; - } - - private boolean linkIncludesRelAndType( Map link ) { - Object rel = link.get( "rel" ); - Object type = link.get( "type" ); - if ( rel != null && type != null ) - return true; - return false; - } - private Map findCollectionByName( String collectionNameFromLandingPage, List collections ) { for ( Object collectionObject : collections ) { Map collection = (Map) collectionObject; @@ -421,14 +376,6 @@ private Map findCollectionByName( String collectionNameFromLandi return null; } - private boolean isSupportedMediaType( Object type, List contentMediaTypes ) { - for ( String contentMediaType : contentMediaTypes ) { - if ( contentMediaType.equals( type ) ) - return true; - } - return false; - } - private List> createCollectionsMap( List collections ) { List> collectionsMap = new ArrayList<>(); for ( Object collection : collections ) @@ -445,4 +392,4 @@ private List createListOfMediaTypesToSupport( TestPoint testPoint, MapLyn Goltz + */ +public class GetFeaturesOperation extends CommonFixture { + + private final Map collectionNameAndResponse = new HashMap<>(); + + private List> collections; + + private OpenApi3 apiModel; + + @DataProvider(name = "collectionItemUris") + public Iterator collectionItemUris( ITestContext testContext ) { + List collectionsData = new ArrayList<>(); + for ( Map collection : collections ) { + collectionsData.add( new Object[] { collection } ); + } + return collectionsData.iterator(); + } + + @DataProvider(name = "collectionItemUrisWithLimit") + public Iterator collectionItemUrisWithLimits( ITestContext testContext ) { + List collectionsWithLimits = new ArrayList<>(); + for ( Map collection : collections ) { + Parameter limit = findParameterByName( (String) collection.get( "name" ), "limit" ); + int[] ints = getTwoRandomLimits( limit ); + collectionsWithLimits.add( new Object[] { collection, ints[0] } ); + collectionsWithLimits.add( new Object[] { collection, ints[0] } ); + } + return collectionsWithLimits.iterator(); + } + + @DataProvider(name = "collectionItemUrisWithBboxes") + public Iterator collectionItemUrisWithBboxes( ITestContext testContext ) { + List collectionsWithBboxes = new ArrayList<>(); + for ( Map collection : collections ) { + BBox extent = parseSpatialExtent( collection ); + if ( extent != null ) { + collectionsWithBboxes.add( new Object[] { collection, extent } ); + // These should include test cases which cross the + // meridian, + collectionsWithBboxes.add( new Object[] { collection, new BBox( -1.5, 50.0, 1.5, 53.0 ) } ); + // equator, + collectionsWithBboxes.add( new Object[] { collection, new BBox( -80.0, -5.0, -70.0, 5.0 ) } ); + // 180 longitude, + collectionsWithBboxes.add( new Object[] { collection, new BBox( 177.0, 65.0, -177.0, 70.0 ) } ); + // and polar regions. + collectionsWithBboxes.add( new Object[] { collection, new BBox( -70.0, -20.0, -70.0, 160.0 ) } ); + collectionsWithBboxes.add( new Object[] { collection, new BBox( 70.0, -20.0, 70.0, 160.0 ) } ); + } + } + return collectionsWithBboxes.iterator(); + } + + @DataProvider(name = "collectionItemUrisWithTimes") + public Iterator collectionItemUrisWithTimes( ITestContext testContext ) { + List collectionsWithTimes = new ArrayList<>(); + for ( Map collection : collections ) { + TemporalExtent temporalExtent = parseTemporalExtent( collection ); + if ( temporalExtent != null ) { + ZonedDateTime begin = temporalExtent.getBegin(); + ZonedDateTime end = temporalExtent.getEnd(); + + Duration between = Duration.between( begin, end ); + Duration quarter = between.dividedBy( 4 ); + ZonedDateTime beginInterval = begin.plus( quarter ); + ZonedDateTime endInterval = beginInterval.plus( quarter ); + + // Example 6. A date-time + collectionsWithTimes.add( new Object[] { collection, formatDate( begin ), beginInterval, null } ); + // Example 7. A period using a start and end time + collectionsWithTimes.add( new Object[] { collection, formatDateRange( beginInterval, endInterval ), + beginInterval, endInterval } ); + // Example 8. A period using start time and a duration + LocalDate beginIntervalDate = beginInterval.toLocalDate(); + LocalDate endIntervalDate = beginIntervalDate.plusDays( 2 ); + collectionsWithTimes.add( new Object[] { + collection, + formatDateRangeWithDuration( beginIntervalDate, endIntervalDate ), + beginIntervalDate, endIntervalDate } ); + } + } + return collectionsWithTimes.iterator(); + } + + @BeforeClass + public void retrieveRequiredInformationFromTestContext( ITestContext testContext ) { + this.apiModel = (OpenApi3) testContext.getSuite().getAttribute( API_MODEL.getName() ); + this.collections = (List>) testContext.getSuite().getAttribute( SuiteAttribute.COLLECTIONS.getName() ); + } + + /** + * A.4.4.9. Validate the Get Features Operation + * + * a) Test Purpose: Validate that the Get Features Operation behaves as required. + * + * b) Pre-conditions: + * + * A feature collection name is provided by test A.4.4.6 + * + * Path = /collections/{name}/items + * + * c) Test Method: + * + * DO FOR each /collections{name}/items test point + * + * Issue an HTTP GET request using the test point URI + * + * Go to test A.4.4.10 + * + * d) References: Requirement 17 + * + * @param collection + * the collection under test, never null + */ + @Test(description = "Implements A.4.4.9. Validate the Get Features Operation (Requirement 17, 24)", groups = "getFeaturesBase", dataProvider = "collectionItemUris", dependsOnGroups = "collections") + public void validateTheGetFeaturesOperation( Map collection ) { + String collectionName = (String) collection.get( "name" ); + + String getFeaturesUrl = findGetFeatureUrlForGeoJson( collection ); + if ( getFeaturesUrl.isEmpty() ) + throw new SkipException( "Could not find url for collection with name " + collectionName + + " supporting GeoJson (type " + GEOJSON_MIME_TYPE + ")" ); + + ZonedDateTime timeStampBeforeResponse = ZonedDateTime.now(); + Response response = init().baseUri( getFeaturesUrl ).accept( GEOJSON_MIME_TYPE ).when().request( GET ); + response.then().statusCode( 200 ); + ZonedDateTime timeStampAfterResponse = ZonedDateTime.now(); + ResponseData responseData = new ResponseData( response, timeStampBeforeResponse, timeStampAfterResponse ); + collectionNameAndResponse.put( collectionName, responseData ); + } + + /** + * A.4.4.10. Validate the Get Features Operation Response (Test method 2, 3) + * + * a) Test Purpose: Validate the Get Feature Operation Response. + * + * b) Pre-conditions: A collection of Features has been retrieved + * + * c) Test Method: + * + * Validate that the following links are included in the response document: To itself, Alternate encodings of this + * document in every other media type as identified by the compliance classes for this server. + * + * Validate that each link includes a rel and type parameter. + * + * d) References: Requirements 25, 26 + * + * @param collection + * the collection under test, never null + */ + @Test(description = "Implements A.4.4.10. Validate the Get Features Operation Response (Requirement 25, 26)", groups = "getFeaturesBase", dataProvider = "collectionItemUris", dependsOnMethods = "validateTheGetFeaturesOperation") + public void validateTheGetFeaturesOperationResponse_Links( Map collection ) { + String collectionName = (String) collection.get( "name" ); + ResponseData response = collectionNameAndResponse.get( collectionName ); + if ( response == null ) + throw new SkipException( "Could not find a response for collection with name " + collectionName ); + + List testPointsForNamedCollection = OpenApiUtils.retrieveTestPoints( apiModel, COLLECTIONS, + collectionName + "/items" ); + if ( testPointsForNamedCollection.isEmpty() ) + throw new SkipException( "Could not find collection with name " + collectionName + + " in the OpenAPI document" ); + TestPoint testPoint = testPointsForNamedCollection.get( 0 ); + + JsonPath jsonPath = response.jsonPath(); + List> links = jsonPath.getList( "links" ); + + // Validate that the retrieved document includes links for: Itself + Map linkToSelf = findLinkByRel( links, "self" ); + assertNotNull( linkToSelf, "Feature Collection Metadata document must include a link for itself" ); + + // Validate that the retrieved document includes links for: Alternate encodings of this document in + // every other media type as identified by the compliance classes for this server. + List mediaTypesToSupport = createListOfMediaTypesToSupport( testPoint, linkToSelf ); + List> alternateLinks = findLinksWithSupportedMediaTypeByRel( links, mediaTypesToSupport, + "alternate" ); + List typesWithoutLink = findUnsupportedTypes( alternateLinks, mediaTypesToSupport ); + assertTrue( typesWithoutLink.isEmpty(), + "Feature Collection Metadata document must include links for alternate encodings. Missing links for types " + + typesWithoutLink ); + + // Validate that each link includes a rel and type parameter. + List linksWithoutRelOrType = findLinksWithoutRelOrType( links ); + assertTrue( linksWithoutRelOrType.isEmpty(), + "Links for alternate encodings must include a rel and type parameter. Missing for links " + + linksWithoutRelOrType ); + } + + /** + * A.4.4.10. Validate the Get Features Operation Response (Test method 4) + * + * a) Test Purpose: Validate the Get Feature Operation Response. + * + * b) Pre-conditions: A collection of Features has been retrieved + * + * c) Test Method: + * + * If a property timeStamp is included in the response, validate that it is close to the current time. + * + * d) References: Requirement 27 + * + * @param collection + * the collection under test, never null + */ + @Test(description = "Implements A.4.4.10. Validate the Get Features Operation Response (Requirement 27)", groups = "getFeaturesBase", dataProvider = "collectionItemUris", dependsOnMethods = "validateTheGetFeaturesOperation") + public void validateTheGetFeaturesOperationResponse_property_timeStamp( Map collection ) { + String collectionName = (String) collection.get( "name" ); + ResponseData response = collectionNameAndResponse.get( collectionName ); + if ( response == null ) + throw new SkipException( "Could not find a response for collection with name " + collectionName ); + + JsonPath jsonPath = response.jsonPath(); + + assertTimeStamp( collectionName, jsonPath, response.timeStampBeforeResponse, response.timeStampAfterResponse, + true ); + } + + /** + * A.4.4.10. Validate the Get Features Operation Response (Test method 5) + * + * a) Test Purpose: Validate the Get Feature Operation Response. + * + * b) Pre-conditions: A collection of Features has been retrieved + * + * c) Test Method: + * + * If a property numberReturned is included in the response, validate that the number is equal to the number of + * features in the response. + * + * d) References: Requirement 29 + * + * @param collection + * the collection under test, never null + */ + @Test(description = "Implements A.4.4.10. Validate the Get Features Operation Response (Requirement 29)", groups = "getFeaturesBase", dataProvider = "collectionItemUris", dependsOnMethods = "validateTheGetFeaturesOperation") + public void validateGetFeaturesOperationResponse_property_numberReturned( Map collection ) { + String collectionName = (String) collection.get( "name" ); + ResponseData response = collectionNameAndResponse.get( collectionName ); + if ( response == null ) + throw new SkipException( "Could not find a response for collection with name " + collectionName ); + + JsonPath jsonPath = response.jsonPath(); + + assertNumberReturned( collectionName, jsonPath, true ); + } + + /** + * A.4.4.10. Validate the Get Features Operation Response (Test method 6) + * + * a) Test Purpose: Validate the Get Feature Operation Response. + * + * b) Pre-conditions: A collection of Features has been retrieved + * + * c) Test Method: + * + * If a property numberMatched is included in the response, iteratively follow the next links until no next link is + * included and count the aggregated number of features returned in all responses during the iteration. Validate + * that the value is identical to the numberReturned stated in the initial response. + * + * d) References: Requirement 28 + * + * @param collection + * the collection under test, never null + * + * @throws URISyntaxException + * if the creation of a uri fails + */ + @Test(description = "Implements A.4.4.10. Validate the Get Features Operation Response (Requirement 28)", groups = "getFeaturesBase", dataProvider = "collectionItemUris", dependsOnMethods = "validateTheGetFeaturesOperation") + public void validateTheGetFeaturesOperationResponse_property_numberMatched( Map collection ) + throws URISyntaxException { + String collectionName = (String) collection.get( "name" ); + ResponseData response = collectionNameAndResponse.get( collectionName ); + if ( response == null ) + throw new SkipException( "Could not find a response for collection with name " + collectionName ); + + JsonPath jsonPath = response.jsonPath(); + + assertNumberMatched( collectionName, jsonPath, true ); + } + + /** + * A.4.4.11. Limit Parameter (Test method 1) + * + * a) Test Purpose: Validate the proper handling of the limit parameter. + * + * b) Pre-conditions: Tests A.4.4.9 and A.4.4.10 have completed successfully. + * + * c) Test Method: + * + * Verify that the OpenAPI document correctly describes the limit parameter for the Get Features operation. + * + * d) References: Requirement 18 + * + * Expected parameter: + * + *
+     * name: limit
+     * in: query
+     * required: false
+     * schema:
+     *   type: integer
+     *   minimum: 1
+     *   maximum: 10000 (example)
+     *   default: 10 (example)
+     * style: form
+     * explode: false
+     * 
+ * + * @param collection + * the collection under test, never null + * + */ + @Test(description = "Implements A.4.4.11. Limit Parameter (Requirement 18)", dataProvider = "collectionItemUris", dependsOnMethods = "validateTheGetFeaturesOperation") + public void limitParameter( Map collection ) { + String collectionName = (String) collection.get( "name" ); + Parameter limit = findParameterByName( collectionName, "limit" ); + + assertNotNull( limit, "Required limit parameter for collections item with name '" + collectionName + + "' in OpenAPI document is missing" ); + + String msg = "Expected property '%s' with value '%s' but was '%s'"; + + assertEquals( limit.getName(), "limit", String.format( msg, "name", "limit", limit.getName() ) ); + assertEquals( limit.getIn(), "query", String.format( msg, "in", "query", limit.getIn() ) ); + assertFalse( limit.getRequired(), String.format( msg, "required", "false", limit.getRequired() ) ); + assertEquals( limit.getStyle(), "form", String.format( msg, "style", "form", limit.getStyle() ) ); + assertFalse( limit.getExplode(), String.format( msg, "explode", "false", limit.getExplode() ) ); + + Schema schema = limit.getSchema(); + assertEquals( schema.getType(), "integer", String.format( msg, "schema -> type", "integer", schema.getType() ) ); + assertEquals( schema.getMinimum(), 1, String.format( msg, "schema -> minimum", "1", schema.getMinimum() ) ); + assertIntegerGreaterZero( schema.getMinimum(), "schema -> minimum" ); + assertIntegerGreaterZero( schema.getDefault(), "schema -> default" ); + } + + /** + * A.4.4.11. Limit Parameter (Test method 2, 3) + * + * a) Test Purpose: Validate the proper handling of the limit parameter. + * + * b) Pre-conditions: Tests A.4.4.9 and A.4.4.10 have completed successfully. + * + * c) Test Method: + * + * Repeat Test A.4.4.9 using different values for the limit parameter. + * + * For each execution of Test A.4.4.9, repeat Test A.4.4.10 to validate the results. + * + * d) References: Requirement 19 + * + * @param collection + * the collection under test, never null + * @param limit + * limit parameter to request, never null + * @throws URISyntaxException + * if the creation of a uri fails + */ + @Test(description = "Implements A.4.4.11. Limit Parameter (Requirement 19)", dataProvider = "collectionItemUrisWithLimit", dependsOnMethods = "validateTheGetFeaturesOperation") + public void limitParameter_requests( Map collection, int limit ) + throws URISyntaxException { + String collectionName = (String) collection.get( "name" ); + + String getFeaturesUrl = findGetFeatureUrlForGeoJson( collection ); + if ( getFeaturesUrl.isEmpty() ) + throw new SkipException( "Could not find url for collection with name " + collectionName + + " supporting GeoJson (type " + GEOJSON_MIME_TYPE + ")" ); + ZonedDateTime timeStampBeforeResponse = ZonedDateTime.now(); + Response response = init().baseUri( getFeaturesUrl ).accept( GEOJSON_MIME_TYPE ).param( "limit", limit ).when().request( GET ); + response.then().statusCode( 200 ); + ZonedDateTime timeStampAfterResponse = ZonedDateTime.now(); + + JsonPath jsonPath = response.jsonPath(); + int numberOfFeatures = jsonPath.getList( "features" ).size(); + assertTrue( numberOfFeatures <= limit, "Number of features for collection with name " + collectionName + + " is unexpected (was " + numberOfFeatures + "), expected are " + limit + + " or less" ); + assertTimeStamp( collectionName, jsonPath, timeStampBeforeResponse, timeStampAfterResponse, false ); + assertNumberReturned( collectionName, jsonPath, false ); + assertNumberMatched( collectionName, jsonPath, false ); + } + + /** + * A.4.4.12. Bounding Box Parameter (Test method 1) + * + * a) Test Purpose:Validate the proper handling of the bbox parameter. + * + * b) Pre-conditions: Tests A.4.4.9 and A.4.4.10 have completed successfully. + * + * c) Test Method: + * + * Verify that the OpenAPI document correctly describes the bbox parameter for the Get Features operation. + * + * d) References: Requirement 20 + * + * Expected parameter: + * + *
+     * name: bbox
+     * in: query
+     * required: false
+     * schema:
+     *   type: array
+     *   minItems: 4
+     *   maxItems: 6
+     *   items:
+     *     type: number
+     * style: form
+     * explode: false
+     * 
+ * + * @param collection + * the collection under test, never null + */ + @Test(description = "Implements A.4.4.12. Bounding Box Parameter (Requirement 20)", dataProvider = "collectionItemUris", dependsOnMethods = "validateTheGetFeaturesOperation") + public void boundingBoxParameter( Map collection ) { + String collectionName = (String) collection.get( "name" ); + Parameter bbox = findParameterByName( collectionName, "bbox" ); + + assertNotNull( bbox, "Required bbox parameter for collections item with name '" + collectionName + + "' in OpenAPI document is missing" ); + + String msg = "Expected property '%s' with value '%s' for collections item with name '" + collectionName + "' but was '%s'."; + + assertEquals( bbox.getName(), "bbox", String.format( msg, "name", "bbox", bbox.getName() ) ); + assertEquals( bbox.getIn(), "query", String.format( msg, "in", "query", bbox.getIn() ) ); + assertFalse( bbox.getRequired(), String.format( msg, "required", "false", bbox.getRequired() ) ); + assertEquals( bbox.getStyle(), "form", String.format( msg, "style", "form", bbox.getStyle() ) ); + assertFalse( bbox.getExplode(), String.format( msg, "explode", "false", bbox.getExplode() ) ); + + Schema schema = bbox.getSchema(); + assertEquals( schema.getType(), "array", String.format( msg, "schema -> type", "array", schema.getType() ) ); + assertEquals( schema.getMinItems().intValue(), 4, + String.format( msg, "schema -> minItems", "4", schema.getMinItems() ) ); + assertEquals( schema.getMaxItems().intValue(), 6, + String.format( msg, "schema -> maxItems", "6", schema.getMaxItems() ) ); + + String itemsType = schema.getItemsSchema().getType(); + assertEquals( itemsType, "number", String.format( msg, "schema -> items -> type", "number", itemsType ) ); + } + + /** + * A.4.4.12. Bounding Box Parameter (Test method 1) + * + * a) Test Purpose:Validate the proper handling of the bbox parameter. + * + * b) Pre-conditions: Tests A.4.4.9 and A.4.4.10 have completed successfully. + * + * c) Test Method: + * + * Repeat Test A.4.4.9 using different values for the limit parameter. + * + * For each execution of Test A.4.4.9, repeat Test A.4.4.10 to validate the results. + * + * d) References: Requirement 21 + * + * @param collection + * the collection under test, never null + * @param bbox + * bbox parameter to request, never null + * @throws URISyntaxException + * if the creation of a uri fails + */ + @Test(description = "Implements A.4.4.12. Bounding Box Parameter (Requirement 21)", dataProvider = "collectionItemUrisWithBboxes", dependsOnMethods = "validateTheGetFeaturesOperation") + public void boundingBoxParameter_requests( Map collection, BBox bbox ) + throws URISyntaxException { + String collectionName = (String) collection.get( "name" ); + + String getFeaturesUrl = findGetFeatureUrlForGeoJson( collection ); + if ( getFeaturesUrl.isEmpty() ) + throw new SkipException( "Could not find url for collection with name " + collectionName + + " supporting GeoJson (type " + GEOJSON_MIME_TYPE + ")" ); + ZonedDateTime timeStampBeforeResponse = ZonedDateTime.now(); + Response response = init().baseUri( getFeaturesUrl ).accept( GEOJSON_MIME_TYPE ).param( "bbox", + bbox.asQueryParameter() ).when().request( GET ); + response.then().statusCode( 200 ); + ZonedDateTime timeStampAfterResponse = ZonedDateTime.now(); + + JsonPath jsonPath = response.jsonPath(); + assertTimeStamp( collectionName, jsonPath, timeStampBeforeResponse, timeStampAfterResponse, false ); + assertNumberReturned( collectionName, jsonPath, false ); + assertNumberMatched( collectionName, jsonPath, false ); + + // TODO: assert returned features + } + + /** + * A.4.4.13. Time Parameter (Test method 1) + * + * a) Test Purpose: Validate the proper handling of the time parameter. + * + * b) Pre-conditions: Tests A.4.4.9 and A.4.4.10 have completed successfully. + * + * c) Test Method: + * + * Verify that the OpenAPI document correctly describes the time parameter for the Get Features operation. + * + * d) References: Requirement 22 + * + * Expected parameter: + * + *
+     * name: time
+     * in: query
+     * required: false
+     * schema:
+     *   type: string
+     * style: form
+     * explode: false
+     * 
+ * + * @param collection + * the collection under test, never null + */ + @Test(description = "Implements A.4.4.13. Time Parameter (Requirement 22)", dataProvider = "collectionItemUris", dependsOnMethods = "validateTheGetFeaturesOperation") + public void timeParameter( Map collection ) { + String collectionName = (String) collection.get( "name" ); + Parameter time = findParameterByName( collectionName, "time" ); + + assertNotNull( time, "Required time parameter for collections item with name '" + collectionName + + "' in OpenAPI document is missing" ); + + String msg = "Expected property '%s' with value '%s' but was '%s'"; + + assertEquals( time.getName(), "time", String.format( msg, "name", "time", time.getName() ) ); + assertEquals( time.getIn(), "query", String.format( msg, "in", "query", time.getIn() ) ); + assertFalse( time.getRequired(), String.format( msg, "required", "false", time.getRequired() ) ); + assertEquals( time.getStyle(), "form", String.format( msg, "style", "form", time.getStyle() ) ); + assertFalse( time.getExplode(), String.format( msg, "explode", "false", time.getExplode() ) ); + + Schema schema = time.getSchema(); + assertEquals( schema.getType(), "string", String.format( msg, "schema -> type", "string", schema.getType() ) ); + } + + /** + * A.4.4.13. Time Parameter (Test method 2, 3) + * + * a) Test Purpose:Validate the proper handling of the bbox parameter. + * + * b) Pre-conditions: Tests A.4.4.9 and A.4.4.10 have completed successfully. + * + * c) Test Method: + * + * Repeat Test A.4.4.9 using different values for the limit parameter. + * + * For each execution of Test A.4.4.9, repeat Test A.4.4.10 to validate the results. + * + * d) References: Requirement 23 + * + * @param collection + * the collection under test, never null + * @param queryParameter + * time parameter as string to use as query parameter, never null + * @param begin + * a {@link ZonedDateTime} or {@link LocalDate}, the begin of the interval (or instant), never + * null + * @param end + * a {@link ZonedDateTime} or {@link LocalDate}, the end of the interval, never null if the + * request is an instant + * @throws URISyntaxException + * if the creation of a uri fails + * + */ + @Test(description = "Implements A.4.4.13. Time Parameter (Requirement 23)", dataProvider = "collectionItemUrisWithTimes", dependsOnMethods = "validateTheGetFeaturesOperation") + public void timeParameter_requests( Map collection, String queryParameter, Object begin, + Object end ) + throws URISyntaxException { + String collectionName = (String) collection.get( "name" ); + + String getFeaturesUrl = findGetFeatureUrlForGeoJson( collection ); + if ( getFeaturesUrl.isEmpty() ) + throw new SkipException( "Could not find url for collection with name " + collectionName + + " supporting GeoJson (type " + GEOJSON_MIME_TYPE + ")" ); + ZonedDateTime timeStampBeforeResponse = ZonedDateTime.now(); + Response response = init().baseUri( getFeaturesUrl ).accept( GEOJSON_MIME_TYPE ).param( "time", queryParameter ).when().request( GET ); + response.then().statusCode( 200 ); + ZonedDateTime timeStampAfterResponse = ZonedDateTime.now(); + + JsonPath jsonPath = response.jsonPath(); + assertTimeStamp( collectionName, jsonPath, timeStampBeforeResponse, timeStampAfterResponse, false ); + assertNumberReturned( collectionName, jsonPath, false ); + assertNumberMatched( collectionName, jsonPath, false ); + + // TODO: assert returned features + } + + private void assertTimeStamp( String collectionName, JsonPath jsonPath, ZonedDateTime timeStampBeforeResponse, + ZonedDateTime timeStampAfterResponse, boolean skipIfNoTimeStamp ) { + String timeStamp = jsonPath.getString( "timeStamp" ); + if ( timeStamp == null ) + if ( skipIfNoTimeStamp ) + throw new SkipException( "Property timeStamp is not set in collection items '" + collectionName + "'" ); + else + return; + + ZonedDateTime date = parseAsDate( timeStamp ); + assertTrue( date.isBefore( timeStampAfterResponse ), + "timeStamp in response must be before the request was send (" + formatDate( timeStampAfterResponse ) + + ") but was '" + timeStamp + "'" ); + assertTrue( date.isAfter( timeStampBeforeResponse ), + "timeStamp in response must be after the request was send (" + formatDate( timeStampBeforeResponse ) + + ") but was '" + timeStamp + "'" ); + } + + private void assertNumberReturned( String collectionName, JsonPath jsonPath, boolean skipIfNoNumberReturned ) { + if ( !hasProperty( "numberReturned", jsonPath ) ) + if ( skipIfNoNumberReturned ) + throw new SkipException( "Property numberReturned is not set in collection items '" + collectionName + + "'" ); + else + return; + + int numberReturned = jsonPath.getInt( "numberReturned" ); + int numberOfFeatures = jsonPath.getList( "features" ).size(); + assertEquals( numberReturned, numberOfFeatures, "Value of numberReturned (" + numberReturned + + ") does not match the number of features in the response (" + + numberOfFeatures + ")" ); + } + + private void assertNumberMatched( String collectionName, JsonPath jsonPath, boolean skipIfNoNumberMatched ) + throws URISyntaxException { + if ( !hasProperty( "numberMatched", jsonPath ) ) + if ( skipIfNoNumberMatched ) + throw new SkipException( "Property numberMatched is not set in collection items '" + collectionName + + "'" ); + else + return; + + int numberMatched = jsonPath.getInt( "numberMatched" ); + int numberOfAllReturnedFeatures = collectNumberOfAllReturnedFeatures( jsonPath ); + assertEquals( numberMatched, numberOfAllReturnedFeatures, + "Value of numberReturned (" + numberMatched + + ") does not match the number of features in all responses (" + + numberOfAllReturnedFeatures + ")" ); + } + + private Parameter findParameterByName( String collectionName, String name ) { + String collectionItemPath = "/" + COLLECTIONS.getPathItem() + "/" + collectionName + "/items"; + Path path = apiModel.getPath( collectionItemPath ); + if ( path != null ) { + for ( Parameter parameter : path.getParameters() ) + if ( name.equals( parameter.getName() ) ) + return parameter; + Operation get = path.getOperation( "get" ); + for ( Parameter parameter : get.getParameters() ) + if ( name.equals( parameter.getName() ) ) + return parameter; + } + return null; + } + + private String findGetFeatureUrlForGeoJson( Map collection ) { + List links = (List) collection.get( "links" ); + for ( Object linkObject : links ) { + Map link = (Map) linkObject; + Object rel = link.get( "rel" ); + Object type = link.get( "type" ); + if ( "item".equals( rel ) && GEOJSON_MIME_TYPE.equals( type ) ) + return (String) link.get( "href" ); + } + return null; + } + + private List createListOfMediaTypesToSupport( TestPoint testPoint, Map linkToSelf ) { + Map contentMediaTypes = testPoint.getContentMediaTypes(); + List mediaTypesToSupport = new ArrayList<>(); + mediaTypesToSupport.addAll( contentMediaTypes.keySet() ); + if ( linkToSelf != null ) + mediaTypesToSupport.remove( linkToSelf.get( "type" ) ); + return mediaTypesToSupport; + } + + private void assertIntegerGreaterZero( Object value, String propertyName ) { + if ( value instanceof Number ) + assertIntegerGreaterZero( ( (Number) value ).intValue(), propertyName ); + else if ( value instanceof String ) + try { + int valueAsInt = Integer.parseInt( (String) value ); + assertIntegerGreaterZero( valueAsInt, propertyName ); + } catch ( NumberFormatException e ) { + String msg = "Expected property '%s' to be an integer, but was '%s'"; + throw new AssertionError( String.format( msg, propertyName, value ) ); + } + } + + private void assertIntegerGreaterZero( int value, String propertyName ) { + String msg = "Expected property '%s' to be an integer greater than 0, but was '%s'"; + assertTrue( value > 0, String.format( msg, propertyName, value ) ); + } + + private int[] getTwoRandomLimits( Parameter limit ) { + Schema schema = limit.getSchema(); + int min = schema.getMinimum().intValue(); + int max = schema.getMaximum().intValue(); + if ( max > 25 ) + max = 25; + Random random = new Random(); + return random.ints( 2, min, max ).toArray(); + } + + private class ResponseData { + + private final Response response; + + private final ZonedDateTime timeStampBeforeResponse; + + private final ZonedDateTime timeStampAfterResponse; + + public ResponseData( Response response, ZonedDateTime timeStampBeforeResponse, + ZonedDateTime timeStampAfterResponse ) { + this.response = response; + this.timeStampBeforeResponse = timeStampBeforeResponse; + this.timeStampAfterResponse = timeStampAfterResponse; + } + + public JsonPath jsonPath() { + return response.jsonPath(); + } + } + +} diff --git a/src/main/java/org/opengis/cite/wfs30/util/BBox.java b/src/main/java/org/opengis/cite/wfs30/util/BBox.java new file mode 100644 index 00000000..ed6e5167 --- /dev/null +++ b/src/main/java/org/opengis/cite/wfs30/util/BBox.java @@ -0,0 +1,64 @@ +package org.opengis.cite.wfs30.util; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +/** + * @author Lyn Goltz + */ +public class BBox { + + private static final String PATTERN = "###.0000000"; + + private final double minX; + + private final double minY; + + private final double maxX; + + private final double maxY; + + /** + * @param minX + * Lower left corner, coordinate axis 1 + * @param minY + * Lower left corner, coordinate axis 2 + * @param maxX + * Upper right corner, coordinate axis 1 + * @param maxY + * Upper right corner, coordinate axis 2 + */ + public BBox( double minX, double minY, double maxX, double maxY ) { + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + /** + * @return the bbox as query string like '-12,10, 12,20' + */ + public String asQueryParameter() { + StringBuilder sb = new StringBuilder(); + DecimalFormat formatter = formatter(); + sb.append( formatter.format( minX ) ).append( "," ); + sb.append( formatter.format( minY ) ).append( "," ); + sb.append( formatter.format( maxX ) ).append( "," ); + sb.append( formatter.format( maxY ) ); + return sb.toString(); + } + + @Override + public String toString() { + return asQueryParameter(); + } + + private DecimalFormat formatter() { + NumberFormat nf = NumberFormat.getNumberInstance( Locale.ENGLISH ); + DecimalFormat df = (DecimalFormat) nf; + df.applyPattern( PATTERN ); + return df; + } + +} \ No newline at end of file diff --git a/src/main/java/org/opengis/cite/wfs30/util/JsonUtils.java b/src/main/java/org/opengis/cite/wfs30/util/JsonUtils.java new file mode 100644 index 00000000..debf9e9b --- /dev/null +++ b/src/main/java/org/opengis/cite/wfs30/util/JsonUtils.java @@ -0,0 +1,313 @@ +package org.opengis.cite.wfs30.util; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.Method.GET; +import static org.opengis.cite.wfs30.WFS3.GEOJSON_MIME_TYPE; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.LocalDate; +import java.time.Period; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.restassured.path.json.JsonPath; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; + +/** + * @author Lyn Goltz + */ +public class JsonUtils { + + private JsonUtils() { + } + + /** + * Parses the temporal extent from the passed collection. + * + * @param collection + * the collection containing the extent to parse, never null + * @return the parsed temporal extent, null if no extent exists + * @throws IllegalArgumentException + * if the number of items in the extent invalid + * + */ + public static TemporalExtent parseTemporalExtent( Map collection ) { + Object extent = collection.get( "extent" ); + if ( extent == null || !( extent instanceof Map ) ) + return null; + Object spatial = ( (Map) extent ).get( "temporal" ); + if ( spatial == null || !( spatial instanceof List ) ) + return null; + List coords = (List) spatial; + if ( coords.size() == 2 ) { + ZonedDateTime begin = parseAsDate( (String) coords.get( 0 ) ); + ZonedDateTime end = parseAsDate( (String) coords.get( 1 ) ); + return new TemporalExtent( begin, end ); + } + throw new IllegalArgumentException( "Temporal extent with " + coords.size() + " items is invalid" ); + } + + /** + * Parses the passed string as ISO 8601 date. + * + * @param dateTime + * the dateTime to parse, never null + * @return the parsed date, never null + */ + public static ZonedDateTime parseAsDate( String dateTime ) { + return ZonedDateTime.parse( dateTime ); + } + + /** + * Formats the passed string as ISO 8601 date. Example: "2018-02-12T23:20:50Z" + * + * @param dateTime + * the dateTime to format, never null + * @return the formatted date, never null + */ + public static String formatDate( ZonedDateTime dateTime ) { + return DateTimeFormatter.ISO_INSTANT.format( dateTime ); + } + + /** + * Formats the passed string as ISO 8601 date. Example: "2018-02-12" + * + * @param date + * the dateTime to format, never null + * @return the formatted date, never null + */ + public static String formatDate( LocalDate date ) { + return DateTimeFormatter.ISO_DATE.format( date ); + } + + /** + * Formats the passed string as a period using a start and end time. Example: + * "2018-02-12T00:00:00Z/2018-03-18T12:31:12Z" + * + * @param beginDateTime + * the begin dateTime to format, never null + * @param endDateTime + * the end dateTime to format, never null + * @return the formatted date, never null + */ + public static String formatDateRange( ZonedDateTime beginDateTime, ZonedDateTime endDateTime ) { + return formatDate( beginDateTime ) + "/" + formatDate( endDateTime ); + } + + /** + * Formats the passed string as a period using start time and a duration. Example: + * "2018-02-12T00:00:00Z/P1M6DT12H31M12S" + * + * @param beginDate + * the begin date to format, never null + * @param endDate + * the end date to format, never null + * @return the formatted date, never null + */ + public static String formatDateRangeWithDuration( LocalDate beginDate, LocalDate endDate ) { + Period betweenDate = Period.between( beginDate, endDate ); + return formatDate( beginDate ) + "/" + betweenDate; + } + + /** + * Parses the spatial extent from the passed collection. + * + * @param collection + * the collection containing the extent to parse, never null + * @return the parsed bbox, null if no extent exists + * @throws IllegalArgumentException + * if the number of items in the extent invalid + * + */ + public static BBox parseSpatialExtent( Map collection ) { + Object extent = collection.get( "extent" ); + if ( extent == null || !( extent instanceof Map ) ) + return null; + Object spatial = ( (Map) extent ).get( "spatial" ); + if ( spatial == null || !( spatial instanceof List ) ) + return null; + List coords = (List) spatial; + if ( coords.size() == 4 ) { + double minX = (Float) coords.get( 0 ); + double minY = (Float) coords.get( 1 ); + double maxX = (Float) coords.get( 2 ); + double maxY = (Float) coords.get( 3 ); + return new BBox( minX, minY, maxX, maxY ); + } else if ( coords.size() == 6 ) { + throw new IllegalArgumentException( "BBox with " + coords.size() + + " coordinates is currently not supported" ); + } + throw new IllegalArgumentException( "BBox with " + coords.size() + " coordinates is invalid" ); + } + + /** + * Parses all links with 'type' of one of the passed mediaTypes and the 'rel' property with the passed value. + * + * @param links + * list of all links, never null + * @param mediaTypesToSupport + * a list of media types the links searched for should support, may be empty but never null + * @param expectedRel + * the expected value of the property 'rel', never null + * @return a list of links supporting one of the media types and with the expected 'rel' property, may be empty but + * never null + */ + public static List> findLinksWithSupportedMediaTypeByRel( List> links, + List mediaTypesToSupport, + String expectedRel ) { + List> alternateLinks = new ArrayList<>(); + for ( Map link : links ) { + Object type = link.get( "type" ); + Object rel = link.get( "rel" ); + if ( expectedRel.equals( rel ) && isSupportedMediaType( type, mediaTypesToSupport ) ) + alternateLinks.add( link ); + } + return alternateLinks; + } + + /** + * Parsing the media types which does not have a link woth property 'type' for. + * + * @param links + * list of links to search in, never null + * @param mediaTypesToSuppport + * a list of media types which should be supported, never null + * @return the media types which does not have a link for. + */ + public static List findUnsupportedTypes( List> links, List mediaTypesToSuppport ) { + List unsupportedType = new ArrayList<>(); + for ( String contentMediaType : mediaTypesToSuppport ) { + boolean hasLinkForContentType = hasLinkForContentType( links, contentMediaType ); + if ( !hasLinkForContentType ) + unsupportedType.add( contentMediaType ); + } + return unsupportedType; + } + + /** + * Parses the links without 'rel' or 'type' property. + * + * @param links + * list of links to search in, never null + * @return the links without 'rel' or 'type' property + */ + public static List findLinksWithoutRelOrType( List> links ) { + List linksWithoutRelOrType = new ArrayList<>(); + for ( Map alternateLink : links ) { + if ( !linkIncludesRelAndType( alternateLink ) ) + linksWithoutRelOrType.add( (String) alternateLink.get( "href" ) ); + } + return linksWithoutRelOrType; + } + + /** + * Parses the link with 'rel=self'. + * + * @param links + * list of links to search in, never null + * @param expectedRel + * the expected value of the property 'rel', never null + * @return the link to itself or null if no such link exists + */ + public static Map findLinkByRel( List> links, String expectedRel ) { + for ( Map link : links ) { + Object rel = link.get( "rel" ); + if ( expectedRel.equals( rel ) ) + return link; + } + return null; + } + + /** + * Checks if the passed link contains 'rel' and 'type' properties. + * + * @param link + * to check, never null + * @return true if the link contains 'rel' and 'type' properties, false otherwise + */ + public static boolean linkIncludesRelAndType( Map link ) { + Object rel = link.get( "rel" ); + Object type = link.get( "type" ); + if ( rel != null && type != null ) + return true; + return false; + } + + /** + * Checks if a property with the passed name exists in the jsonPath. + * + * @param propertyName + * name of the property to check, never null + * @param jsonPath + * to check, never null + * @return true if the property exists, false otherwise + */ + public static boolean hasProperty( String propertyName, JsonPath jsonPath ) { + return jsonPath.get( propertyName ) != null; + } + + /** + * Collects the number of all returned features by iterating over all 'next' links and summarizing the size of + * features in 'features' array property. + * + * @param jsonPath + * the initial collection, never null + * @return the number of all returned features + * @throws URISyntaxException + * if the creation of a uri fails + */ + public static int collectNumberOfAllReturnedFeatures( JsonPath jsonPath ) + throws URISyntaxException { + int numberOfAllReturnedFeatures = jsonPath.getList( "features" ).size(); + Map nextLink = findLinkByRel( jsonPath.getList( "links" ), "next" ); + while ( nextLink != null ) { + String nextUrl = (String) nextLink.get( "href" ); + URI uri = new URI( nextUrl ); + + RequestSpecification accept = given().log().all().baseUri( nextUrl ).accept( GEOJSON_MIME_TYPE ); + String[] pairs = uri.getQuery().split( "&" ); + for ( String pair : pairs ) { + int idx = pair.indexOf( "=" ); + String key = pair.substring( 0, idx ); + String value = pair.substring( idx + 1 ); + accept.param( key, value ); + } + + Response response = accept.when().request( GET ); + response.then().statusCode( 200 ); + + JsonPath nextJsonPath = response.jsonPath(); + int features = nextJsonPath.getList( "features" ).size(); + if ( features > 0 ) { + numberOfAllReturnedFeatures += features; + nextLink = findLinkByRel( nextJsonPath.getList( "links" ), "next" ); + } else { + nextLink = null; + } + } + return numberOfAllReturnedFeatures; + } + + private static boolean hasLinkForContentType( List> alternateLinks, String mediaType ) { + for ( Map alternateLink : alternateLinks ) { + Object type = alternateLink.get( "type" ); + if ( mediaType.equals( type ) ) + return true; + } + return false; + } + + private static boolean isSupportedMediaType( Object type, List contentMediaTypes ) { + for ( String contentMediaType : contentMediaTypes ) { + if ( contentMediaType.equals( type ) ) + return true; + } + return false; + } + +} diff --git a/src/main/java/org/opengis/cite/wfs30/util/TemporalExtent.java b/src/main/java/org/opengis/cite/wfs30/util/TemporalExtent.java new file mode 100644 index 00000000..9cc20234 --- /dev/null +++ b/src/main/java/org/opengis/cite/wfs30/util/TemporalExtent.java @@ -0,0 +1,27 @@ +package org.opengis.cite.wfs30.util; + +import java.time.ZonedDateTime; + +/** + * @author Lyn Goltz + */ +public class TemporalExtent { + + private ZonedDateTime begin; + + private ZonedDateTime end; + + public TemporalExtent( ZonedDateTime begin, ZonedDateTime end ) { + this.begin = begin; + this.end = end; + } + + public ZonedDateTime getBegin() { + return begin; + } + + public ZonedDateTime getEnd() { + return end; + } + +} \ No newline at end of file diff --git a/src/test/java/org/opengis/cite/wfs30/collections/GetFeaturesOperationIT.java b/src/test/java/org/opengis/cite/wfs30/collections/GetFeaturesOperationIT.java new file mode 100644 index 00000000..c00212f3 --- /dev/null +++ b/src/test/java/org/opengis/cite/wfs30/collections/GetFeaturesOperationIT.java @@ -0,0 +1,109 @@ +package org.opengis.cite.wfs30.collections; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.opengis.cite.wfs30.SuiteAttribute; +import org.opengis.cite.wfs30.util.BBox; +import org.testng.ISuite; +import org.testng.ITestContext; + +import com.reprezen.kaizen.oasparser.OpenApi3Parser; +import com.reprezen.kaizen.oasparser.model3.OpenApi3; + +import io.restassured.path.json.JsonPath; + +/** + * @author Lyn Goltz + */ +public class GetFeaturesOperationIT { + + private static ITestContext testContext; + + private static ISuite suite; + + @BeforeClass + public static void initTestFixture() + throws Exception { + OpenApi3Parser parser = new OpenApi3Parser(); + URL openAppiDocument = FeatureCollectionsMetadataOperationIT.class.getResource( "../openapi3/openapi.json" ); + OpenApi3 apiModel = parser.parse( openAppiDocument, true ); + + InputStream json = GetFeaturesOperationIT.class.getResourceAsStream( "../collections/collections.json" ); + JsonPath collectionsResponse = new JsonPath( json ); + List> collections = collectionsResponse.getList( "collections" ); + + testContext = mock( ITestContext.class ); + suite = mock( ISuite.class ); + when( testContext.getSuite() ).thenReturn( suite ); + + URI landingPageUri = new URI( "https://www.ldproxy.nrw.de/kataster" ); + when( suite.getAttribute( SuiteAttribute.IUT.getName() ) ).thenReturn( landingPageUri ); + when( suite.getAttribute( SuiteAttribute.API_MODEL.getName() ) ).thenReturn( apiModel ); + when( suite.getAttribute( SuiteAttribute.COLLECTIONS.getName() ) ).thenReturn( collections ); + } + + @Test + public void testGetFeatureOperations() + throws URISyntaxException { + GetFeaturesOperation getFeaturesOperation = new GetFeaturesOperation(); + getFeaturesOperation.initCommonFixture( testContext ); + getFeaturesOperation.retrieveRequiredInformationFromTestContext( testContext ); + + Iterator collections = getFeaturesOperation.collectionItemUris( testContext ); + for ( Iterator it = collections; it.hasNext(); ) { + Object[] collection = it.next(); + Map parameter = (Map) collection[0]; + getFeaturesOperation.validateTheGetFeaturesOperation( parameter ); + getFeaturesOperation.validateTheGetFeaturesOperationResponse_Links( parameter ); + // skipped (parameter missing): + // getFeaturesOperation.validateTheGetFeaturesOperationResponse_property_timeStamp( parameter ); + // skipped (parameter missing): + // getFeaturesOperation.validateGetFeaturesOperationResponse_property_numberReturned( parameter ); + // skipped (parameter missing): + // getFeaturesOperation.validateTheGetFeaturesOperationResponse_property_numberMatched( parameter ); + getFeaturesOperation.limitParameter( parameter ); + // fails (schema->items->type missing): getFeaturesOperation.boundingBoxParameter(); + // fails (parameter is missing): getFeaturesOperation.timeParameter( parameter ); + } + + Iterator collectionsWithLimits = getFeaturesOperation.collectionItemUrisWithLimits( testContext ); + for ( Iterator it = collectionsWithLimits; it.hasNext(); ) { + Object[] collection = it.next(); + Map parameter = (Map) collection[0]; + int limit = (int) collection[1]; + // skipped (parameter missing): + getFeaturesOperation.limitParameter_requests( parameter, limit ); + } + + Iterator collectionsWithBboxes = getFeaturesOperation.collectionItemUrisWithBboxes( testContext ); + for ( Iterator collectionWithBbox = collectionsWithBboxes; collectionWithBbox.hasNext(); ) { + Object[] collection = collectionWithBbox.next(); + Map parameter = (Map) collection[0]; + BBox bbox = (BBox) collection[1]; + // fails: in collections.json must the links (rel: item, type: application/geo+json) changed to https + // getFeaturesOperation.boundingBoxParameter_requests( parameter, bbox ); + } + + Iterator collectionsWithTimes = getFeaturesOperation.collectionItemUrisWithTimes( testContext ); + for ( Iterator it = collectionsWithTimes; it.hasNext(); ) { + Object[] collection = it.next(); + Map parameter = (Map) collection[0]; + String queryParam = (String) collection[1]; + Object begin = collection[2]; + Object end = collection[3]; + getFeaturesOperation.timeParameter_requests( parameter, queryParam, begin, end ); + } + } + +} diff --git a/src/test/java/org/opengis/cite/wfs30/util/JsonUtilsIT.java b/src/test/java/org/opengis/cite/wfs30/util/JsonUtilsIT.java new file mode 100644 index 00000000..95e8a112 --- /dev/null +++ b/src/test/java/org/opengis/cite/wfs30/util/JsonUtilsIT.java @@ -0,0 +1,29 @@ +package org.opengis.cite.wfs30.util; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.opengis.cite.wfs30.util.JsonUtils.collectNumberOfAllReturnedFeatures; + +import java.net.URL; + +import org.junit.Test; + +import io.restassured.path.json.JsonPath; + +/** + * @author Lyn Goltz + */ +public class JsonUtilsIT { + + @Test + public void testCollectNumberOfAllReturnedFeatures() + throws Exception { + URL json = new URL( "http://geo.kralidis.ca/pygeoapi/collections/lakes/items" ); + JsonPath jsonPath = new JsonPath( json ); + + int numberOfAllFeatures = collectNumberOfAllReturnedFeatures( jsonPath ); + + assertThat( numberOfAllFeatures, is( 25 ) ); + } + +} diff --git a/src/test/java/org/opengis/cite/wfs30/util/JsonUtilsTest.java b/src/test/java/org/opengis/cite/wfs30/util/JsonUtilsTest.java new file mode 100644 index 00000000..a8fea6a9 --- /dev/null +++ b/src/test/java/org/opengis/cite/wfs30/util/JsonUtilsTest.java @@ -0,0 +1,167 @@ +package org.opengis.cite.wfs30.util; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.opengis.cite.wfs30.util.JsonUtils.findLinkByRel; +import static org.opengis.cite.wfs30.util.JsonUtils.findLinksWithSupportedMediaTypeByRel; +import static org.opengis.cite.wfs30.util.JsonUtils.findLinksWithoutRelOrType; +import static org.opengis.cite.wfs30.util.JsonUtils.formatDate; +import static org.opengis.cite.wfs30.util.JsonUtils.formatDateRange; +import static org.opengis.cite.wfs30.util.JsonUtils.formatDateRangeWithDuration; +import static org.opengis.cite.wfs30.util.JsonUtils.hasProperty; +import static org.opengis.cite.wfs30.util.JsonUtils.linkIncludesRelAndType; +import static org.opengis.cite.wfs30.util.JsonUtils.parseAsDate; +import static org.opengis.cite.wfs30.util.JsonUtils.parseSpatialExtent; +import static org.opengis.cite.wfs30.util.JsonUtils.parseTemporalExtent; + +import java.io.InputStream; +import java.time.LocalDate; +import java.time.Month; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; + +import io.restassured.path.json.JsonPath; + +/** + * @author Lyn Goltz + */ +public class JsonUtilsTest { + + private static JsonPath jsonPath; + + @BeforeClass + public static void parseJson() { + InputStream json = JsonUtilsTest.class.getResourceAsStream( "../collections/collections.json" ); + jsonPath = new JsonPath( json ); + } + + @Test + public void testParseAsDate() { + String timeStamp = "2017-03-04T01:02:33Z"; + ZonedDateTime dateTime = parseAsDate( timeStamp ); + + assertThat( dateTime.getYear(), is( 2017 ) ); + assertThat( dateTime.getMonth(), is( Month.MARCH ) ); + assertThat( dateTime.getDayOfMonth(), is( 4 ) ); + assertThat( dateTime.getHour(), is( 1 ) ); + assertThat( dateTime.getMinute(), is( 2 ) ); + assertThat( dateTime.getSecond(), is( 33 ) ); + } + + @Test + public void testFormatDate() { + String dateTime = "2017-03-04T01:02:33Z"; + ZonedDateTime dateTimeAsZonedDateTime = parseAsDate( dateTime ); + + String dateTimeAsString = formatDate( dateTimeAsZonedDateTime ); + assertThat( dateTimeAsString, is( dateTime ) ); + + String dateAsString = formatDate( dateTimeAsZonedDateTime.toLocalDate() ); + assertThat( dateAsString, is( "2017-03-04" ) ); + } + + @Test + public void testFormatDateRange() { + String beginDateTime = "2017-03-04T01:02:33Z"; + ZonedDateTime begin = parseAsDate( beginDateTime ); + String endDateTime = "2018-03-04T01:02:33Z"; + ZonedDateTime end = parseAsDate( endDateTime ); + + String asString = formatDateRange( begin, end ); + assertThat( asString, is( "2017-03-04T01:02:33Z/2018-03-04T01:02:33Z" ) ); + } + + @Test + public void testFormatDateRangeWithDuration() { + String beginDate = "2017-03-04"; + LocalDate begin = LocalDate.parse( beginDate ); + String endDate = "2017-04-06"; + LocalDate end = LocalDate.parse( endDate ); + + String asString = formatDateRangeWithDuration( begin, end ); + assertThat( asString, is( "2017-03-04/P1M2D" ) ); + } + + @Test + public void testTemporalExtent() { + List collections = jsonPath.getList( "collections" ); + TemporalExtent extent = parseTemporalExtent( (Map) collections.get( 0 ) ); + + ZonedDateTime begin = extent.getBegin(); + ZonedDateTime end = extent.getEnd(); + assertThat( begin, is( ZonedDateTime.parse( "2017-01-01T00:00:00Z" ) ) ); + assertThat( end, is( ZonedDateTime.parse( "2017-12-31T23:59:59Z" ) ) ); + } + + @Test + public void testParseSpatialExtent() { + List collections = jsonPath.getList( "collections" ); + BBox extent = parseSpatialExtent( (Map) collections.get( 0 ) ); + + String queryParam = extent.asQueryParameter(); + String[] queryParams = queryParam.split( "," ); + assertThat( queryParams.length, is( 4 ) ); + assertEquals( Double.parseDouble( queryParams[0] ), 5.61272621360749, 0.00001 ); + assertEquals( Double.parseDouble( queryParams[1] ), 50.2373512077239, 0.00001 ); + assertEquals( Double.parseDouble( queryParams[2] ), 9.58963433710139, 0.00001 ); + assertEquals( Double.parseDouble( queryParams[3] ), 52.5286304537795, 0.00001 ); + } + + @Test + public void testFindLinkToItself() { + List> links = jsonPath.getList( "links" ); + Map linkToItself = findLinkByRel( links, "self" ); + + assertThat( linkToItself.get( "href" ), + is( "http://www.ldproxy.nrw.de/rest/services/kataster/collections/?f=json" ) ); + assertThat( linkToItself.get( "rel" ), is( "self" ) ); + assertThat( linkToItself.get( "type" ), is( "application/json" ) ); + assertThat( linkToItself.get( "title" ), is( "this document" ) ); + } + + @Test + public void testLinkIncludesRelAndType() { + List> links = jsonPath.getList( "links" ); + Map linkToItself = findLinkByRel( links, "self" ); + boolean includesRelAndType = linkIncludesRelAndType( linkToItself ); + + assertThat( includesRelAndType, is( true ) ); + } + + @Test + public void testFindLinksWithoutRelOrType() { + List> links = jsonPath.getList( "links" ); + List linksWithoutRelOrType = findLinksWithoutRelOrType( links ); + + assertThat( linksWithoutRelOrType.size(), is( 0 ) ); + } + + @Test + public void testFindLinksWithSupportedMediaTypeByRel() { + List> links = jsonPath.getList( "links" ); + List mediaTypes = Arrays.asList( "text/html", "application/json" ); + List> linksWithMediaTypes = findLinksWithSupportedMediaTypeByRel( links, mediaTypes, + "alternate" ); + + assertThat( linksWithMediaTypes.size(), is( 1 ) ); + } + + @Test + public void testHasProperty_true() { + boolean hasProperty = hasProperty( "links", jsonPath ); + assertThat( hasProperty, is( true ) ); + } + + @Test + public void testHasProperty_false() { + boolean hasProperty = hasProperty( "doesNotExist", jsonPath ); + assertThat( hasProperty, is( false ) ); + } + +} diff --git a/src/test/resources/org/opengis/cite/wfs30/collections/collections.json b/src/test/resources/org/opengis/cite/wfs30/collections/collections.json index d3d7b5af..8b8d1e80 100644 --- a/src/test/resources/org/opengis/cite/wfs30/collections/collections.json +++ b/src/test/resources/org/opengis/cite/wfs30/collections/collections.json @@ -48,6 +48,10 @@ 50.2373512077239, 9.58963433710139, 52.5286304537795 + ], + "temporal": [ + "2017-01-01T00:00:00Z", + "2017-12-31T23:59:59Z" ] }, "links": [