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 79458a39..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,7 +5,7 @@ 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.findLinkToItself; +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; @@ -157,7 +157,7 @@ public void validateFeatureCollectionsMetadataOperationResponse_Links( TestPoint List> links = jsonPath.getList( "links" ); // Validate that the retrieved document includes links for: Itself, - Map linkToSelf = findLinkToItself( links ); + 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" ); diff --git a/src/main/java/org/opengis/cite/wfs30/collections/GetFeaturesOperation.java b/src/main/java/org/opengis/cite/wfs30/collections/GetFeaturesOperation.java index 9e593f92..39decc03 100644 --- a/src/main/java/org/opengis/cite/wfs30/collections/GetFeaturesOperation.java +++ b/src/main/java/org/opengis/cite/wfs30/collections/GetFeaturesOperation.java @@ -4,14 +4,20 @@ import static org.opengis.cite.wfs30.SuiteAttribute.API_MODEL; import static org.opengis.cite.wfs30.WFS3.GEOJSON_MIME_TYPE; import static org.opengis.cite.wfs30.WFS3.PATH.COLLECTIONS; -import static org.opengis.cite.wfs30.util.JsonUtils.findLinkToItself; +import static org.opengis.cite.wfs30.util.JsonUtils.collectNumberOfAllReturnedFeatures; +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.hasProperty; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; +import java.net.URISyntaxException; +import java.text.ParseException; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,6 +32,7 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import com.fasterxml.jackson.databind.util.ISO8601DateFormat; import com.reprezen.kaizen.oasparser.model3.MediaType; import com.reprezen.kaizen.oasparser.model3.OpenApi3; @@ -37,7 +44,9 @@ */ public class GetFeaturesOperation extends CommonFixture { - private final Map collectionNameAndResponse = new HashMap<>(); + private static final ISO8601DateFormat DATE_FORMAT = new ISO8601DateFormat(); + + private final Map collectionNameAndResponse = new HashMap<>(); private List> collections; @@ -91,13 +100,16 @@ public void validateGetFeaturesOperation( Map collection ) { throw new SkipException( "Could not find url for collection with name " + collectionName + " supporting GeoJson (type " + GEOJSON_MIME_TYPE + ")" ); + Date timeStampBeforeResponse = new Date(); Response response = init().baseUri( getFeaturesUrl ).accept( GEOJSON_MIME_TYPE ).when().request( GET ); response.then().statusCode( 200 ); - collectionNameAndResponse.put( collectionName, response ); + Date timeStampAfterResponse = new Date(); + ResponseData responseData = new ResponseData( response, timeStampBeforeResponse, timeStampAfterResponse ); + collectionNameAndResponse.put( collectionName, responseData ); } /** - * A.4.4.10. Validate the Get Features Operation Response (Part 1) + * A.4.4.10. Validate the Get Features Operation Response (Test method 2, 3) * * a) Test Purpose: Validate the Get Feature Operation Response. * @@ -118,7 +130,7 @@ public void validateGetFeaturesOperation( Map collection ) { @Test(description = "Implements A.4.4.10. Validate the Get Features Operation Response (Requirement 25, 26)", dataProvider = "collectionItemUris", dependsOnMethods = "validateGetFeaturesOperation") public void validateGetFeaturesOperationResponse_Links( Map collection ) { String collectionName = (String) collection.get( "name" ); - Response response = collectionNameAndResponse.get( collectionName ); + ResponseData response = collectionNameAndResponse.get( collectionName ); if ( response == null ) throw new SkipException( "Could not find a response for collection with name " + collectionName ); @@ -133,7 +145,7 @@ public void validateGetFeaturesOperationResponse_Links( Map coll List> links = jsonPath.getList( "links" ); // Validate that the retrieved document includes links for: Itself - Map linkToSelf = findLinkToItself( links ); + 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 @@ -154,19 +166,91 @@ public void validateGetFeaturesOperationResponse_Links( Map coll } /** - * A.4.4.10. Validate the Get Features Operation Response (Part 2) + * 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: Requirements 27, 28 and 29 + * + * @param collection + * the collection under test, never null + */ + @Test(description = "Implements A.4.4.10. Validate the Get Features Operation Response (Requirement 27, 28, 29)", dataProvider = "collectionItemUris", dependsOnMethods = "validateGetFeaturesOperation") + public void validateGetFeaturesOperationResponse_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(); + + String timeStamp = jsonPath.getString( "timeStamp" ); + if ( timeStamp == null ) + throw new SkipException( "Property timeStamp is not set in collection items '" + collectionName + "'" ); + + Date date = parseAsDate( timeStamp ); + assertTrue( date.before( response.timeStampAfterResponse ), + "timeStamp in response must be before the request was send (" + + DATE_FORMAT.format( response.timeStampAfterResponse ) + ") but was '" + + timeStamp + "'" ); + assertTrue( date.after( response.timeStampBeforeResponse ), + "timeStamp in response must be after the request was send (" + + DATE_FORMAT.format( response.timeStampBeforeResponse ) + ") but was '" + + timeStamp + "'" ); + } + + /** + * 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: Requirements 27, 28 and 29 + * + * @param collection + * the collection under test, never null + */ + @Test(description = "Implements A.4.4.10. Validate the Get Features Operation Response (Requirement 27, 28, 29)", dataProvider = "collectionItemUris", dependsOnMethods = "validateGetFeaturesOperation") + 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(); + + if ( !hasProperty( "numberReturned", jsonPath ) ) { + throw new SkipException( "Property numberReturned is not set in collection items '" + collectionName + "'" ); + } + 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 + ")" ); + } + + /** + * 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. @@ -175,15 +259,28 @@ public void validateGetFeaturesOperationResponse_Links( Map coll * * @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 27, 28, 29)", dataProvider = "collectionItemUris", dependsOnMethods = "validateGetFeaturesOperation") - public void validateGetFeaturesOperationResponse_Properties( Map collection ) { + public void validateGetFeaturesOperationResponse_property_numberMatched( Map collection ) + throws URISyntaxException { String collectionName = (String) collection.get( "name" ); - Response response = collectionNameAndResponse.get( collectionName ); + ResponseData response = collectionNameAndResponse.get( collectionName ); if ( response == null ) throw new SkipException( "Could not find a response for collection with name " + collectionName ); - // TODO + JsonPath jsonPath = response.jsonPath(); + + if ( !hasProperty( "numberMatched", jsonPath ) ) { + throw new SkipException( "Property numberMatched is not set in collection items '" + collectionName + "'" ); + } + 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 String findGetFeatureUrlForGeoJson( Map collection ) { @@ -206,4 +303,32 @@ private List createListOfMediaTypesToSupport( TestPoint testPoint, MapLyn Goltz */ @@ -77,12 +87,14 @@ public static List findLinksWithoutRelOrType( List> * * @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 findLinkToItself( List> links ) { + public static Map findLinkByRel( List> links, String expectedRel ) { for ( Map link : links ) { Object rel = link.get( "rel" ); - if ( "self".equals( rel ) ) + if ( expectedRel.equals( rel ) ) return link; } return null; @@ -103,6 +115,61 @@ public static boolean linkIncludesRelAndType( Map link ) { 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" ); diff --git a/src/test/java/org/opengis/cite/wfs30/collections/GetFeaturesOperationIT.java b/src/test/java/org/opengis/cite/wfs30/collections/GetFeaturesOperationIT.java index 5bae62c2..10d5875a 100644 --- a/src/test/java/org/opengis/cite/wfs30/collections/GetFeaturesOperationIT.java +++ b/src/test/java/org/opengis/cite/wfs30/collections/GetFeaturesOperationIT.java @@ -5,6 +5,8 @@ import java.io.InputStream; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.util.List; import java.util.Map; @@ -14,6 +16,9 @@ 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; /** @@ -28,6 +33,10 @@ public class GetFeaturesOperationIT { @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" ); @@ -38,11 +47,13 @@ public static void initTestFixture() 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() { + public void testGetFeatureOperations() + throws URISyntaxException { GetFeaturesOperation getFeaturesOperation = new GetFeaturesOperation(); getFeaturesOperation.initCommonFixture( testContext ); getFeaturesOperation.retrieveRequiredInformationFromTestContext( testContext ); @@ -52,7 +63,9 @@ public void testGetFeatureOperations() { Map parameter = (Map) collection[0]; getFeaturesOperation.validateGetFeaturesOperation( parameter ); getFeaturesOperation.validateGetFeaturesOperationResponse_Links( parameter ); - getFeaturesOperation.validateGetFeaturesOperationResponse_Properties( parameter ); + //skipped: getFeaturesOperation.validateGetFeaturesOperationResponse_property_timeStamp( parameter ); + //getFeaturesOperation.validateGetFeaturesOperationResponse_property_numberReturned( parameter ); + //getFeaturesOperation.validateGetFeaturesOperationResponse_property_numberMatched( parameter ); } } 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 index 0f88b70f..38a1a45f 100644 --- a/src/test/java/org/opengis/cite/wfs30/util/JsonUtilsTest.java +++ b/src/test/java/org/opengis/cite/wfs30/util/JsonUtilsTest.java @@ -2,9 +2,10 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import static org.opengis.cite.wfs30.util.JsonUtils.findLinkToItself; +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.hasProperty; import static org.opengis.cite.wfs30.util.JsonUtils.linkIncludesRelAndType; import java.io.InputStream; @@ -33,7 +34,7 @@ public static void parseJson() { @Test public void testFindLinkToItself() { List> links = jsonPath.getList( "links" ); - Map linkToItself = findLinkToItself( links ); + Map linkToItself = findLinkByRel( links, "self" ); assertThat( linkToItself.get( "href" ), is( "http://www.ldproxy.nrw.de/rest/services/kataster/collections/?f=json" ) ); @@ -45,7 +46,7 @@ public void testFindLinkToItself() { @Test public void testLinkIncludesRelAndType() { List> links = jsonPath.getList( "links" ); - Map linkToItself = findLinkToItself( links ); + Map linkToItself = findLinkByRel( links, "self" ); boolean includesRelAndType = linkIncludesRelAndType( linkToItself ); assertThat( includesRelAndType, is( true ) ); @@ -69,4 +70,16 @@ public void testFindLinksWithSupportedMediaTypeByRel() { 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 ) ); + } + }