From 53c738abaf233f13a8d30d750a40405a07ec6bf3 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 19 May 2018 11:54:08 +0200 Subject: [PATCH] Merge leaflet_storage tests --- .gitignore | 1 + Makefile | 2 +- umap/tests/__init__.py | 1 - umap/tests/base.py | 96 ++++ umap/tests/conftest.py | 64 +++ umap/tests/fixtures/test_upload_data.csv | 2 + umap/tests/fixtures/test_upload_data.gpx | 17 + umap/tests/fixtures/test_upload_data.json | 188 ++++++++ umap/tests/fixtures/test_upload_data.kml | 33 ++ .../test_upload_empty_coordinates.json | 36 ++ .../fixtures/test_upload_missing_name.json | 153 ++++++ .../fixtures/test_upload_non_linear_ring.json | 51 ++ umap/tests/test_datalayer.py | 81 ++++ umap/tests/test_datalayer_views.py | 162 +++++++ umap/tests/test_fields.py | 43 ++ umap/tests/test_licence.py | 12 + umap/tests/test_map.py | 109 +++++ umap/tests/test_map_views.py | 449 ++++++++++++++++++ umap/tests/test_tilelayer.py | 21 + 19 files changed, 1519 insertions(+), 2 deletions(-) create mode 100644 umap/tests/base.py create mode 100644 umap/tests/conftest.py create mode 100644 umap/tests/fixtures/test_upload_data.csv create mode 100644 umap/tests/fixtures/test_upload_data.gpx create mode 100644 umap/tests/fixtures/test_upload_data.json create mode 100644 umap/tests/fixtures/test_upload_data.kml create mode 100644 umap/tests/fixtures/test_upload_empty_coordinates.json create mode 100644 umap/tests/fixtures/test_upload_missing_name.json create mode 100644 umap/tests/fixtures/test_upload_non_linear_ring.json create mode 100644 umap/tests/test_datalayer.py create mode 100644 umap/tests/test_datalayer_views.py create mode 100644 umap/tests/test_fields.py create mode 100644 umap/tests/test_licence.py create mode 100644 umap/tests/test_map.py create mode 100644 umap/tests/test_map_views.py create mode 100644 umap/tests/test_tilelayer.py diff --git a/.gitignore b/.gitignore index 60d85dc4..a9a3b7a9 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ nosetests.xml coverage.xml *,cover .hypothesis/ +.pytest_cache/ # Translations *.mo diff --git a/Makefile b/Makefile index 6d0d58cb..c02b8c8e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ test: - py.test + py.test -xv develop: python setup.py develop compilemessages: diff --git a/umap/tests/__init__.py b/umap/tests/__init__.py index 92018579..e69de29b 100644 --- a/umap/tests/__init__.py +++ b/umap/tests/__init__.py @@ -1 +0,0 @@ -from .test_views import * diff --git a/umap/tests/base.py b/umap/tests/base.py new file mode 100644 index 00000000..820425e6 --- /dev/null +++ b/umap/tests/base.py @@ -0,0 +1,96 @@ +import json + +import factory +from django.contrib.auth import get_user_model +from django.urls import reverse + +from umap.forms import DEFAULT_CENTER +from umap.models import DataLayer, Licence, Map, TileLayer + +User = get_user_model() + + +class LicenceFactory(factory.DjangoModelFactory): + name = "WTFPL" + + class Meta: + model = Licence + + +class TileLayerFactory(factory.DjangoModelFactory): + name = "Test zoom layer" + url_template = "http://{s}.test.org/{z}/{x}/{y}.png" + attribution = "Test layer attribution" + + class Meta: + model = TileLayer + + +class UserFactory(factory.DjangoModelFactory): + username = 'Joe' + email = factory.LazyAttribute( + lambda a: '{0}@example.com'.format(a.username).lower()) + password = factory.PostGenerationMethodCall('set_password', '123123') + + class Meta: + model = User + + +class MapFactory(factory.DjangoModelFactory): + name = "test map" + slug = "test-map" + center = DEFAULT_CENTER + settings = { + 'geometry': { + 'coordinates': [13.447265624999998, 48.94415123418794], + 'type': 'Point' + }, + 'properties': { + 'datalayersControl': True, + 'description': 'Which is just the Danube, at the end', + 'displayCaptionOnLoad': False, + 'displayDataBrowserOnLoad': False, + 'displayPopupFooter': False, + 'licence': '', + 'miniMap': False, + 'moreControl': True, + 'name': 'Cruising on the Donau', + 'scaleControl': True, + 'tilelayer': { + 'attribution': u'\xa9 OSM Contributors', + 'maxZoom': 18, + 'minZoom': 0, + 'url_template': 'http://{s}.osm.fr/{z}/{x}/{y}.png' + }, + 'tilelayersControl': True, + 'zoom': 7, + 'zoomControl': True + }, + 'type': 'Feature' + } + + licence = factory.SubFactory(LicenceFactory) + owner = factory.SubFactory(UserFactory) + + class Meta: + model = Map + + +class DataLayerFactory(factory.DjangoModelFactory): + map = factory.SubFactory(MapFactory) + name = "test datalayer" + description = "test description" + display_on_load = True + geojson = factory.django.FileField(data="""{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[13.68896484375,48.55297816440071]},"properties":{"_storage_options":{"color":"DarkCyan","iconClass":"Ball"},"name":"Here","description":"Da place anonymous again 755"}}],"_storage":{"displayOnLoad":true,"name":"Donau","id":926}}""") # noqa + + class Meta: + model = DataLayer + + +def login_required(response): + assert response.status_code == 200 + j = json.loads(response.content.decode()) + assert 'login_required' in j + redirect_url = reverse('login') + assert j['login_required'] == redirect_url + return True diff --git a/umap/tests/conftest.py b/umap/tests/conftest.py new file mode 100644 index 00000000..441513f7 --- /dev/null +++ b/umap/tests/conftest.py @@ -0,0 +1,64 @@ +import shutil +import tempfile + +import pytest +from django.core.signing import get_cookie_signer + +from .base import (DataLayerFactory, LicenceFactory, MapFactory, + TileLayerFactory, UserFactory) + +TMP_ROOT = tempfile.mkdtemp() + + +def pytest_configure(config): + from django.conf import settings + settings.MEDIA_ROOT = TMP_ROOT + + +def pytest_unconfigure(config): + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + +@pytest.fixture +def user(): + return UserFactory(password="123123") + + +@pytest.fixture +def licence(): + return LicenceFactory() + + +@pytest.fixture +def map(licence, tilelayer): + user = UserFactory(username="Gabriel", password="123123") + return MapFactory(owner=user, licence=licence) + + +@pytest.fixture +def anonymap(map): + map.owner = None + map.save() + return map + + +@pytest.fixture +def cookieclient(client, anonymap): + key, value = anonymap.signed_cookie_elements + client.cookies[key] = get_cookie_signer(salt=key).sign(value) + return client + + +@pytest.fixture +def allow_anonymous(settings): + settings.LEAFLET_STORAGE_ALLOW_ANONYMOUS = True + + +@pytest.fixture +def datalayer(map): + return DataLayerFactory(map=map, name="Default Datalayer") + + +@pytest.fixture +def tilelayer(): + return TileLayerFactory() diff --git a/umap/tests/fixtures/test_upload_data.csv b/umap/tests/fixtures/test_upload_data.csv new file mode 100644 index 00000000..107f2510 --- /dev/null +++ b/umap/tests/fixtures/test_upload_data.csv @@ -0,0 +1,2 @@ +Foo,Latitude,geo_Longitude,title,description +bar,41.34,122.86,a point somewhere,the description of this point \ No newline at end of file diff --git a/umap/tests/fixtures/test_upload_data.gpx b/umap/tests/fixtures/test_upload_data.gpx new file mode 100644 index 00000000..ea7e1080 --- /dev/null +++ b/umap/tests/fixtures/test_upload_data.gpx @@ -0,0 +1,17 @@ + + 1374Simple PointSimple description + + Simple path + Simple description + + + + + + + \ No newline at end of file diff --git a/umap/tests/fixtures/test_upload_data.json b/umap/tests/fixtures/test_upload_data.json new file mode 100644 index 00000000..8d7fc002 --- /dev/null +++ b/umap/tests/fixtures/test_upload_data.json @@ -0,0 +1,188 @@ +{ + "crs": null, + "type": "FeatureCollection", + "features": [ + { + "geometry": { + "type": "Point", + "coordinates": [ + -0.1318359375, + 51.474540439419755 + ] + }, + "type": "Feature", + "properties": { + "name": "London", + "description": "London description", + "color": "Pink" + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 4.350585937499999, + 51.26878915771344 + ] + }, + "type": "Feature", + "properties": { + "name": "Antwerpen", + "description": "" + } + }, + { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 2.4005126953125, + 48.81228985866255 + ], + [ + 2.78228759765625, + 48.89903236496008 + ], + [ + 2.845458984375, + 48.89903236496008 + ], + [ + 2.86468505859375, + 48.96218736991556 + ], + [ + 2.9278564453125, + 48.93693495409401 + ], + [ + 2.93060302734375, + 48.99283383694349 + ], + [ + 3.04046630859375, + 49.01085236926211 + ], + [ + 3.0157470703125, + 48.96038404976431 + ], + [ + 3.12286376953125, + 48.94415123418794 + ], + [ + 3.1805419921874996, + 48.99824008113872 + ], + [ + 3.2684326171875, + 48.95497369808868 + ], + [ + 3.53759765625, + 49.0900564769189 + ], + [ + 3.57330322265625, + 49.057670047140604 + ], + [ + 3.72161865234375, + 49.095452162534826 + ], + [ + 3.9578247070312496, + 49.06486885623368 + ] + ] + }, + "type": "Feature", + "properties": { + "name": "2011" + } + }, + { + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 10.04150390625, + 52.70967533219883 + ], + [ + 8.800048828125, + 51.80182150078305 + ], + [ + 11.271972656249998, + 51.12421275782688 + ], + [ + 12.689208984375, + 52.214338608258196 + ], + [ + 10.04150390625, + 52.70967533219883 + ] + ] + ] + }, + "type": "Feature", + "properties": { + "name": "test" + } + }, + { + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 7.327880859374999, + 50.52041218671901 + ], + [ + 6.064453125, + 49.6462914122132 + ], + [ + 7.503662109375, + 49.54659778073743 + ], + [ + 6.8115234375, + 49.167338606291075 + ], + [ + 9.635009765625, + 48.99463598353408 + ], + [ + 10.557861328125, + 49.937079756975294 + ], + [ + 8.4814453125, + 49.688954878870305 + ], + [ + 9.173583984375, + 51.04830113331224 + ], + [ + 7.327880859374999, + 50.52041218671901 + ] + ] + ] + }, + "type": "Feature", + "properties": { + "name": "test polygon 2" + } + } + ] +} \ No newline at end of file diff --git a/umap/tests/fixtures/test_upload_data.kml b/umap/tests/fixtures/test_upload_data.kml new file mode 100644 index 00000000..af857078 --- /dev/null +++ b/umap/tests/fixtures/test_upload_data.kml @@ -0,0 +1,33 @@ + + + + Simple point + Here is a simple description. + + -122.0822035425683,37.42228990140251,0 + + + + Simple path + Simple description + + -112.2550785337791,36.07954952145647,2357 -112.2549277039738,36.08117083492122,2357 -112.2552505069063,36.08260761307279,2357 + + + + Simple polygon + A description. + + + + + -77.05788457660967,38.87253259892824,100 + -77.05465973756702,38.87291016281703,100 + -77.05315536854791,38.87053267794386,100 + -77.05788457660967,38.87253259892824,100 + + + + + + \ No newline at end of file diff --git a/umap/tests/fixtures/test_upload_empty_coordinates.json b/umap/tests/fixtures/test_upload_empty_coordinates.json new file mode 100644 index 00000000..65f8dd16 --- /dev/null +++ b/umap/tests/fixtures/test_upload_empty_coordinates.json @@ -0,0 +1,36 @@ +{ + "crs": null, + "type": "FeatureCollection", + "features": [ + { + "geometry": { + "type": "Point", + "coordinates": [] + }, + "type": "Feature", + "properties": { + "name": "London" + } + }, + { + "geometry": { + "type": "LineString", + "coordinates": [] + }, + "type": "Feature", + "properties": { + "name": "2011" + } + }, + { + "geometry": { + "type": "Polygon", + "coordinates": [[]] + }, + "type": "Feature", + "properties": { + "name": "test" + } + } + ] +} \ No newline at end of file diff --git a/umap/tests/fixtures/test_upload_missing_name.json b/umap/tests/fixtures/test_upload_missing_name.json new file mode 100644 index 00000000..e4e4acbf --- /dev/null +++ b/umap/tests/fixtures/test_upload_missing_name.json @@ -0,0 +1,153 @@ +{ + "crs": null, + "type": "FeatureCollection", + "features": [ + { + "geometry": { + "type": "Point", + "coordinates": [ + -0.1318359375, + 51.474540439419755 + ] + }, + "type": "Feature", + "properties": { + "name": "London" + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 4.350585937499999, + 51.26878915771344 + ] + }, + "type": "Feature", + "properties": { + "noname": "this feature is missing a name", + "name": null + } + }, + { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 2.4005126953125, + 48.81228985866255 + ], + [ + 2.78228759765625, + 48.89903236496008 + ], + [ + 2.845458984375, + 48.89903236496008 + ], + [ + 2.86468505859375, + 48.96218736991556 + ], + [ + 2.9278564453125, + 48.93693495409401 + ], + [ + 2.93060302734375, + 48.99283383694349 + ], + [ + 3.04046630859375, + 49.01085236926211 + ], + [ + 3.0157470703125, + 48.96038404976431 + ], + [ + 3.12286376953125, + 48.94415123418794 + ], + [ + 3.1805419921874996, + 48.99824008113872 + ], + [ + 3.2684326171875, + 48.95497369808868 + ], + [ + 3.53759765625, + 49.0900564769189 + ], + [ + 3.57330322265625, + 49.057670047140604 + ], + [ + 3.72161865234375, + 49.095452162534826 + ], + [ + 3.9578247070312496, + 49.06486885623368 + ] + ] + }, + "type": "Feature", + "properties": { + "name": "2011" + } + }, + { + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 7.327880859374999, + 50.52041218671901 + ], + [ + 6.064453125, + 49.6462914122132 + ], + [ + 7.503662109375, + 49.54659778073743 + ], + [ + 6.8115234375, + 49.167338606291075 + ], + [ + 9.635009765625, + 48.99463598353408 + ], + [ + 10.557861328125, + 49.937079756975294 + ], + [ + 8.4814453125, + 49.688954878870305 + ], + [ + 9.173583984375, + 51.04830113331224 + ], + [ + 7.327880859374999, + 50.52041218671901 + ] + ] + ] + }, + "type": "Feature", + "properties": { + "name": "test polygon 2" + } + } + ] +} \ No newline at end of file diff --git a/umap/tests/fixtures/test_upload_non_linear_ring.json b/umap/tests/fixtures/test_upload_non_linear_ring.json new file mode 100644 index 00000000..db22bb0b --- /dev/null +++ b/umap/tests/fixtures/test_upload_non_linear_ring.json @@ -0,0 +1,51 @@ +{ + "crs": null, + "type": "FeatureCollection", + "features": [ + { + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 7.327880859374999, + 50.52041218671901 + ], + [ + 6.064453125, + 49.6462914122132 + ], + [ + 7.503662109375, + 49.54659778073743 + ], + [ + 6.8115234375, + 49.167338606291075 + ], + [ + 9.635009765625, + 48.99463598353408 + ], + [ + 10.557861328125, + 49.937079756975294 + ], + [ + 8.4814453125, + 49.688954878870305 + ], + [ + 9.173583984375, + 51.04830113331224 + ] + ] + ] + }, + "type": "Feature", + "properties": { + "name": "non linear ring" + } + } + ] +} \ No newline at end of file diff --git a/umap/tests/test_datalayer.py b/umap/tests/test_datalayer.py new file mode 100644 index 00000000..65f8e2f7 --- /dev/null +++ b/umap/tests/test_datalayer.py @@ -0,0 +1,81 @@ +import os + +import pytest +from django.core.files.base import ContentFile + +from .base import DataLayerFactory, MapFactory + +pytestmark = pytest.mark.django_db + + +def test_datalayers_should_be_ordered_by_rank(map, datalayer): + datalayer.rank = 5 + datalayer.save() + c4 = DataLayerFactory(map=map, rank=4) + c1 = DataLayerFactory(map=map, rank=1) + c3 = DataLayerFactory(map=map, rank=3) + c2 = DataLayerFactory(map=map, rank=2) + assert list(map.datalayer_set.all()) == [c1, c2, c3, c4, datalayer] + + +def test_upload_to(map, datalayer): + map.pk = 302 + datalayer.pk = 17 + assert datalayer.upload_to().startswith('datalayer/2/0/302/17_') + + +def test_save_should_use_pk_as_name(map, datalayer): + assert "/{}_".format(datalayer.pk) in datalayer.geojson.name + + +def test_same_geojson_file_name_will_be_suffixed(map, datalayer): + before = datalayer.geojson.name + datalayer.geojson.save(before, ContentFile("{}")) + assert datalayer.geojson.name != before + assert "/{}_".format(datalayer.pk) in datalayer.geojson.name + + +def test_clone_should_return_new_instance(map, datalayer): + clone = datalayer.clone() + assert datalayer.pk != clone.pk + assert datalayer.name == clone.name + assert datalayer.map == clone.map + + +def test_clone_should_update_map_if_passed(datalayer, user, licence): + map = MapFactory(owner=user, licence=licence) + clone = datalayer.clone(map_inst=map) + assert datalayer.pk != clone.pk + assert datalayer.name == clone.name + assert datalayer.map != clone.map + assert map == clone.map + + +def test_clone_should_clone_geojson_too(datalayer): + clone = datalayer.clone() + assert datalayer.pk != clone.pk + assert clone.geojson is not None + assert clone.geojson.path != datalayer.geojson.path + + +def test_should_remove_old_versions_on_save(datalayer, map, settings): + settings.LEAFLET_STORAGE_KEEP_VERSIONS = 3 + root = datalayer.storage_root() + before = len(datalayer.geojson.storage.listdir(root)[1]) + newer = '%s/%s_1440924889.geojson' % (root, datalayer.pk) + medium = '%s/%s_1440923687.geojson' % (root, datalayer.pk) + older = '%s/%s_1440918637.geojson' % (root, datalayer.pk) + for path in [medium, newer, older]: + datalayer.geojson.storage.save(path, ContentFile("{}")) + datalayer.geojson.storage.save(path + '.gz', ContentFile("{}")) + assert len(datalayer.geojson.storage.listdir(root)[1]) == 6 + before + datalayer.save() + files = datalayer.geojson.storage.listdir(root)[1] + assert len(files) == 5 + assert os.path.basename(newer) in files + assert os.path.basename(newer + '.gz') in files + assert os.path.basename(medium) in files + assert os.path.basename(medium + '.gz') in files + assert os.path.basename(datalayer.geojson.path) in files + assert os.path.basename(older) not in files + assert os.path.basename(older + '.gz') not in files diff --git a/umap/tests/test_datalayer_views.py b/umap/tests/test_datalayer_views.py new file mode 100644 index 00000000..b4f90f73 --- /dev/null +++ b/umap/tests/test_datalayer_views.py @@ -0,0 +1,162 @@ +import json + +import pytest +from django.core.files.base import ContentFile +from django.urls import reverse + +from umap.models import DataLayer, Map + +from .base import MapFactory + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def post_data(): + return { + "name": 'name', + "display_on_load": True, + "rank": 0, + "geojson": '{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.1640625,53.014783245859235],[-3.1640625,51.86292391360244],[-0.50537109375,51.385495069223204],[1.16455078125,52.38901106223456],[-0.41748046875,53.91728101547621],[-2.109375,53.85252660044951],[-3.1640625,53.014783245859235]]]},"properties":{"_storage_options":{},"name":"Ho god, sounds like a polygouine"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[1.8017578124999998,51.16556659836182],[-0.48339843749999994,49.710272582105695],[-3.1640625,50.0923932109388],[-5.60302734375,51.998410382390325]]},"properties":{"_storage_options":{},"name":"Light line"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[0.63720703125,51.15178610143037]},"properties":{"_storage_options":{},"name":"marker he"}}],"_storage":{"displayOnLoad":true,"name":"new name","id":1668,"remoteData":{},"color":"LightSeaGreen","description":"test"}}' # noqa + } + + +def test_get(client, settings, datalayer): + url = reverse('datalayer_view', args=(datalayer.pk, )) + response = client.get(url) + if getattr(settings, 'LEAFLET_STORAGE_XSENDFILE_HEADER', None): + assert response['ETag'] is not None + assert response['Last-Modified'] is not None + assert response['Cache-Control'] is not None + assert 'Content-Encoding' not in response + j = json.loads(response.content.decode()) + assert '_storage' in j + assert 'features' in j + assert j['type'] == 'FeatureCollection' + + +def test_update(client, datalayer, map, post_data): + url = reverse('datalayer_update', args=(map.pk, datalayer.pk)) + client.login(username=map.owner.username, password="123123") + name = 'new name' + rank = 2 + post_data['name'] = name + post_data['rank'] = rank + response = client.post(url, post_data, follow=True) + assert response.status_code == 200 + modified_datalayer = DataLayer.objects.get(pk=datalayer.pk) + assert modified_datalayer.name == name + assert modified_datalayer.rank == rank + # Test response is a json + j = json.loads(response.content.decode()) + assert "id" in j + assert datalayer.pk == j['id'] + + +def test_should_not_be_possible_to_update_with_wrong_map_id_in_url(client, datalayer, map, post_data): # noqa + other_map = MapFactory(owner=map.owner) + url = reverse('datalayer_update', args=(other_map.pk, datalayer.pk)) + client.login(username=map.owner.username, password="123123") + name = 'new name' + post_data['name'] = name + response = client.post(url, post_data, follow=True) + assert response.status_code == 403 + modified_datalayer = DataLayer.objects.get(pk=datalayer.pk) + assert modified_datalayer.name == datalayer.name + + +def test_delete(client, datalayer, map): + url = reverse('datalayer_delete', args=(map.pk, datalayer.pk)) + client.login(username=map.owner.username, password='123123') + response = client.post(url, {}, follow=True) + assert response.status_code == 200 + assert not DataLayer.objects.filter(pk=datalayer.pk).count() + # Check that map has not been impacted + assert Map.objects.filter(pk=map.pk).exists() + # Test response is a json + j = json.loads(response.content.decode()) + assert 'info' in j + + +def test_should_not_be_possible_to_delete_with_wrong_map_id_in_url(client, datalayer, map): # noqa + other_map = MapFactory(owner=map.owner) + url = reverse('datalayer_delete', args=(other_map.pk, datalayer.pk)) + client.login(username=map.owner.username, password='123123') + response = client.post(url, {}, follow=True) + assert response.status_code == 403 + assert DataLayer.objects.filter(pk=datalayer.pk).exists() + + +def test_get_gzipped(client, datalayer, settings): + url = reverse('datalayer_view', args=(datalayer.pk, )) + response = client.get(url, HTTP_ACCEPT_ENCODING='gzip') + if getattr(settings, 'LEAFLET_STORAGE_XSENDFILE_HEADER', None): + assert response['ETag'] is not None + assert response['Last-Modified'] is not None + assert response['Cache-Control'] is not None + assert response['Content-Encoding'] == 'gzip' + + +def test_optimistic_concurrency_control_with_good_etag(client, datalayer, map, post_data): # noqa + # Get Etag + url = reverse('datalayer_view', args=(datalayer.pk, )) + response = client.get(url) + etag = response['ETag'] + url = reverse('datalayer_update', + args=(map.pk, datalayer.pk)) + client.login(username=map.owner.username, password="123123") + name = 'new name' + post_data['name'] = 'new name' + response = client.post(url, post_data, follow=True, HTTP_IF_MATCH=etag) + assert response.status_code == 200 + modified_datalayer = DataLayer.objects.get(pk=datalayer.pk) + assert modified_datalayer.name == name + + +def test_optimistic_concurrency_control_with_bad_etag(client, datalayer, map, post_data): # noqa + url = reverse('datalayer_update', args=(map.pk, datalayer.pk)) + client.login(username=map.owner.username, password='123123') + name = 'new name' + post_data['name'] = name + response = client.post(url, post_data, follow=True, HTTP_IF_MATCH='xxx') + assert response.status_code == 412 + modified_datalayer = DataLayer.objects.get(pk=datalayer.pk) + assert modified_datalayer.name != name + + +def test_optimistic_concurrency_control_with_empty_etag(client, datalayer, map, post_data): # noqa + url = reverse('datalayer_update', args=(map.pk, datalayer.pk)) + client.login(username=map.owner.username, password='123123') + name = 'new name' + post_data['name'] = name + response = client.post(url, post_data, follow=True, HTTP_IF_MATCH=None) + assert response.status_code == 200 + modified_datalayer = DataLayer.objects.get(pk=datalayer.pk) + assert modified_datalayer.name == name + + +def test_versions_should_return_versions(client, datalayer, map, settings): + root = datalayer.storage_root() + datalayer.geojson.storage.save( + '%s/%s_1440924889.geojson' % (root, datalayer.pk), + ContentFile("{}")) + datalayer.geojson.storage.save( + '%s/%s_1440923687.geojson' % (root, datalayer.pk), + ContentFile("{}")) + datalayer.geojson.storage.save( + '%s/%s_1440918637.geojson' % (root, datalayer.pk), + ContentFile("{}")) + url = reverse('datalayer_versions', args=(datalayer.pk, )) + versions = json.loads(client.get(url).content.decode()) + assert len(versions['versions']) == 4 + version = {'name': '%s_1440918637.geojson' % datalayer.pk, 'size': 2, + 'at': '1440918637'} + assert version in versions['versions'] + + +def test_version_should_return_one_version_geojson(client, datalayer, map): + root = datalayer.storage_root() + name = '%s_1440924889.geojson' % datalayer.pk + datalayer.geojson.storage.save('%s/%s' % (root, name), ContentFile("{}")) + url = reverse('datalayer_version', args=(datalayer.pk, name)) + assert client.get(url).content.decode() == "{}" diff --git a/umap/tests/test_fields.py b/umap/tests/test_fields.py new file mode 100644 index 00000000..4d744cd3 --- /dev/null +++ b/umap/tests/test_fields.py @@ -0,0 +1,43 @@ +import json + +import pytest + +from umap.models import Map + +pytestmark = pytest.mark.django_db + + +def test_can_use_dict(map): + d = {'locateControl': True} + map.settings = d + map.save() + assert Map.objects.get(pk=map.pk).settings == d + + +def test_can_set_item(map): + d = {'locateControl': True} + map.settings = d + map.save() + map_inst = Map.objects.get(pk=map.pk) + map_inst.settings['color'] = 'DarkGreen' + assert map_inst.settings['locateControl'] is True + + +def test_should_return_a_dict_if_none(map): + map.settings = None + map.save() + assert Map.objects.get(pk=map.pk).settings == {} + + +def test_should_not_double_dumps(map): + map.settings = '{"locate": true}' + map.save() + assert Map.objects.get(pk=map.pk).settings == {'locate': True} + + +def test_value_to_string(map): + d = {'locateControl': True} + map.settings = d + map.save() + field = Map._meta.get_field('settings') + assert json.loads(field.value_to_string(map)) == d diff --git a/umap/tests/test_licence.py b/umap/tests/test_licence.py new file mode 100644 index 00000000..fd5e606b --- /dev/null +++ b/umap/tests/test_licence.py @@ -0,0 +1,12 @@ +import pytest + +from umap.models import DataLayer, Map + +pytestmark = pytest.mark.django_db + + +def test_licence_delete_should_not_remove_linked_maps(map, licence, datalayer): + assert map.licence == licence + licence.delete() + assert Map.objects.filter(pk=map.pk).exists() + assert DataLayer.objects.filter(pk=datalayer.pk).exists() diff --git a/umap/tests/test_map.py b/umap/tests/test_map.py new file mode 100644 index 00000000..c72d3d1f --- /dev/null +++ b/umap/tests/test_map.py @@ -0,0 +1,109 @@ +import pytest +from django.contrib.auth.models import AnonymousUser +from django.urls import reverse + +from umap.models import Map + +from .base import MapFactory + +pytestmark = pytest.mark.django_db + + +def test_anonymous_can_edit_if_status_anonymous(map): + anonymous = AnonymousUser() + map.edit_status = map.ANONYMOUS + map.save() + assert map.can_edit(anonymous) + + +def test_anonymous_cannot_edit_if_not_status_anonymous(map): + anonymous = AnonymousUser() + map.edit_status = map.OWNER + map.save() + assert not map.can_edit(anonymous) + + +def test_non_editors_can_edit_if_status_anonymous(map, user): + assert map.owner != user + map.edit_status = map.ANONYMOUS + map.save() + assert map.can_edit(user) + + +def test_non_editors_cannot_edit_if_not_status_anonymous(map, user): + map.edit_status = map.OWNER + map.save() + assert not map.can_edit(user) + + +def test_editors_cannot_edit_if_status_owner(map, user): + map.edit_status = map.OWNER + map.editors.add(user) + map.save() + assert not map.can_edit(user) + + +def test_editors_can_edit_if_status_editors(map, user): + map.edit_status = map.EDITORS + map.editors.add(user) + map.save() + assert map.can_edit(user) + + +def test_logged_in_user_should_be_allowed_for_anonymous_map_with_anonymous_edit_status(map, user, rf): # noqa + map.owner = None + map.edit_status = map.ANONYMOUS + map.save() + url = reverse('map_update', kwargs={'map_id': map.pk}) + request = rf.get(url) + request.user = user + assert map.can_edit(user, request) + + +def test_clone_should_return_new_instance(map, user): + clone = map.clone() + assert map.pk != clone.pk + assert u"Clone of " + map.name == clone.name + assert map.settings == clone.settings + assert map.center == clone.center + assert map.zoom == clone.zoom + assert map.licence == clone.licence + assert map.tilelayer == clone.tilelayer + + +def test_clone_should_keep_editors(map, user): + map.editors.add(user) + clone = map.clone() + assert map.pk != clone.pk + assert user in map.editors.all() + assert user in clone.editors.all() + + +def test_clone_should_update_owner_if_passed(map, user): + clone = map.clone(owner=user) + assert map.pk != clone.pk + assert map.owner != clone.owner + assert user == clone.owner + + +def test_clone_should_clone_datalayers_and_features_too(map, user, datalayer): + clone = map.clone() + assert map.pk != clone.pk + assert map.datalayer_set.count() == 1 + assert clone.datalayer_set.count() == 1 + other = clone.datalayer_set.all()[0] + assert datalayer in map.datalayer_set.all() + assert other.pk != datalayer.pk + assert other.name == datalayer.name + assert other.geojson is not None + assert other.geojson.path != datalayer.geojson.path + + +def test_publicmanager_should_get_only_public_maps(map, user, licence): + map.share_status = map.PUBLIC + open_map = MapFactory(owner=user, licence=licence, share_status=Map.OPEN) + private_map = MapFactory(owner=user, licence=licence, + share_status=Map.PRIVATE) + assert map in Map.public.all() + assert open_map not in Map.public.all() + assert private_map not in Map.public.all() diff --git a/umap/tests/test_map_views.py b/umap/tests/test_map_views.py new file mode 100644 index 00000000..37241776 --- /dev/null +++ b/umap/tests/test_map_views.py @@ -0,0 +1,449 @@ +import json + +import pytest +from django.contrib.auth import get_user_model +from django.urls import reverse + +from umap.models import DataLayer, Map + +from .base import login_required + +pytestmark = pytest.mark.django_db +User = get_user_model() + + +@pytest.fixture +def post_data(): + return { + 'name': 'name', + 'center': '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa + 'settings': '{"type":"Feature","geometry":{"type":"Point","coordinates":[5.0592041015625,52.05924589011585]},"properties":{"tilelayer":{"maxZoom":20,"url_template":"http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png","minZoom":0,"attribution":"HOT and friends"},"licence":"","description":"","name":"test enrhûmé","tilelayersControl":true,"displayDataBrowserOnLoad":false,"displayPopupFooter":true,"displayCaptionOnLoad":false,"miniMap":true,"moreControl":true,"scaleControl":true,"zoomControl":true,"datalayersControl":true,"zoom":8}}' # noqa + } + + +def test_create(client, user, post_data): + url = reverse('map_create') + # POST only mendatory fields + name = 'test-map-with-new-name' + post_data['name'] = name + client.login(username=user.username, password="123123") + response = client.post(url, post_data) + assert response.status_code == 200 + j = json.loads(response.content.decode()) + created_map = Map.objects.latest('pk') + assert j['id'] == created_map.pk + assert created_map.name == name + + +def test_map_create_permissions(client, settings): + settings.LEAFLET_STORAGE_ALLOW_ANONYMOUS = False + url = reverse('map_create') + # POST anonymous + response = client.post(url, {}) + assert login_required(response) + + +def test_map_update_access(client, map, user): + url = reverse('map_update', kwargs={'map_id': map.pk}) + # GET anonymous + response = client.get(url) + assert login_required(response) + # POST anonymous + response = client.post(url, {}) + assert login_required(response) + # GET with wrong permissions + client.login(username=user.username, password="123123") + response = client.get(url) + assert response.status_code == 403 + # POST with wrong permissions + client.login(username=user.username, password="123123") + response = client.post(url, {}) + assert response.status_code == 403 + + +def test_map_update_permissions_access(client, map, user): + url = reverse('map_update_permissions', kwargs={'map_id': map.pk}) + # GET anonymous + response = client.get(url) + assert login_required(response) + # POST anonymous + response = client.post(url, {}) + assert login_required(response) + # GET with wrong permissions + client.login(username=user.username, password="123123") + response = client.get(url) + assert response.status_code == 403 + # POST with wrong permissions + client.login(username=user.username, password="123123") + response = client.post(url, {}) + assert response.status_code == 403 + + +def test_update(client, map, post_data): + url = reverse('map_update', kwargs={'map_id': map.pk}) + # POST only mendatory fields + name = 'new map name' + post_data['name'] = name + client.login(username=map.owner.username, password="123123") + response = client.post(url, post_data) + assert response.status_code == 200 + j = json.loads(response.content.decode()) + assert 'html' not in j + updated_map = Map.objects.get(pk=map.pk) + assert j['id'] == updated_map.pk + assert updated_map.name == name + + +def test_delete(client, map, datalayer): + url = reverse('map_delete', args=(map.pk, )) + client.login(username=map.owner.username, password="123123") + response = client.post(url, {}, follow=True) + assert response.status_code == 200 + assert not Map.objects.filter(pk=map.pk).exists() + assert not DataLayer.objects.filter(pk=datalayer.pk).exists() + # Check that user has not been impacted + assert User.objects.filter(pk=map.owner.pk).exists() + # Test response is a json + j = json.loads(response.content.decode()) + assert 'redirect' in j + + +def test_wrong_slug_should_redirect_to_canonical(client, map): + url = reverse('map', kwargs={'pk': map.pk, 'slug': 'wrong-slug'}) + canonical = reverse('map', kwargs={'pk': map.pk, + 'slug': map.slug}) + response = client.get(url) + assert response.status_code == 301 + assert response['Location'] == canonical + + +def test_wrong_slug_should_redirect_with_query_string(client, map): + url = reverse('map', kwargs={'pk': map.pk, 'slug': 'wrong-slug'}) + url = "{}?allowEdit=0".format(url) + canonical = reverse('map', kwargs={'pk': map.pk, + 'slug': map.slug}) + canonical = "{}?allowEdit=0".format(canonical) + response = client.get(url) + assert response.status_code == 301 + assert response['Location'] == canonical + + +def test_should_not_consider_the_query_string_for_canonical_check(client, map): + url = reverse('map', kwargs={'pk': map.pk, 'slug': map.slug}) + url = "{}?allowEdit=0".format(url) + response = client.get(url) + assert response.status_code == 200 + + +def test_short_url_should_redirect_to_canonical(client, map): + url = reverse('map_short_url', kwargs={'pk': map.pk}) + canonical = reverse('map', kwargs={'pk': map.pk, + 'slug': map.slug}) + response = client.get(url) + assert response.status_code == 301 + assert response['Location'] == canonical + + +def test_old_url_should_redirect_to_canonical(client, map): + url = reverse( + 'map_old_url', + kwargs={'username': map.owner.username, 'slug': map.slug} + ) + canonical = reverse('map', kwargs={'pk': map.pk, + 'slug': map.slug}) + response = client.get(url) + assert response.status_code == 301 + assert response['Location'] == canonical + + +def test_clone_map_should_create_a_new_instance(client, map): + assert Map.objects.count() == 1 + url = reverse('map_clone', kwargs={'map_id': map.pk}) + client.login(username=map.owner.username, password="123123") + response = client.post(url) + assert response.status_code == 200 + assert Map.objects.count() == 2 + clone = Map.objects.latest('pk') + assert clone.pk != map.pk + assert clone.name == u"Clone of " + map.name + + +def test_user_not_allowed_should_not_clone_map(client, map, user, settings): + settings.LEAFLET_STORAGE_ALLOW_ANONYMOUS = False + assert Map.objects.count() == 1 + url = reverse('map_clone', kwargs={'map_id': map.pk}) + map.edit_status = map.OWNER + map.save() + response = client.post(url) + assert login_required(response) + client.login(username=user.username, password="123123") + response = client.post(url) + assert response.status_code == 403 + map.edit_status = map.ANONYMOUS + map.save() + client.logout() + response = client.post(url) + assert response.status_code == 403 + assert Map.objects.count() == 1 + + +def test_clone_should_set_cloner_as_owner(client, map, user): + url = reverse('map_clone', kwargs={'map_id': map.pk}) + map.edit_status = map.EDITORS + map.editors.add(user) + map.save() + client.login(username=user.username, password="123123") + response = client.post(url) + assert response.status_code == 200 + assert Map.objects.count() == 2 + clone = Map.objects.latest('pk') + assert clone.pk != map.pk + assert clone.name == u"Clone of " + map.name + assert clone.owner == user + + +def test_map_creation_should_allow_unicode_names(client, map, post_data): + url = reverse('map_create') + # POST only mendatory fields + name = u'Академический' + post_data['name'] = name + client.login(username=map.owner.username, password="123123") + response = client.post(url, post_data) + assert response.status_code == 200 + j = json.loads(response.content.decode()) + created_map = Map.objects.latest('pk') + assert j['id'] == created_map.pk + assert created_map.name == name + # Lower case of the russian original name + # self.assertEqual(created_map.slug, u"академический") + # for now we fallback to "map", see unicode_name branch + assert created_map.slug == 'map' + + +def test_anonymous_can_access_map_with_share_status_public(client, map): + url = reverse('map', args=(map.slug, map.pk)) + map.share_status = map.PUBLIC + map.save() + response = client.get(url) + assert response.status_code == 200 + + +def test_anonymous_can_access_map_with_share_status_open(client, map): + url = reverse('map', args=(map.slug, map.pk)) + map.share_status = map.OPEN + map.save() + response = client.get(url) + assert response.status_code == 200 + + +def test_anonymous_cannot_access_map_with_share_status_private(client, map): + url = reverse('map', args=(map.slug, map.pk)) + map.share_status = map.PRIVATE + map.save() + response = client.get(url) + assert response.status_code == 403 + + +def test_owner_can_access_map_with_share_status_private(client, map): + url = reverse('map', args=(map.slug, map.pk)) + map.share_status = map.PRIVATE + map.save() + client.login(username=map.owner.username, password="123123") + response = client.get(url) + assert response.status_code == 200 + + +def test_editors_can_access_map_with_share_status_private(client, map, user): + url = reverse('map', args=(map.slug, map.pk)) + map.share_status = map.PRIVATE + map.editors.add(user) + map.save() + client.login(username=user.username, password="123123") + response = client.get(url) + assert response.status_code == 200 + + +def test_non_editor_cannot_access_map_if_share_status_private(client, map, user): # noqa + url = reverse('map', args=(map.slug, map.pk)) + map.share_status = map.PRIVATE + map.save() + client.login(username=user.username, password="123123") + response = client.get(url) + assert response.status_code == 403 + + +def test_map_geojson_view(client, map): + url = reverse('map_geojson', args=(map.pk, )) + response = client.get(url) + j = json.loads(response.content.decode()) + assert 'type' in j + + +def test_only_owner_can_delete(client, map, user): + map.editors.add(user) + url = reverse('map_delete', kwargs={'map_id': map.pk}) + client.login(username=user.username, password="123123") + response = client.post(url, {}, follow=True) + assert response.status_code == 403 + + +def test_map_editors_do_not_see_owner_change_input(client, map, user): + map.editors.add(user) + map.edit_status = map.EDITORS + map.save() + url = reverse('map_update_permissions', kwargs={'map_id': map.pk}) + client.login(username=user.username, password="123123") + response = client.get(url) + assert 'id_owner' not in response + + +def test_logged_in_user_can_edit_map_editable_by_anonymous(client, map, user): + map.owner = None + map.edit_status = map.ANONYMOUS + map.save() + client.login(username=user.username, password="123123") + url = reverse('map_update', kwargs={'map_id': map.pk}) + new_name = 'this is my new name' + data = { + 'center': '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa + 'name': new_name + } + response = client.post(url, data) + assert response.status_code == 200 + assert Map.objects.get(pk=map.pk).name == new_name + + +@pytest.mark.usefixtures('allow_anonymous') +def test_anonymous_create(cookieclient, post_data): + url = reverse('map_create') + # POST only mendatory fields + name = 'test-map-with-new-name' + post_data['name'] = name + response = cookieclient.post(url, post_data) + assert response.status_code == 200 + j = json.loads(response.content.decode()) + created_map = Map.objects.latest('pk') + assert j['id'] == created_map.pk + assert created_map.name == name + key, value = created_map.signed_cookie_elements + assert key in cookieclient.cookies + + +@pytest.mark.usefixtures('allow_anonymous') +def test_anonymous_update_without_cookie_fails(client, anonymap, post_data): # noqa + url = reverse('map_update', kwargs={'map_id': anonymap.pk}) + response = client.post(url, post_data) + assert response.status_code == 403 + + +@pytest.mark.usefixtures('allow_anonymous') +def test_anonymous_update_with_cookie_should_work(cookieclient, anonymap, post_data): # noqa + url = reverse('map_update', kwargs={'map_id': anonymap.pk}) + # POST only mendatory fields + name = 'new map name' + post_data['name'] = name + response = cookieclient.post(url, post_data) + assert response.status_code == 200 + j = json.loads(response.content.decode()) + updated_map = Map.objects.get(pk=anonymap.pk) + assert j['id'] == updated_map.pk + + +@pytest.mark.usefixtures('allow_anonymous') +def test_anonymous_delete(cookieclient, anonymap): + url = reverse('map_delete', args=(anonymap.pk, )) + response = cookieclient.post(url, {}, follow=True) + assert response.status_code == 200 + assert not Map.objects.filter(pk=anonymap.pk).count() + # Test response is a json + j = json.loads(response.content.decode()) + assert 'redirect' in j + + +@pytest.mark.usefixtures('allow_anonymous') +def test_no_cookie_cant_delete(client, anonymap): + url = reverse('map_delete', args=(anonymap.pk, )) + response = client.post(url, {}, follow=True) + assert response.status_code == 403 + + +@pytest.mark.usefixtures('allow_anonymous') +def test_anonymous_edit_url(cookieclient, anonymap): + url = anonymap.get_anonymous_edit_url() + canonical = reverse('map', kwargs={'pk': anonymap.pk, + 'slug': anonymap.slug}) + response = cookieclient.get(url) + assert response.status_code == 302 + assert response['Location'] == canonical + key, value = anonymap.signed_cookie_elements + assert key in cookieclient.cookies + + +@pytest.mark.usefixtures('allow_anonymous') +def test_bad_anonymous_edit_url_should_return_403(cookieclient, anonymap): + url = anonymap.get_anonymous_edit_url() + url = reverse( + 'map_anonymous_edit_url', + kwargs={'signature': "%s:badsignature" % anonymap.pk} + ) + response = cookieclient.get(url) + assert response.status_code == 403 + + +@pytest.mark.usefixtures('allow_anonymous') +def test_authenticated_user_with_cookie_is_attached_as_owner(cookieclient, anonymap, post_data, user): # noqa + url = reverse('map_update', kwargs={'map_id': anonymap.pk}) + cookieclient.login(username=user.username, password="123123") + assert anonymap.owner is None + # POST only mendatory filds + name = 'new map name for authenticat_anonymoused user' + post_data['name'] = name + response = cookieclient.post(url, post_data) + assert response.status_code == 200 + j = json.loads(response.content.decode()) + updated_map = Map.objects.get(pk=anonymap.pk) + assert j['id'] == updated_map.pk + assert updated_map.owner.pk, user.pk + + +@pytest.mark.usefixtures('allow_anonymous') +def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(client, anonymap, user): # noqa + assert Map.objects.count() == 1 + url = reverse('map_clone', kwargs={'map_id': anonymap.pk}) + anonymap.edit_status = anonymap.OWNER + anonymap.save() + response = client.post(url) + assert response.status_code == 403 + client.login(username=user.username, password="123123") + response = client.post(url) + assert response.status_code == 403 + assert Map.objects.count() == 1 + + +@pytest.mark.usefixtures('allow_anonymous') +def test_clone_map_should_be_possible_if_edit_status_is_anonymous(client, anonymap): # noqa + assert Map.objects.count() == 1 + url = reverse('map_clone', kwargs={'map_id': anonymap.pk}) + anonymap.edit_status = anonymap.ANONYMOUS + anonymap.save() + response = client.post(url) + assert response.status_code == 200 + assert Map.objects.count() == 2 + clone = Map.objects.latest('pk') + assert clone.pk != anonymap.pk + assert clone.name == 'Clone of ' + anonymap.name + assert clone.owner is None + + +@pytest.mark.usefixtures('allow_anonymous') +def test_anyone_can_access_anonymous_map(cookieclient, anonymap): + url = reverse('map', args=(anonymap.slug, anonymap.pk)) + anonymap.share_status = anonymap.PUBLIC + response = cookieclient.get(url) + assert response.status_code == 200 + anonymap.share_status = anonymap.OPEN + response = cookieclient.get(url) + assert response.status_code == 200 + anonymap.share_status = anonymap.PRIVATE + response = cookieclient.get(url) + assert response.status_code == 200 diff --git a/umap/tests/test_tilelayer.py b/umap/tests/test_tilelayer.py new file mode 100644 index 00000000..11e81709 --- /dev/null +++ b/umap/tests/test_tilelayer.py @@ -0,0 +1,21 @@ +import pytest + +from .base import TileLayerFactory + +pytestmark = pytest.mark.django_db + + +def test_tilelayer_json(): + tilelayer = TileLayerFactory(attribution='Attribution', maxZoom=19, + minZoom=0, name='Name', rank=1, tms=True, + url_template='http://{s}.x.fr/{z}/{x}/{y}') + assert tilelayer.json == { + 'attribution': 'Attribution', + 'id': tilelayer.id, + 'maxZoom': 19, + 'minZoom': 0, + 'name': 'Name', + 'rank': 1, + 'tms': True, + 'url_template': 'http://{s}.x.fr/{z}/{x}/{y}' + }