310 lines
12 KiB
Python
310 lines
12 KiB
Python
# passerelle - uniform access to multiple data sources and services
|
|
# Copyright (C) 2021 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import datetime
|
|
from httmock import urlmatch, HTTMock, response, remember_called
|
|
import json
|
|
import mock
|
|
import pytest
|
|
import utils
|
|
|
|
from requests.exceptions import HTTPError
|
|
|
|
from django.core.management import call_command
|
|
|
|
from passerelle.apps.photon.models import Photon, AddressCacheModel
|
|
|
|
|
|
CONTENT = {
|
|
"features": [
|
|
{
|
|
"geometry": {"coordinates": [4.8522272, 45.7587414], "type": "Point"},
|
|
"properties": {
|
|
"city": "Lyon 3ème Arrondissement",
|
|
"country": "France",
|
|
"housenumber": "208",
|
|
"osm_id": 154419,
|
|
"osm_key": "place",
|
|
"osm_type": "N",
|
|
"osm_value": "house",
|
|
"postcode": "69003",
|
|
"street": "Rue Garibaldi",
|
|
"type": "house",
|
|
},
|
|
"type": "Feature",
|
|
},
|
|
{
|
|
"geometry": {"coordinates": [4.8522681, 45.7585214], "type": "Point"},
|
|
"properties": {
|
|
"city": "Lyon 3ème Arrondissement",
|
|
"country": "France",
|
|
"housenumber": "208bis",
|
|
"osm_id": 153400,
|
|
"osm_key": "place",
|
|
"osm_type": "N",
|
|
"osm_value": "house",
|
|
"postcode": "69003",
|
|
"street": "Rue Garibaldi",
|
|
"type": "house",
|
|
},
|
|
"type": "Feature",
|
|
},
|
|
],
|
|
"type": "FeatureCollection",
|
|
}
|
|
|
|
FAKED_CONTENT = json.dumps(CONTENT)
|
|
|
|
|
|
@urlmatch(netloc='^photon.komoot.io$', path='^/api/$')
|
|
@remember_called
|
|
def photon_search(url, request):
|
|
return response(200, CONTENT, request=request)
|
|
|
|
|
|
@urlmatch(netloc='^photon.komoot.io$', path='^/reverse/$')
|
|
def photon_reverse(url, request):
|
|
return response(200, CONTENT, request=request)
|
|
|
|
|
|
@pytest.yield_fixture
|
|
def mock_photon_search():
|
|
with HTTMock(photon_search):
|
|
yield photon_search
|
|
|
|
|
|
@pytest.yield_fixture
|
|
def mock_photon_reverse():
|
|
with HTTMock(photon_reverse):
|
|
yield None
|
|
|
|
|
|
@pytest.fixture
|
|
def photon(db):
|
|
return utils.setup_access_rights(Photon.objects.create(slug='test'))
|
|
|
|
|
|
@pytest.fixture
|
|
def photon_coordinates(db):
|
|
return utils.setup_access_rights(Photon.objects.create(slug='test', latitude=1.2, longitude=2.1))
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_search(mocked_get, app, photon):
|
|
photon.service_url = 'http://example.net/path/'
|
|
photon.save()
|
|
endpoint = utils.generic_endpoint_url('photon', 'search', slug=photon.slug)
|
|
mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
|
|
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
|
assert mocked_get.call_args[0][0].startswith('http://example.net/path/api/?')
|
|
data = resp.json[0]
|
|
assert data['lat'] == '45.7587414'
|
|
assert data['lon'] == '4.8522272'
|
|
assert data['display_name'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
|
|
|
|
|
def test_photon_search_qs(app, photon, mock_photon_search):
|
|
resp = app.get('/photon/%s/search?q=plop' % photon.slug)
|
|
assert 'display_name' in resp.json[0]
|
|
|
|
|
|
def test_photon_search_qs_zipcode(app, photon, mock_photon_search):
|
|
resp = app.get('/photon/%s/search?q=plop&zipcode=69003' % photon.slug)
|
|
assert 'display_name' in resp.json[0]
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_search_qs_lat_lon(mocked_get, app, photon):
|
|
app.get('/photon/%s/search?q=plop&lat=0&lon=1' % photon.slug)
|
|
assert 'lat=0' in mocked_get.call_args[0][0]
|
|
assert 'lon=1' in mocked_get.call_args[0][0]
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_search_qs_viewbox(mocked_get, app, photon):
|
|
app.get('/photon/%s/search?q=plop&viewbox=4.830,45.753,4.831,45.754&bounded=1' % photon.slug)
|
|
assert (
|
|
mocked_get.call_args[0][0]
|
|
== 'https://photon.komoot.io/api/?q=plop&limit=1&lang=fr&bbox=4.830%2C45.753%2C4.831%2C45.754'
|
|
)
|
|
|
|
|
|
def test_photon_search_qs_empty(app, photon, mock_photon_search):
|
|
resp = app.get('/photon/%s/search?q=' % photon.slug)
|
|
assert len(resp.json) == 0
|
|
|
|
|
|
def test_photon_search_qs_parameters_error(app, photon, mock_photon_search):
|
|
resp = app.get('/photon/%s/search' % photon.slug, status=400)
|
|
assert resp.json['err'] == 1
|
|
assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
|
|
assert resp.json['err_desc'] == u"missing parameters: 'q'."
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_api_error(mocked_get, app, photon):
|
|
def raise_for_status():
|
|
raise HTTPError("400 Client Error: Bad Request for url: xxx")
|
|
|
|
response = utils.FakedResponse(content=json.dumps({'title': 'error'}), status_code=400)
|
|
response.raise_for_status = raise_for_status
|
|
mocked_get.return_value = response
|
|
resp = app.get('/photon/%s/search' % photon.slug, params={'q': 'plop'}, status=200)
|
|
|
|
assert resp.json['err'] == 1
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_reverse_path(mocked_get, app, photon):
|
|
mocked_get.return_value = utils.FakedResponse(content=json.dumps({'features': []}), status_code=200)
|
|
photon.service_url = 'http://example.net/path/'
|
|
photon.save()
|
|
app.get('/photon/%s/reverse?lon=4.8522272&lat=45.7587414' % photon.slug)
|
|
assert mocked_get.call_args[0][0].startswith('http://example.net/path/reverse/?')
|
|
|
|
|
|
def test_photon_reverse(app, photon, mock_photon_reverse):
|
|
resp = app.get('/photon/%s/reverse?lon=4.8522272&lat=45.7587414' % photon.slug)
|
|
data = resp.json
|
|
assert 'display_name' in data
|
|
assert data['address']['city'] == 'Lyon 3ème Arrondissement'
|
|
assert data['address']['postcode'] == '69003'
|
|
assert data['address']['house_number'] == '208'
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_addresses(mocked_get, app, photon):
|
|
endpoint = utils.generic_endpoint_url('photon', 'addresses', slug=photon.slug)
|
|
assert endpoint == '/photon/test/addresses'
|
|
mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
|
|
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
|
data = resp.json['data'][0]
|
|
assert data['lat'] == '45.7587414'
|
|
assert data['lon'] == '4.8522272'
|
|
assert data['display_name'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
|
assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
|
assert data['id'] == 154419
|
|
assert data['address']['city'] == 'Lyon 3ème Arrondissement'
|
|
assert data['address']['postcode'] == '69003'
|
|
assert data['address']['road'] == 'Rue Garibaldi'
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_addresses_qs_page_limit(mocked_get, app, photon):
|
|
resp = app.get('/photon/%s/addresses?q=plop&page_limit=1' % photon.slug)
|
|
assert 'limit=1' in mocked_get.call_args[0][0]
|
|
|
|
resp = app.get('/photon/%s/addresses?q=plop&page_limit=100' % photon.slug)
|
|
assert 'limit=20' in mocked_get.call_args[0][0]
|
|
|
|
resp = app.get('/photon/%s/addresses?q=plop&page_limit=blabla' % photon.slug, status=400)
|
|
assert 'invalid value' in resp.json['err_desc']
|
|
|
|
|
|
def test_photon_addresses_qs_zipcode(app, photon, mock_photon_search):
|
|
resp = app.get('/photon/%s/addresses?q=plop&zipcode=69003' % photon.slug)
|
|
assert 'display_name' in resp.json['data'][0]
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_addresses_qs_coordinates(mocked_get, app, photon_coordinates):
|
|
resp = app.get('/photon/%s/addresses?q=plop' % photon_coordinates.slug)
|
|
assert 'lat=%s' % photon_coordinates.latitude in mocked_get.call_args[0][0]
|
|
assert 'lon=%s' % photon_coordinates.longitude in mocked_get.call_args[0][0]
|
|
|
|
resp = app.get('/photon/%s/addresses?q=plop&lat=42&lon=43' % photon_coordinates.slug)
|
|
assert 'lat=42' in mocked_get.call_args[0][0]
|
|
assert 'lon=43' in mocked_get.call_args[0][0]
|
|
|
|
|
|
def test_photon_addresses_cache(app, photon, mock_photon_search):
|
|
resp = app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
|
assert mock_photon_search.call['count'] == 1
|
|
|
|
data = resp.json['data'][0]
|
|
assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
|
|
|
api_id = data['id']
|
|
assert AddressCacheModel.objects.filter(api_id=api_id).exists()
|
|
assert AddressCacheModel.objects.count() == 2
|
|
|
|
resp = app.get('/photon/%s/addresses?id=%s' % (photon.slug, api_id))
|
|
assert mock_photon_search.call['count'] == 1 # no new call
|
|
assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
|
assert 'address' in data
|
|
|
|
resp = app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
|
assert AddressCacheModel.objects.count() == 2 # no new object has been created
|
|
|
|
|
|
def test_photon_addresses_cache_err(app, photon, mock_photon_search):
|
|
resp = app.get('/photon/%s/addresses?id=%s' % (photon.slug, 'wrong_id'))
|
|
assert mock_photon_search.call['count'] == 0
|
|
assert 'err' in resp.json
|
|
|
|
|
|
def test_photon_addresses_clean_cache(app, photon, freezer, mock_photon_search):
|
|
app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
|
assert AddressCacheModel.objects.count() == 2
|
|
|
|
freezer.move_to(datetime.timedelta(minutes=30))
|
|
call_command('cron', 'hourly')
|
|
assert AddressCacheModel.objects.count() == 2
|
|
|
|
freezer.move_to(datetime.timedelta(minutes=30, seconds=1))
|
|
call_command('cron', 'hourly')
|
|
assert AddressCacheModel.objects.count() == 0
|
|
|
|
app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
|
assert AddressCacheModel.objects.count() == 2
|
|
|
|
# asking for the address again resets the timestamp
|
|
freezer.move_to(datetime.timedelta(hours=1, seconds=1))
|
|
app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
|
call_command('cron', 'hourly')
|
|
assert AddressCacheModel.objects.count() == 2
|
|
|
|
freezer.move_to(datetime.timedelta(hours=1, seconds=1))
|
|
app.get('/photon/%s/addresses?id=%s' % (photon.slug, '154419'))
|
|
call_command('cron', 'hourly')
|
|
assert AddressCacheModel.objects.count() == 1
|
|
|
|
|
|
@mock.patch('passerelle.utils.Request.get')
|
|
def test_photon_addresses_data_change(mocked_get, app, photon):
|
|
endpoint = utils.generic_endpoint_url('photon', 'addresses', slug=photon.slug)
|
|
mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
|
|
|
|
# one user selects an address
|
|
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
|
data = resp.json['data'][0]
|
|
address_id, address_text = data['id'], data['text']
|
|
|
|
# another requests the same while upstream data has been updated
|
|
new_content = json.loads(FAKED_CONTENT)
|
|
new_content['features'][0]['properties']['housenumber'] = 'changed'
|
|
mocked_get.return_value = utils.FakedResponse(content=json.dumps(new_content), status_code=200)
|
|
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
|
|
|
# first user saves the form, data should not have changed
|
|
resp = app.get(endpoint, params={'id': address_id}, status=200)
|
|
assert resp.json['data'][0]['text'] == address_text
|
|
|
|
# when cache is cleared, we get the updated data
|
|
AddressCacheModel.objects.all().delete()
|
|
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
|
assert 'changed' in resp.json['data'][0]['text']
|