import json from unittest import mock import pytest from django.core.management import call_command import tests.utils from passerelle.apps.opengis.models import FeatureCache, OpenGIS, Query from passerelle.base.models import Job from passerelle.utils import import_site from tests.test_manager import login pytestmark = pytest.mark.django_db FAKE_FEATURE_INFO = ''' Parcelle cadastrale (Plan cadastral informatise du Grand Lyon) 4.784140,45.796890 4.784834,45.797365 69040BD309 040000BD0309 309 2406 Particulier Parcelle figuree au plan Arpentee 75404 bar ''' FAKE_FEATURE_INFO_FEATURE_COLLECTION = ''' 531238.20604672 5735107.61605308 546464.26335844 5751147.23178471 531238.20604672 5735107.61605308 546464.26335844 5751147.23178471 200046977-ZFE-001 2020-01-01 V5 24/7 V3 24/7 V3 24/7 V5 24/7 V5 24/7 https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf https://www.grandlyon.com/actions/zfe.html#c20726 ... 200046977-ZFE-001 ''' FAKE_SERVICE_CAPABILITIES = ''' WFS2.0.0 application/gml+xml; version=3.2 application/json; subtype=geojson ''' FAKE_SERVICE_CAPABILITIES_V1_0_0 = ''' WFS1.0.0 application/gml+xml; version=3.2 application/json; subtype=geojson ''' FAKE_FEATURES_JSON = ''' { "features": [ { "geometry": null, "id": "ref_metro_limites_communales.fid--204aa923_15ffdce8d91_-27be", "properties": { "nom": "Bri\u00e9-et-Angonnes" }, "type": "Feature" }, { "geometry": null, "id": "ref_metro_limites_communales.fid--204aa923_15ffdce8d91_-27bd", "properties": { "nom": "Champagnier" }, "type": "Feature" }, { "geometry": null, "id": "ref_metro_limites_communales.fid--204aa923_15ffdce8d91_-27bb", "properties": { "nom": "Claix" }, "type": "Feature" }, { "geometry": null, "id": "ref_metro_limites_communales.fid--204aa923_15ffdce8d91_-27ba", "properties": { "nom": "Corenc" }, "type": "Feature" }, { "geometry": null, "id": "ref_metro_limites_communales.fid--204aa923_15ffdce8d91_-27b9", "properties": { "nom": "\u00c9chirolles" }, "type": "Feature" }, { "geometry": null, "id": "ref_metro_limites_communales.fid--204aa923_15ffdce8d91_-27b8", "properties": { "nom": "Eybens" }, "type": "Feature" }, { "geometry": null, "id": "ref_metro_limites_communales.fid--204aa923_15ffdce8d91_-27b7", "properties": { "nom": "Fontaine" }, "type": "Feature" } ], "type": "FeatureCollection" }''' FAKE_ERROR = ''' Could not parse CQL filter list. Encountered "BIS" at line 1, column 129. Was expecting one of: <EOF> "and" ... "or" ... ";" ... "/" ... "*" ... "+" ... "-" ... Parsing : strEqualsIgnoreCase(nom_commune, 'Grenoble') = true AND strEqualsIgnoreCase(nom_voie, 'rue albert recoura') = true AND numero=8 BIS. ''' FAKE_GEOLOCATED_FEATURE = '''{ "crs": { "properties": { "name": "urn:ogc:def:crs:EPSG::3945" }, "type": "name" }, "features": [ { "geometry": { "coordinates": [ 1914059.51, 4224699.2 ], "type": "Point" }, "geometry_name": "the_geom", "properties": { "code_insee": 38185, "code_post": 38000, "nom_afnor": "BOULEVARD EDOUARD REY", "nom_commune": "Grenoble", "nom_voie": "boulevard \u00e9douard rey", "numero": 17 }, "type": "Feature" }, { "geometry": { "coordinates": [ 1914042.47, 4224665.2 ], "type": "Point" }, "geometry_name": "the_geom", "properties": { "code_insee": 38185, "code_post": 38000, "nom_commune": "Grenoble", "nom_voie": "place victor hugo", "numero": 2 }, "type": "Feature" }, { "geometry": { "coordinates": [ 1914035.7, 4224700.42 ], "type": "Point" }, "geometry_name": "the_geom", "properties": { "code_insee": 38185, "code_post": 38000, "nom_commune": "Grenoble", "nom_voie": "boulevard \u00e9douard rey", "numero": 28 }, "type": "Feature" }, { "geometry": { "coordinates": [ 1914018.64, 4224644.61 ], "type": "Point" }, "geometry_name": "the_geom", "properties": { "code_insee": 38185, "code_post": 38000, "nom_commune": "Grenoble", "nom_voie": "place victor hugo", "numero": 4 }, "type": "Feature" }, { "geometry": { "coordinates": [ [ [1914018, 4224644], [1914018, 4224844], [1914318, 4224944], [1914318, 4224544] ] ], "type": "Polygon" }, "geometry_name": "the_geom", "properties": { "code_insee": 38185, "code_post": 38000, "nom_commune": "Grenoble", "nom_square": "place trapeze" }, "type": "Feature" }, { "geometry": { "coordinates": [ [ [1914059, 4224699], [1914059, 4224899], [1914259, 4224699] ] ], "type": "Polygon" }, "geometry_name": "the_geom", "properties": { "code_insee": 38185, "code_post": 38000, "nom_commune": "Grenoble", "nom_square": "place triangle" }, "type": "Feature" } ], "totalFeatures": 6, "type": "FeatureCollection" }''' FAKE_GEOLOCATED_FEATURE_CIRCLE = { 'type': 'FeatureCollection', 'features': [ { 'type': 'Feature', 'properties': { 'in-circle': False, 'in-bbox': False, }, 'geometry': {'type': 'Point', 'coordinates': [2.3555374145507812, 48.906705448392216]}, }, { 'type': 'Feature', 'properties': { 'in-circle': True, 'in-bbox': True, }, 'geometry': {'type': 'Point', 'coordinates': [2.3366546630859375, 48.86990906900767]}, }, { 'type': 'Feature', 'properties': { 'in-circle': True, 'in-bbox': True, }, 'geometry': {'type': 'Point', 'coordinates': [2.344207763671875, 48.82901755447848]}, }, { 'type': 'Feature', 'properties': { 'in-circle': False, 'in-bbox': True, }, 'geometry': {'type': 'Point', 'coordinates': [2.304, 48.8086]}, }, { 'type': 'Feature', 'properties': { 'in-circle': True, 'in-bbox': True, }, 'geometry': { 'type': 'Polygon', 'coordinates': [[[2.304, 48.8086], [2.304, 49.8086], [1.304, 48.8086]]], }, }, { 'type': 'Feature', 'properties': { 'in-circle': False, 'in-bbox': False, }, 'geometry': { 'type': 'Polygon', 'coordinates': [[[-2.304, 48.8086], [-2.304, 49.8086], [-1.304, 48.8086]]], }, }, ], } @pytest.fixture def connector(): return tests.utils.setup_access_rights( OpenGIS.objects.create( slug='test', wms_service_url='http://example.net/wms', wfs_service_url='http://example.net/wfs' ) ) @pytest.fixture def query(connector): return Query.objects.create( resource=connector, name='Test Query', slug='test_query', description='Test query.', typename='pvo_patrimoine_voirie.pvoparking', filter_expression=( 'typeparking' '' ), ) def geoserver_responses(url, **kwargs): if kwargs['params'].get('request') == 'GetCapabilities': assert kwargs['params'].get('service') return tests.utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES) return tests.utils.FakedResponse(status_code=200, content=FAKE_FEATURES_JSON) def geoserver_responses_v1_0_0(url, **kwargs): if kwargs['params'].get('request') == 'GetCapabilities': assert kwargs['params'].get('service') return tests.utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES_V1_0_0) return tests.utils.FakedResponse(status_code=200, content=FAKE_FEATURES_JSON) def geoserver_responses_errors(url, **kwargs): if kwargs['params'].get('request') == 'GetCapabilities': return tests.utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES) return tests.utils.FakedResponse(status_code=200, content=FAKE_ERROR) def geoserver_responses_errors_unparsable(url, **kwargs): if kwargs['params'].get('request') == 'GetCapabilities': return tests.utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES) return tests.utils.FakedResponse(status_code=200, content=FAKE_ERROR[:10]) def geoserver_geolocated_responses(url, **kwargs): if kwargs['params'].get('request') == 'GetCapabilities': return tests.utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES) return tests.utils.FakedResponse(status_code=200, content=FAKE_GEOLOCATED_FEATURE) def geoserver_circle_responses(url, **kwargs): if kwargs['params'].get('request') == 'GetCapabilities': return tests.utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES) return tests.utils.FakedResponse(status_code=200, content=json.dumps(FAKE_GEOLOCATED_FEATURE_CIRCLE)) @mock.patch('passerelle.utils.Request.get') def test_feature_info(mocked_get, app, connector): endpoint = tests.utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug) assert endpoint == '/opengis/test/feature_info' mocked_get.return_value = tests.utils.FakedResponse(content=FAKE_FEATURE_INFO, status_code=200) resp = app.get(endpoint, params={'lat': '45.796890', 'lon': '4.784140'}) assert ( mocked_get.call_args[1]['params']['bbox'] == '532556.896735,5747844.261214,532579.160633,5747876.194333' ) assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:3857' assert ( resp.json['data']['cad_cadastrecadparcelle_layer']['cad_cadastrecadparcelle_feature'][ 'natureproprietaire' ] == 'Particulier' ) assert 'name' not in resp.json['data']['cad_cadastrecadparcelle_layer'] assert ( 'boundedBy' not in resp.json['data']['cad_cadastrecadparcelle_layer']['cad_cadastrecadparcelle_feature'] ) assert 'foo' in resp.json['data']['cad_cadastrecadparcelle_layer']['cad_cadastrecadparcelle_feature'] connector.projection = 'EPSG:4326' connector.save() resp = app.get(endpoint, params={'lat': '45.796890', 'lon': '4.784140'}) assert mocked_get.call_args[1]['params']['bbox'] == '45.796790,4.784040,45.796990,4.784240' assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:4326' @mock.patch('passerelle.utils.Request.get') def test_feature_info_feature_collection(mocked_get, app, connector): endpoint = tests.utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug) assert endpoint == '/opengis/test/feature_info' mocked_get.return_value = tests.utils.FakedResponse( content=FAKE_FEATURE_INFO_FEATURE_COLLECTION, status_code=200 ) resp = app.get(endpoint, params={'lat': '45.796890', 'lon': '4.784140'}) assert ( mocked_get.call_args[1]['params']['bbox'] == '532556.896735,5747844.261214,532579.160633,5747876.194333' ) assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:3857' assert resp.json['data']['autobus_autocars_critair'] == 'V5' @mock.patch('passerelle.utils.Request.get') @pytest.mark.parametrize( 'lat,lon', [ ('bad-value', '4.784140'), ('45.796890', 'bad-value'), ], ) def test_feature_info_bad_request(mocked_get, app, connector, lat, lon): endpoint = tests.utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug) assert endpoint == '/opengis/test/feature_info' mocked_get.return_value = tests.utils.FakedResponse(content=FAKE_FEATURE_INFO, status_code=200) resp = app.get(endpoint, params={'lat': lat, 'lon': lon}) assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'Bad coordinates format' @mock.patch('passerelle.utils.Request.get') def test_tile(mocked_get, app, connector): endpoint = tests.utils.generic_endpoint_url('opengis', 'tile', slug=connector.slug) assert endpoint == '/opengis/test/tile' mocked_get.return_value = tests.utils.FakedResponse( content=b'\x89PNG\r\n\x1a\n\x00\x00...', status_code=200 ) resp = app.get(endpoint + '/16/33650/23378.png') assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:3857' assert ( mocked_get.call_args[1]['params']['bbox'] == '539339.671580,5741338.068556,539951.167806,5741949.564782' ) connector.projection = 'EPSG:4326' connector.save() resp = app.get(endpoint + '/16/33650/23378.png') assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:4326' assert mocked_get.call_args[1]['params']['bbox'] == '45.756026,4.844971,45.759859,4.850464' assert resp.content == b'\x89PNG\r\n\x1a\n\x00\x00...' @mock.patch('passerelle.utils.Request.get') def test_get_feature_with_no_wfs_url(mocked_get, app, connector): connector.wfs_service_url = '' connector.save() endpoint = tests.utils.generic_endpoint_url('opengis', 'features', slug=connector.slug) assert endpoint == '/opengis/test/features' mocked_get.side_effect = geoserver_responses resp = app.get(endpoint, params={'type_names': 'ref_metro_limites_communales', 'property_name': 'nom'}) assert resp.json['data'] is None assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'no wfs URL declared' @mock.patch('passerelle.utils.Request.get') def test_get_feature(mocked_get, app, connector): endpoint = tests.utils.generic_endpoint_url('opengis', 'features', slug=connector.slug) assert endpoint == '/opengis/test/features' mocked_get.side_effect = geoserver_responses resp = app.get(endpoint, params={'type_names': 'ref_metro_limites_communales', 'property_name': 'nom'}) assert mocked_get.call_args[1]['params']['request'] == 'GetFeature' assert mocked_get.call_args[1]['params']['propertyName'] == 'nom' assert mocked_get.call_args[1]['params']['typenames'] == 'ref_metro_limites_communales' assert 'json' in mocked_get.call_args[1]['params']['outputFormat'] assert mocked_get.call_args[1]['params']['service'] == 'WFS' assert mocked_get.call_args[1]['params']['version'] == connector.get_wfs_service_version() assert len(resp.json['data']) == 7 for item in resp.json['data']: assert 'text' in item @mock.patch('passerelle.utils.Request.get') def test_get_filtered_feature(mocked_get, app, connector): endpoint = tests.utils.generic_endpoint_url('opengis', 'features', slug=connector.slug) mocked_get.side_effect = geoserver_responses app.get( endpoint, params={ 'type_names': 'ref_metro_limites_communales', 'property_name': 'nom', 'cql_filter': 'nom=\'Fontaine\'', }, ) assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\'' @mock.patch('passerelle.utils.Request.get') def test_get_filtered_by_property_feature(mocked_get, app, connector): endpoint = tests.utils.generic_endpoint_url('opengis', 'features', slug=connector.slug) mocked_get.side_effect = geoserver_responses params = { 'type_names': 'ref_metro_limites_communales', 'property_name': 'nom', 'cql_filter': 'nom=\'Fontaine\'', 'filter_property_name': 'nom', } app.get(endpoint, params=params) assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\'' params['q'] = 'bens' app.get(endpoint, params=params) assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\' AND nom LIKE \'%bens%\'' params['case-insensitive'] = True app.get(endpoint, params=params) assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\' AND nom ILIKE \'%bens%\'' del params['case-insensitive'] params['case_insensitive'] = True assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\' AND nom ILIKE \'%bens%\'' params.pop('cql_filter') app.get(endpoint, params=params) assert 'cql_filter' not in mocked_get.call_args[1]['params'] @mock.patch('passerelle.utils.Request.get') def test_get_feature_error(mocked_get, app, connector): endpoint = tests.utils.generic_endpoint_url('opengis', 'features', slug=connector.slug) assert endpoint == '/opengis/test/features' mocked_get.side_effect = geoserver_responses_errors resp = app.get(endpoint, params={'type_names': 'ref_metro_limites_communales', 'property_name': 'nom'}) assert mocked_get.call_args[1]['params']['request'] == 'GetFeature' assert mocked_get.call_args[1]['params']['propertyName'] == 'nom' assert mocked_get.call_args[1]['params']['typenames'] == 'ref_metro_limites_communales' assert 'json' in mocked_get.call_args[1]['params']['outputFormat'] assert mocked_get.call_args[1]['params']['service'] == 'WFS' assert mocked_get.call_args[1]['params']['version'] == connector.get_wfs_service_version() result = resp.json assert result['err'] == 1 assert result['err_desc'] == 'OpenGIS Error: NoApplicableCode' assert 'Could not parse' in result['data']['text'] @mock.patch('passerelle.utils.Request.get') def test_get_feature_error2(mocked_get, app, connector): endpoint = tests.utils.generic_endpoint_url('opengis', 'features', slug=connector.slug) assert endpoint == '/opengis/test/features' mocked_get.side_effect = geoserver_responses_errors_unparsable resp = app.get(endpoint, params={'type_names': 'ref_metro_limites_communales', 'property_name': 'nom'}) assert mocked_get.call_args[1]['params']['request'] == 'GetFeature' assert mocked_get.call_args[1]['params']['propertyName'] == 'nom' assert mocked_get.call_args[1]['params']['typenames'] == 'ref_metro_limites_communales' assert 'json' in mocked_get.call_args[1]['params']['outputFormat'] assert mocked_get.call_args[1]['params']['service'] == 'WFS' assert mocked_get.call_args[1]['params']['version'] == connector.get_wfs_service_version() result = resp.json assert result['err'] == 1 assert result['err_desc'] == 'OpenGIS Error: unparsable error' assert '