def test_get(self, client): """Prove that the happy flow works""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=ListStoredQueries&VERSION=2.0.0") content = response.content.decode() assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "ListStoredQueriesResponse" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert len(xml_doc) == 1 assert_xml_equal( content, """<wfs:ListStoredQueriesResponse xmlns="http://www.opengis.net/wfs/2.0" xmlns:app="http://example.org/gisserver" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd"> <StoredQuery id="urn:ogc:def:query:OGC-WFS::GetFeatureById"> <Title>Get feature by identifier</Title> <ReturnFeatureType>restaurant</ReturnFeatureType> <ReturnFeatureType>mini-restaurant</ReturnFeatureType> <ReturnFeatureType>denied-feature</ReturnFeatureType> </StoredQuery> </wfs:ListStoredQueriesResponse>""", # noqa: E501 )
def test_get(self, client): """Prove that the happy flow works""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=DescribeStoredQueries&VERSION=2.0.0") content = response.content.decode() assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "DescribeStoredQueriesResponse" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert len(xml_doc) == 1 assert_xml_equal( content, """<wfs:DescribeStoredQueriesResponse xmlns="http://www.opengis.net/wfs/2.0" xmlns:app="http://example.org/gisserver" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd"> <StoredQueryDescription id="urn:ogc:def:query:OGC-WFS::GetFeatureById"> <Title>Get feature by identifier</Title> <Abstract>Returns the single feature that corresponds with the ID argument</Abstract> <Parameter name="ID" type="xs:string"/> <QueryExpressionText isPrivate="true" language="urn:ogc:def:queryLanguage:OGC-WFS::WFS_QueryExpression" returnFeatureTypes="restaurant mini-restaurant denied-feature"/> </StoredQueryDescription> </wfs:DescribeStoredQueriesResponse>""", # noqa: E501 )
def test_missing_parameters(self, client): """Prove that missing arguments are handled""" response = client.get("/v1/wfs/?SERVICE=WFS") content = response.content.decode() assert response.status_code == 400, content assert response["content-type"] == "text/xml; charset=utf-8", content assert_xml_equal( response.content, """<ows:ExceptionReport version="2.0.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xml:lang="en-US" xsi:schemaLocation="http://www.opengis.net/ows/1.1 http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd"> <ows:Exception exceptionCode="MissingParameterValue" locator="request"> <ows:ExceptionText>Missing required 'request' parameter.</ows:ExceptionText> </ows:Exception> </ows:ExceptionReport>""", # noqa: E501 ) xml_doc = validate_xsd(response.content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" exception = xml_doc.find("ows:Exception", NAMESPACES) assert exception.attrib["exceptionCode"] == "MissingParameterValue"
def test_pagination(self, client, restaurant, bad_restaurant): """Prove that that parsing BBOX=... works""" names = [] url = ( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0&TYPENAMES=restaurant" "&VALUEREFERENCE=name&SORTBY=name" ) for _ in range(4): # test whether last page stops response = client.get(f"{url}&COUNT=1") content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "</wfs:ValueCollection>" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["numberMatched"] == "2" assert xml_doc.attrib["numberReturned"] == "1" # Collect the names members = xml_doc.findall("wfs:member", namespaces=NAMESPACES) names.extend( res.find("app:name", namespaces=NAMESPACES).text for res in members ) url = xml_doc.attrib.get("next") if not url: break # Prove that both items were returned assert len(names) == 2 assert names[0] != names[1]
def test_get(self, client, restaurant, bad_restaurant, xpath): """Prove that the happy flow works""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0&TYPENAMES=restaurant" f"&VALUEREFERENCE={xpath}" ) content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "</wfs:ValueCollection>" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["numberMatched"] == "2" assert xml_doc.attrib["numberReturned"] == "2" timestamp = xml_doc.attrib["timeStamp"] assert_xml_equal( content, f"""<wfs:ValueCollection xmlns:app="http://example.org/gisserver" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://example.org/gisserver http://testserver/v1/wfs/?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES=restaurant http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd http://www.opengis.net/gml/3.2 http://schemas.opengis.net/gml/3.2.1/gml.xsd" timeStamp="{timestamp}" numberMatched="2" numberReturned="2"> <wfs:member> <app:name>Café Noir</app:name> </wfs:member> <wfs:member> <app:name>Foo Bar</app:name> </wfs:member> </wfs:ValueCollection>""", # noqa: E501 )
def test_get_location(self, client, restaurant, coordinates): """Prove that rendering geometry values also works""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0&TYPENAMES=restaurant" "&VALUEREFERENCE=location" ) content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "</wfs:ValueCollection>" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["numberMatched"] == "1" assert xml_doc.attrib["numberReturned"] == "1" timestamp = xml_doc.attrib["timeStamp"] assert_xml_equal( content, f"""<wfs:ValueCollection xmlns:app="http://example.org/gisserver" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://example.org/gisserver http://testserver/v1/wfs/?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES=restaurant http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd http://www.opengis.net/gml/3.2 http://schemas.opengis.net/gml/3.2.1/gml.xsd" timeStamp="{timestamp}" numberMatched="1" numberReturned="1"> <wfs:member> <app:location> <gml:Point gml:id="restaurant.{restaurant.id}.1" srsName="urn:ogc:def:crs:EPSG::4326"> <gml:pos srsDimension="2">{coordinates.point1_xml_wgs84}</gml:pos> </gml:Point> </app:location> </wfs:member> </wfs:ValueCollection>""", # noqa: E501 )
def test_version_negotiation(self, client): """Prove that version negotiation still returns 2.0.0""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetCapabilities&ACCEPTVERSIONS=1.0.0,2.0.0" ) content = response.content.decode() assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content xml_doc = validate_xsd(response.content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0"
def test_get_invalid_version(self, client): """Prove that version negotiation works""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetCapabilities&ACCEPTVERSIONS=1.5.0" ) content = response.content.decode() assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 400, content xml_doc = validate_xsd(response.content, WFS_20_XSD) exception = xml_doc.find("ows:Exception", NAMESPACES) assert exception.attrib["exceptionCode"] == "VersionNegotiationFailed"
def test_empty_typenames(self, client): """Prove that missing arguments are handled""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAMES=" ) content = response.content.decode() assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 400, content assert "</ows:Exception>" in content xml_doc = validate_xsd(response.content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" exception = xml_doc.find("ows:Exception", NAMESPACES) assert exception.attrib["exceptionCode"] == "MissingParameterValue"
def _assert_filter(self, response, expect="Café Noir"): content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "</wfs:ValueCollection>" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["numberMatched"] == "1" assert xml_doc.attrib["numberReturned"] == "1" # Assert that the correct object was matched name = xml_doc.find("wfs:member/app:name", namespaces=NAMESPACES).text assert name == expect
def test_resource_id_invalid(self, client, restaurant, bad_restaurant): """Prove that TYPENAMES should be omitted, or match the RESOURCEID.""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0" "&RESOURCEID=restaurant.ABC&VALUEREFERENCE=name" ) content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 400, content assert "</ows:Exception>" in content xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" exception = xml_doc.find("ows:Exception", NAMESPACES) message = exception.find("ows:ExceptionText", NAMESPACES).text assert exception.attrib["exceptionCode"] == "InvalidParameterValue", message assert exception.attrib["locator"] == "resourceId", message
def test_get_feature_by_id_404(self, client, restaurant, bad_restaurant): """Prove that missing IDs are properly handled.""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0" "&STOREDQUERY_ID=urn:ogc:def:query:OGC-WFS::GetFeatureById" "&ID=restaurant.0&VALUEREFERENCE=name") content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 404, content xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" exception = xml_doc.find("ows:Exception", NAMESPACES) assert exception.attrib["exceptionCode"] == "NotFound" message = exception.find("ows:ExceptionText", NAMESPACES).text assert message == "Feature not found with ID 0."
def test_resource_id_unknown_id(self, client, restaurant, bad_restaurant): """Prove that unknown IDs simply return an empty list.""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0&TYPENAMES=restaurant" "&RESOURCEID=restaurant.0&VALUEREFERENCE=name") content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "</wfs:ValueCollection>" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["numberMatched"] == "0" assert xml_doc.attrib["numberReturned"] == "0" # Test sort ordering. members = xml_doc.findall("wfs:member", namespaces=NAMESPACES) assert len(members) == 0
def test_get_unauth(self, client): """Prove that features may block access. Note that HTTP 403 is not in the WFS 2.0 spec, but still useful to have. """ response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0&TYPENAMES=denied-feature" "&VALUEREFERENCE=name") content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 403, content assert "</ows:Exception>" in content xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" exception = xml_doc.find("ows:Exception", NAMESPACES) assert exception.attrib["exceptionCode"] == "PermissionDenied" message = exception.find("ows:ExceptionText", NAMESPACES).text assert message == "No access to this feature."
def test_resource_id(self, client, restaurant, bad_restaurant): """Prove that fetching objects by ID works.""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0" f"&RESOURCEID=restaurant.{restaurant.id}&VALUEREFERENCE=name" ) content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "</wfs:ValueCollection>" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["numberMatched"] == "1" assert xml_doc.attrib["numberReturned"] == "1" # Test sort ordering. members = xml_doc.findall("wfs:member", namespaces=NAMESPACES) names = [res.find("app:name", namespaces=NAMESPACES).text for res in members] assert names == ["Café Noir"]
def test_get_filter_invalid(self, client, restaurant, filter_name): """Prove that that parsing FILTER=<fes:Filter>... works""" filter, expect_exception = INVALID_FILTERS[filter_name] response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0&TYPENAMES=restaurant" "&VALUEREFERENCE=name&FILTER=" + quote_plus(filter.strip()) ) content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 400, content assert "</ows:Exception>" in content xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" exception = xml_doc.find("ows:Exception", NAMESPACES) message = exception.find("ows:ExceptionText", NAMESPACES).text assert exception.attrib["exceptionCode"] == expect_exception.code, message assert message == expect_exception.text
def test_get_sort_by(self, client, restaurant, bad_restaurant, ordering): """Prove that that parsing BBOX=... works""" sort_by, expect = SORT_BY[ordering] response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0&TYPENAMES=restaurant" f"&VALUEREFERENCE=name&SORTBY={sort_by}" ) content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "</wfs:ValueCollection>" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["numberMatched"] == "2" assert xml_doc.attrib["numberReturned"] == "2" # Test sort ordering. members = xml_doc.findall("wfs:member", namespaces=NAMESPACES) names = [res.find("app:name", namespaces=NAMESPACES).text for res in members] assert names == expect
def test_resource_id_typename_mismatch(self, client, restaurant, bad_restaurant): """Prove that TYPENAMES should be omitted, or match the RESOURCEID.""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0" "&TYPENAMES=mini-restaurant" f"&RESOURCEID=restaurant.{restaurant.id}&VALUEREFERENCE=location") content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 400, content assert "</ows:Exception>" in content xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" exception = xml_doc.find("ows:Exception", NAMESPACES) assert exception.attrib["exceptionCode"] == "InvalidParameterValue" message = exception.find("ows:ExceptionText", NAMESPACES).text assert message == ( "When TYPENAMES and RESOURCEID are combined, " "the RESOURCEID type should be included in TYPENAMES.")
def test_get_feature_by_id_bad_id(self, client, restaurant, bad_restaurant): """Prove that invalid IDs are properly handled.""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetPropertyValue&VERSION=2.0.0" "&STOREDQUERY_ID=urn:ogc:def:query:OGC-WFS::GetFeatureById" "&ID=restaurant.ABC&VALUEREFERENCE=name") content = read_response(response) assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 400, content xml_doc = validate_xsd(content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" exception = xml_doc.find("ows:Exception", NAMESPACES) assert exception.attrib["exceptionCode"] == "InvalidParameterValue" message = exception.find("ows:ExceptionText", NAMESPACES).text expect = ( "Invalid ID value: Field 'id' expected a number but got 'ABC'." if django.VERSION >= (3, 0) else "Invalid ID value: invalid literal for int() with base 10: 'ABC'") assert message == expect
def test_get(self, client, restaurant): """Prove that the happy flow works""" response = client.get( "/v1/wfs/?SERVICE=WFS&REQUEST=GetCapabilities&ACCEPTVERSIONS=2.0.0" ) content = response.content.decode() assert response["content-type"] == "text/xml; charset=utf-8", content assert response.status_code == 200, content assert "<ows:OperationsMetadata>" in content # Validate against the WFS 2.0 XSD xml_doc = validate_xsd(response.content, WFS_20_XSD) assert xml_doc.attrib["version"] == "2.0.0" # Check exposed allowed vesions allowed_values = xml_doc.xpath( "ows:OperationsMetadata/ows:Operation[@name='GetCapabilities']" "/ows:Parameter[@name='AcceptVersions']/ows:AllowedValues", namespaces=NAMESPACES, )[0] versions = [el.text for el in allowed_values.findall("ows:Value", NAMESPACES)] assert versions == ["2.0.0"] # Check exposed FeatureTypeList feature_type_list = xml_doc.find("wfs:FeatureTypeList", NAMESPACES) # The box should be within WGS84 limits, otherwise gis tools can't process the service. wgs84_bbox = feature_type_list.find( "wfs:FeatureType/ows:WGS84BoundingBox", NAMESPACES ) lower = wgs84_bbox.find("ows:LowerCorner", NAMESPACES).text.split(" ") upper = wgs84_bbox.find("ows:UpperCorner", NAMESPACES).text.split(" ") coords = list(map(float, lower + upper)) assert coords[0] >= -180 assert coords[1] >= -90 assert coords[2] <= 180 assert coords[2] <= 90 assert_xml_equal( etree.tostring(feature_type_list, inclusive_ns_prefixes=True).decode(), f"""<FeatureTypeList xmlns="{WFS_NS}" xmlns:ows="{OWS_NS}" xmlns:xlink="{XLINK_NS}"> <FeatureType> <Name>app:restaurant</Name> <Title>restaurant</Title> <ows:Keywords> <ows:Keyword>unittest</ows:Keyword> </ows:Keywords> <DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS> <OtherCRS>urn:ogc:def:crs:EPSG::28992</OtherCRS> <OutputFormats> <Format>application/gml+xml; version=3.2</Format> <Format>text/xml; subtype=gml/3.2.1</Format> <Format>application/json; subtype=geojson; charset=utf-8</Format> <Format>text/csv; subtype=csv; charset=utf-8</Format> </OutputFormats> <ows:WGS84BoundingBox dimensions="2"> <ows:LowerCorner>4.90876101285122 52.3631712637357</ows:LowerCorner> <ows:UpperCorner>4.90876101285122 52.3631712637357</ows:UpperCorner> </ows:WGS84BoundingBox> <MetadataURL xlink:href="http://testserver/v1/wfs/" /> </FeatureType> <FeatureType> <Name>app:mini-restaurant</Name> <Title>restaurant</Title> <ows:Keywords> <ows:Keyword>unittest</ows:Keyword> <ows:Keyword>limited-fields</ows:Keyword> </ows:Keywords> <DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS> <OtherCRS>urn:ogc:def:crs:EPSG::28992</OtherCRS> <OutputFormats> <Format>application/gml+xml; version=3.2</Format> <Format>text/xml; subtype=gml/3.2.1</Format> <Format>application/json; subtype=geojson; charset=utf-8</Format> <Format>text/csv; subtype=csv; charset=utf-8</Format> </OutputFormats> <ows:WGS84BoundingBox dimensions="2"> <ows:LowerCorner>4.90876101285122 52.3631712637357</ows:LowerCorner> <ows:UpperCorner>4.90876101285122 52.3631712637357</ows:UpperCorner> </ows:WGS84BoundingBox> <MetadataURL xlink:href="http://testserver/v1/wfs/" /> </FeatureType> <FeatureType> <Name>app:denied-feature</Name> <Title>restaurant</Title> <DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS> <OutputFormats> <Format>application/gml+xml; version=3.2</Format> <Format>text/xml; subtype=gml/3.2.1</Format> <Format>application/json; subtype=geojson; charset=utf-8</Format> <Format>text/csv; subtype=csv; charset=utf-8</Format> </OutputFormats> </FeatureType> </FeatureTypeList>""", )