From 8efdc751392d596bec76cf750c119337fa85d732 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Thu, 9 Feb 2023 12:18:59 +1100 Subject: [PATCH 1/2] Add a PATH_PREFIX variable This allows us to "easily" deploy Django to a path prefix but has a ton of caveats and pitfalls that need to be addressed in subsequent commits. Specifically: tests will not work with the current definition, nor will our generated documentation. All of it assumes that the API is deployed at the root of the URL. We can change it to use `BASE_URL`, but then tests will fail if `PATH_PREFIX` is defined unless we switch to `reverse` retrieve the paths in tests. Doing this, however, creates a ton of circular imports. --- api/catalog/settings.py | 10 +++++++++- api/catalog/urls/__init__.py | 4 ++++ justfile | 8 +++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/api/catalog/settings.py b/api/catalog/settings.py index f04fd2d8f..c1d7d6ddb 100644 --- a/api/catalog/settings.py +++ b/api/catalog/settings.py @@ -298,8 +298,16 @@ def _make_cache_config(dbnum: int, **overrides) -> dict: # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ +PATH_PREFIX = config("PATH_PREFIX", default=None) + +if PATH_PREFIX and (PATH_PREFIX[0] == "/" or PATH_PREFIX[-1] == "/"): + raise ValueError("Path prefix must not start or end with `/`") + STATIC_URL = "/static/" +if PATH_PREFIX: + STATIC_URL = f"/{PATH_PREFIX}{STATIC_URL}" + # Allow anybody to access the API from any domain CORS_ORIGIN_ALLOW_ALL = True @@ -380,4 +388,4 @@ def _make_cache_config(dbnum: int, **overrides) -> dict: MAX_AUTHED_PAGE_SIZE = 500 MAX_PAGINATION_DEPTH = 20 -BASE_URL = config("BASE_URL", default="https://wordpress.org/openverse/") +BASE_URL = config("BASE_URL", default="https://openverse.org/") diff --git a/api/catalog/urls/__init__.py b/api/catalog/urls/__init__.py index 1458e2f81..091e183a1 100644 --- a/api/catalog/urls/__init__.py +++ b/api/catalog/urls/__init__.py @@ -16,6 +16,7 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings from django.conf.urls import include from django.contrib import admin from django.urls import path, re_path @@ -84,3 +85,6 @@ # API path("v1/", include(versioned_paths)), ] + +if settings.PATH_PREFIX: + urlpatterns = [path(f"{settings.PATH_PREFIX}/", include(urlpatterns))] diff --git a/justfile b/justfile index 6e203f085..79d6739a5 100644 --- a/justfile +++ b/justfile @@ -208,9 +208,11 @@ ing-testlocal *args: _api-install: cd api && pipenv install --dev +API_PATH_PREFIX := "/" + `(grep 'PATH_PREFIX' api/.env | awk -F= '{print $2}') || ""` + # Check the health of the API @web-health: - -curl -s -o /dev/null -w '%{http_code}' 'http://localhost:50280/healthcheck/' + -curl -s -o /dev/null -w '%{http_code}' 'http://localhost:50280{{ API_PATH_PREFIX }}/healthcheck/' # Wait for the API to be healthy @wait-for-web: @@ -220,7 +222,7 @@ _api-install: # Run smoke test for the API docs api-doctest: _api-up - curl --fail 'http://localhost:50280/v1/?format=openapi' + curl --fail 'http://localhost:50280{{ API_PATH_PREFIX }}/v1/?format=openapi' # Run API tests inside Docker api-test *args: _api-up @@ -236,7 +238,7 @@ dj-local +args: # Make a test cURL request to the API stats media="images": - curl "http://localhost:50280/v1/{{ media }}/stats/" + curl "http://localhost:50280{{ API_PATH_PREFIX }}/v1/{{ media }}/stats/" # Get Django shell with IPython ipython: From fe60f71ad9f733152f5a4e6638689f2e53bff887 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Thu, 9 Feb 2023 13:00:13 +1100 Subject: [PATCH 2/2] Start fixing tests; fiddle with circular imports --- api/catalog/api/examples/audio_requests.py | 14 +++-- api/catalog/api/examples/audio_responses.py | 15 ++--- api/catalog/api/examples/image_requests.py | 16 +++--- api/catalog/api/examples/image_responses.py | 15 ++--- api/catalog/api/views/audio_views.py | 32 ++++++++--- api/catalog/settings.py | 2 +- api/test/audio_integration_test.py | 12 ++-- api/test/auth_test.py | 12 ++-- api/test/backwards_compat_test.py | 24 +++++--- api/test/dead_link_filter_test.py | 11 ++-- api/test/image_integration_test.py | 38 ++++++------ api/test/media_integration.py | 48 ++++++++++------ api/test/unit/views/image_views_test.py | 2 +- api/test/v1_integration_test.py | 64 +++++++-------------- 14 files changed, 172 insertions(+), 133 deletions(-) diff --git a/api/catalog/api/examples/audio_requests.py b/api/catalog/api/examples/audio_requests.py index b817ede4f..a61954868 100644 --- a/api/catalog/api/examples/audio_requests.py +++ b/api/catalog/api/examples/audio_requests.py @@ -1,5 +1,7 @@ import os +from django.urls import reverse_lazy + token = os.getenv("AUDIO_REQ_TOKEN", "DLBYIcfnKfolaXKcmMC8RIDCavc2hW") origin = os.getenv("AUDIO_REQ_ORIGIN", "https://api.openverse.engineering") @@ -25,7 +27,7 @@ # Example {index}: Search for audio {purpose} curl \\ {auth} \\ - "{origin}/v1/audio/?q={syntax}" + "{origin}{reverse('audio-list')}?q={syntax}" """ for (index, (purpose, syntax)) in enumerate(syntax_examples.items()) ) @@ -34,28 +36,28 @@ # Search for music titled "Wish You Were Here" by The.madpix.project curl \\ {auth} \\ - "{origin}/v1/audio/?title=Wish%20You%20Were%20Here&creator=The.madpix.project" + "{origin}{reverse('audio-list')}?title=Wish%20You%20Were%20Here&creator=The.madpix.project" """ audio_stats_curl = f""" # Get the statistics for audio sources curl \\ {auth} \\ - "{origin}/v1/audio/stats/" + "{origin}{reverse('audio-list')}stats/" """ audio_detail_curl = f""" # Get the details of audio ID {identifier} curl \\ {auth} \\ - "{origin}/v1/audio/{identifier}/" + "{origin}{reverse('audio-list')}{identifier}/" """ audio_related_curl = f""" # Get related audio files for audio ID {identifier} curl \\ {auth} \\ - "{origin}/v1/audio/{identifier}/related/" + "{origin}{reverse('audio-list')}{identifier}/related/" """ audio_complain_curl = f""" @@ -65,5 +67,5 @@ -H "Content-Type: application/json" \\ {auth} \\ -d '{{"reason": "mature", "description": "This audio contains sensitive content"}}' \\ - "{origin}/v1/audio/{identifier}/report/" + "{origin}{reverse('audio-list')}{identifier}/report/" """ diff --git a/api/catalog/api/examples/audio_responses.py b/api/catalog/api/examples/audio_responses.py index 898ba2191..0f285e707 100644 --- a/api/catalog/api/examples/audio_responses.py +++ b/api/catalog/api/examples/audio_responses.py @@ -1,7 +1,8 @@ import os +from django.conf import settings +from django.urls import reverse -origin = os.getenv("AUDIO_REQ_ORIGIN", "https://api.openverse.engineering") identifier = "8624ba61-57f1-4f98-8a85-ece206c319cf" @@ -49,10 +50,10 @@ "duration": 270000, "bit_rate": 128000, "sample_rate": 44100, - "thumbnail": f"{origin}/v1/audio/{identifier}/thumb/", - "detail_url": f"{origin}/v1/audio/{identifier}/", - "related_url": f"{origin}/v1/audio/{identifier}/related/", - "waveform": f"{origin}/v1/audio/{identifier}/waveform/", + "thumbnail": f"{settings.BASE_URL}{reverse('audio-thumbnail', identifier=idenfitier)}", + "detail_url": f"{settings.BASE_URL}{reverse('audio-retrieve', identifier=identifier)}", + "related_url": f"{settings.BASE_URL}{reverse('audio-related', identifier=identifier)}", + "waveform": f"{settings.BASE_URL}{reverse('audio-waveform', identifier=identifier)}", } audio_search_200_example = { @@ -122,8 +123,8 @@ "license_version": "2.0", "license_url": "https://creativecommons.org/licenses/by-sa/2.0/", "foreign_landing_url": "https://commons.wikimedia.org/w/index.php?curid=3536953", # noqa: E501 - "detail_url": "http://api.openverse.engineering/v1/audio/36537842-b067-4ca0-ad67-e00ff2e06b2e", # noqa: E501 - "related_url": "http://api.openverse.engineering/v1/recommendations/audio/36537842-b067-4ca0-ad67-e00ff2e06b2e", # noqa: E501 + "detail_url": f"{settings.BASE_URL}{reverse('audio-retrieve', identifier='36537842-b067-4ca0-ad67-e00ff2e06b2e')}", # noqa: E501 + "related_url": f"{settings.BASE_URL}{reverse('audio-related', identifier='36537842-b067-4ca0-ad67-e00ff2e06b2e')}", # noqa: E501 "fields_matched": ["description", "title"], "tags": [{"name": "exam"}, {"name": "tactics"}], } diff --git a/api/catalog/api/examples/image_requests.py b/api/catalog/api/examples/image_requests.py index 3524e4745..d76e55c50 100644 --- a/api/catalog/api/examples/image_requests.py +++ b/api/catalog/api/examples/image_requests.py @@ -1,5 +1,7 @@ import os +from django.urls import reverse + token = os.getenv("AUDIO_REQ_TOKEN", "DLBYIcfnKfolaXKcmMC8RIDCavc2hW") origin = os.getenv("AUDIO_REQ_ORIGIN", "https://api.openverse.engineering") @@ -25,7 +27,7 @@ # Example {index}: Search for images {purpose} curl \\ {auth} \\ - "{origin}/v1/images/?q={syntax}" + "{origin}{reverse('image-list')}?q={syntax}" """ for (index, (purpose, syntax)) in enumerate(syntax_examples.items()) ) @@ -34,28 +36,28 @@ # Search for images titled "Bark" by Sullivan curl \\ {auth} \\ - "{origin}/v1/images/?title=Bark&creator=Sullivan" + "{origin}{reverse('image-list')}?title=Bark&creator=Sullivan" """ image_stats_curl = f""" # Get the statistics for image sources curl \\ {auth} \\ - "{origin}/v1/images/stats/" + "{origin}{reverse('image-list')}stats/" """ image_detail_curl = f""" # Get the details of image ID {identifier} curl \\ {auth} \\ - "{origin}/v1/images/{identifier}/" + "{origin}{reverse('image-list')}{identifier}/" """ image_related_curl = f""" # Get related images for image ID {identifier} curl \\ {auth} \\ - "{origin}/v1/images/{identifier}/related/" + "{origin}{reverse('image-list')}{identifier}/related/" """ image_complain_curl = f""" @@ -65,12 +67,12 @@ -H "Content-Type: application/json" \\ {auth} \\ -d '{{"reason": "mature", "description": "Image contains sensitive content"}}' \\ - "{origin}/v1/images/{identifier}/report/" + "{origin}{reverse('image-list')}{identifier}/report/" """ image_oembed_curl = f""" # Retrieve embedded content from an image's URL curl \\ {auth} \\ - "{origin}/v1/images/oembed/?url=https://wordpress.org/openverse/photos/{identifier}" + "{origin}{reverse('image-list')}oembed/?url=https://wordpress.org/openverse/photos/{identifier}" """ diff --git a/api/catalog/api/examples/image_responses.py b/api/catalog/api/examples/image_responses.py index dcd3c5ee5..e96c33255 100644 --- a/api/catalog/api/examples/image_responses.py +++ b/api/catalog/api/examples/image_responses.py @@ -1,7 +1,8 @@ import os +from django.conf import settings +from django.urls import reverse -origin = os.getenv("AUDIO_REQ_ORIGIN", "https://api.openverse.engineering") identifier = "4bc43a04-ef46-4544-a0c1-63c63f56e276" @@ -52,9 +53,9 @@ "mature": False, "height": 4016, "width": 6016, - "thumbnail": f"{origin}/v1/images/{identifier}/thumb/", - "detail_url": f"{origin}/v1/images/{identifier}/", - "related_url": f"{origin}/v1/images/{identifier}/related/", + "thumbnail": f"{settings.BASE_URL}{reverse('image-thumbnail', identifier=identifier)}", + "detail_url": f"{settings.BASE_URL}{reverse('image-retrieve', identifier=identifier)}", + "related_url": f"{settings.BASE_URL}{reverse('image-related', identifier=identifier)}", } detailed_image = base_image | { @@ -118,15 +119,15 @@ "creator_url": "https://www.flickr.com/photos/18090920@N07", "tags": [{"name": "exam"}, {"name": "tactics"}], "url": "https://live.staticflickr.com/4065/4459771899_07595dc42e.jpg", # noqa: E501 - "thumbnail": "https://api.openverse.engineering/v1/thumbs/610756ec-ae31-4d5e-8f03-8cc52f31b71d", # noqa: E501 + "thumbnail": f"{settings.BASE_URL}{reverse('image-thumbnail', identifier='610756ec-ae31-4d5e-8f03-8cc52f31b71d')}", # noqa: E501 "provider": "flickr", "source": "flickr", "license": "by", "license_version": "2.0", "license_url": "https://creativecommons.org/licenses/by/2.0/", "foreign_landing_url": "https://www.flickr.com/photos/18090920@N07/4459771899", # noqa: E501 - "detail_url": "http://api.openverse.engineering/v1/images/610756ec-ae31-4d5e-8f03-8cc52f31b71d", # noqa: E501 - "related_url": "http://api.openverse.engineering/v1/recommendations/images/610756ec-ae31-4d5e-8f03-8cc52f31b71d", # noqa: E501 + "detail_url": f"{settings.BASE_URL}{reverse('image-retrieve', identifier='610756ec-ae31-4d5e-8f03-8cc52f31b71d')}", # noqa: E501 + "related_url": f"{settings.BASE_URL}{reverse('image-related', identifier='610756ec-ae31-4d5e-8f03-8cc52f31b71d')}", # noqa: E501 } ], } diff --git a/api/catalog/api/views/audio_views.py b/api/catalog/api/views/audio_views.py index fb65cd78a..0f2e414d6 100644 --- a/api/catalog/api/views/audio_views.py +++ b/api/catalog/api/views/audio_views.py @@ -27,13 +27,13 @@ from catalog.api.views.media_views import MediaViewSet -@method_decorator(swagger_auto_schema(**AudioSearch.swagger_setup), "list") -@method_decorator(swagger_auto_schema(**AudioStats.swagger_setup), "stats") -@method_decorator(swagger_auto_schema(**AudioDetail.swagger_setup), "retrieve") -@method_decorator(swagger_auto_schema(**AudioRelated.swagger_setup), "related") -@method_decorator(swagger_auto_schema(**AudioComplain.swagger_setup), "report") -@method_decorator(swagger_auto_schema(**AudioThumbnail.swagger_setup), "thumbnail") -@method_decorator(swagger_auto_schema(auto_schema=None), "waveform") +# @method_decorator(swagger_auto_schema(**AudioSearch.swagger_setup), "list") +# @method_decorator(swagger_auto_schema(**AudioStats.swagger_setup), "stats") +# @method_decorator(swagger_auto_schema(**AudioDetail.swagger_setup), "retrieve") +# @method_decorator(swagger_auto_schema(**AudioRelated.swagger_setup), "related") +# @method_decorator(swagger_auto_schema(**AudioComplain.swagger_setup), "report") +# @method_decorator(swagger_auto_schema(**AudioThumbnail.swagger_setup), "thumbnail") +# @method_decorator(swagger_auto_schema(auto_schema=None), "waveform") class AudioViewSet(MediaViewSet): """Viewset for all endpoints pertaining to audio.""" @@ -92,3 +92,21 @@ def waveform(self, *_, **__): ) def report(self, *args, **kwargs): return super().report(*args, **kwargs) + + @staticmethod + def apply_swagger(): + options = ( + (AudioSearch.swagger_setup, "list"), + (AudioStats.swagger_setup, "stats"), + (AudioDetail.swagger_setup, "retrieve"), + (AudioRelated.swagger_setup, "related"), + (AudioComplain.swagger_setup, "report"), + (AudioThumbnail.swagger_setup, "thumbnail"), + ({"auto_schema": None}, "waveform"), + ) + + for decorator_kwargs, method_name in options: + nonlocal AudioViewSet + AudioViewSet = method_decorator( + swagger_auto_schema(**decorator_kwargs), method_name + )(AudioViewSet) diff --git a/api/catalog/settings.py b/api/catalog/settings.py index c1d7d6ddb..7ab833f29 100644 --- a/api/catalog/settings.py +++ b/api/catalog/settings.py @@ -388,4 +388,4 @@ def _make_cache_config(dbnum: int, **overrides) -> dict: MAX_AUTHED_PAGE_SIZE = 500 MAX_PAGINATION_DEPTH = 20 -BASE_URL = config("BASE_URL", default="https://openverse.org/") +BASE_URL = config("BASE_URL", default="https://openverse.org/api") diff --git a/api/test/audio_integration_test.py b/api/test/audio_integration_test.py index 6a90b146e..892998f83 100644 --- a/api/test/audio_integration_test.py +++ b/api/test/audio_integration_test.py @@ -24,6 +24,8 @@ uuid_validation, ) +from django.urls import reverse + import pytest import requests from django_redis import get_redis_connection @@ -52,7 +54,7 @@ def force_validity(query_response): @pytest.fixture def audio_fixture(force_result_validity): - res = requests.get(f"{API_URL}/v1/audio/", verify=False) + res = requests.get(f"{API_URL}{reverse('audio-list')}", verify=False) parsed = res.json() force_result_validity(parsed) assert res.status_code == 200 @@ -68,7 +70,7 @@ def jamendo_audio_fixture(force_result_validity): sample audio results do not have thumbnails. """ res = requests.get( - f"{API_URL}/v1/audio/", + f"{API_URL}{reverse('audio-list')}", data={"source": "jamendo"}, verify=False, ) @@ -127,7 +129,9 @@ def test_audio_stats(): def test_audio_detail_without_thumb(): - resp = requests.get(f"{API_URL}/v1/audio/44540200-91eb-483d-9e99-38ce86a52fb6") + resp = requests.get( + f"{API_URL}{reverse('audio-retrieve', identifier='44540200-91eb-483d-9e99-38ce86a52fb6')}" + ) assert resp.status_code == 200 parsed = json.loads(resp.text) assert parsed["thumbnail"] is None @@ -135,7 +139,7 @@ def test_audio_detail_without_thumb(): def test_audio_search_without_thumb(): """The first audio of this search should not have a thumbnail.""" - resp = requests.get(f"{API_URL}/v1/audio/?q=zaus") + resp = requests.get(f"{API_URL}{reverse('audio-list')}?q=zaus") assert resp.status_code == 200 parsed = json.loads(resp.text) assert parsed["results"][0]["thumbnail"] is None diff --git a/api/test/auth_test.py b/api/test/auth_test.py index d4675a335..3d4d904a8 100644 --- a/api/test/auth_test.py +++ b/api/test/auth_test.py @@ -99,10 +99,10 @@ def test_auth_rate_limit_reporting( @pytest.mark.django_db def test_page_size_limit_unauthed(client): query_params = {"page_size": 20} - res = client.get("/v1/images/", query_params) + res = client.get(reverse("image-list"), query_params) assert res.status_code == 200 query_params["page_size"] = 21 - res = client.get("/v1/images/", query_params) + res = client.get(reverse("image-list"), query_params) assert res.status_code == 401 @@ -111,9 +111,13 @@ def test_page_size_limit_authed(client, test_auth_token_exchange): time.sleep(1) token = test_auth_token_exchange["access_token"] query_params = {"page_size": 21} - res = client.get("/v1/images/", query_params, HTTP_AUTHORIZATION=f"Bearer {token}") + res = client.get( + reverse("image-list"), query_params, HTTP_AUTHORIZATION=f"Bearer {token}" + ) assert res.status_code == 200 query_params = {"page_size": 500} - res = client.get("/v1/images/", query_params, HTTP_AUTHORIZATION=f"Bearer {token}") + res = client.get( + reverse("image-list"), query_params, HTTP_AUTHORIZATION=f"Bearer {token}" + ) assert res.status_code == 200 diff --git a/api/test/backwards_compat_test.py b/api/test/backwards_compat_test.py index fe1ebe9c8..7c8a5f4d4 100644 --- a/api/test/backwards_compat_test.py +++ b/api/test/backwards_compat_test.py @@ -8,44 +8,52 @@ import uuid from test.constants import API_URL +from django.urls import reverse + import requests def test_old_stats_endpoint(): response = requests.get( - f"{API_URL}/v1/sources?type=images", allow_redirects=False, verify=False + f"{API_URL}{reverse('about-image')}?type=images", + allow_redirects=False, + verify=False, ) assert response.status_code == 301 assert response.is_permanent_redirect - assert response.headers.get("Location") == "/v1/images/stats/" + assert response.headers.get("Location") == reverse("image-stats") def test_old_related_images_endpoint(): idx = uuid.uuid4() response = requests.get( - f"{API_URL}/v1/recommendations/images/{idx}", + f"{API_URL}{reverse('related-images', identifier=idx)}", allow_redirects=False, verify=False, ) assert response.status_code == 301 assert response.is_permanent_redirect - assert response.headers.get("Location") == f"/v1/images/{idx}/related/" + assert response.headers.get("Location") == reverse("image-related", identifier=idx) def test_old_oembed_endpoint(): response = requests.get( - f"{API_URL}/v1/oembed?key=value", allow_redirects=False, verify=False + f"{API_URL}{reverse('oembed')}?key=value", allow_redirects=False, verify=False ) assert response.status_code == 301 assert response.is_permanent_redirect - assert response.headers.get("Location") == "/v1/images/oembed/?key=value" + assert response.headers.get("Location") == f"{reverse('image-oembed')}?key=value" def test_old_thumbs_endpoint(): idx = uuid.uuid4() response = requests.get( - f"{API_URL}/v1/thumbs/{idx}", allow_redirects=False, verify=False + f"{API_URL}{reverse('thumbs', identifier=idx)}", + allow_redirects=False, + verify=False, ) assert response.status_code == 301 assert response.is_permanent_redirect - assert response.headers.get("Location") == f"/v1/images/{idx}/thumb/" + assert response.headers.get("Location") == reverse( + "image-thumbnail", identifier=idx + ) diff --git a/api/test/dead_link_filter_test.py b/api/test/dead_link_filter_test.py index dbca58e65..9a484433e 100644 --- a/api/test/dead_link_filter_test.py +++ b/api/test/dead_link_filter_test.py @@ -3,6 +3,7 @@ from uuid import uuid4 from django.conf import settings +from django.urls import reverse import pytest import requests @@ -96,7 +97,7 @@ def _make_head_requests(urls): @pytest.mark.django_db @_patch_make_head_requests() def test_dead_link_filtering(mocked_map, client): - path = "/v1/images/" + path = reverse("image-list") query_params = {"q": "*", "page_size": 20} # Make a request that does not filter dead links... @@ -147,7 +148,7 @@ def test_dead_link_filtering_all_dead_links( unique_query_hash, empty_validation_cache, ): - path = "/v1/images/" + path = reverse("image-list") query_params = {"q": "*", "page_size": page_size} with patch_link_validation_dead_for_count(page_size / DEAD_LINK_RATIO): @@ -170,7 +171,9 @@ def search_factory(client): """Allow passing url parameters along with a search request.""" def _parameterized_search(**kwargs): - response = requests.get(f"{API_URL}/v1/images", params=kwargs, verify=False) + response = requests.get( + f"{API_URL}{reverse('image-list')}", params=kwargs, verify=False + ) assert response.status_code == 200 parsed = response.json() return parsed @@ -230,7 +233,7 @@ def no_duplicates(xs): @pytest.mark.django_db def test_max_page_count(): response = requests.get( - f"{API_URL}/v1/images", + f"{API_URL}{reverse('image-list')}", params={"page": settings.MAX_PAGINATION_DEPTH + 1}, verify=False, ) diff --git a/api/test/image_integration_test.py b/api/test/image_integration_test.py index f9e8709e1..6e32883c4 100644 --- a/api/test/image_integration_test.py +++ b/api/test/image_integration_test.py @@ -25,6 +25,8 @@ ) from urllib.parse import urlencode +from django.urls import reverse + import pytest import requests @@ -34,7 +36,7 @@ @pytest.fixture def image_fixture(): - response = requests.get(f"{API_URL}/v1/images?q=dog", verify=False) + response = requests.get(f"{API_URL}{reverse('image-list')}?q=dog", verify=False) assert response.status_code == 200 parsed = json.loads(response.text) return parsed @@ -45,41 +47,41 @@ def test_search(image_fixture): def test_search_all_excluded(): - search_all_excluded("images", ["flickr", "stocksnap"]) + search_all_excluded("image", ["flickr", "stocksnap"]) def test_search_source_and_excluded(): - search_source_and_excluded("images") + search_source_and_excluded("image") def test_search_quotes(): - search_quotes("images", "dog") + search_quotes("image", "dog") def test_search_quotes_exact(): # ``bird perched`` returns different results when quoted vs unquoted - search_quotes_exact("images", "bird perched") + search_quotes_exact("image", "bird perched") def test_search_with_special_characters(): - search_special_chars("images", "dog") + search_special_chars("image", "dog") def test_search_consistency(): n_pages = 5 - search_consistency("images", n_pages) + search_consistency("image", n_pages) def test_image_detail(image_fixture): - detail("images", image_fixture) + detail("image", image_fixture) def test_image_stats(): - stats("images") + stats("image") def test_audio_report(image_fixture): - report("images", image_fixture) + report("image", image_fixture) def test_oembed_endpoint_with_non_existent_image(): @@ -87,7 +89,7 @@ def test_oembed_endpoint_with_non_existent_image(): "url": "https://any.domain/any/path/00000000-0000-0000-0000-000000000000", } response = requests.get( - f"{API_URL}/v1/images/oembed?{urlencode(params)}", verify=False + f"{API_URL}{reverse('image-oembed')}?{urlencode(params)}", verify=False ) assert response.status_code == 404 @@ -103,7 +105,7 @@ def test_oembed_endpoint_with_non_existent_image(): def test_oembed_endpoint_with_fuzzy_input(url): params = {"url": url} response = requests.get( - f"{API_URL}/v1/images/oembed?{urlencode(params)}", verify=False + f"{API_URL}{reverse('image-oembed')}?{urlencode(params)}", verify=False ) assert response.status_code == 200 @@ -114,7 +116,7 @@ def test_oembed_endpoint_for_json(): # 'format': 'json' is the default } response = requests.get( - f"{API_URL}/v1/images/oembed?{urlencode(params)}", verify=False + f"{API_URL}{reverse('image-oembed')}?{urlencode(params)}", verify=False ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" @@ -131,7 +133,7 @@ def test_oembed_endpoint_for_xml(): "format": "xml", } response = requests.get( - f"{API_URL}/v1/images/oembed?{urlencode(params)}", verify=False + f"{API_URL}{reverse('image-oembed')}?{urlencode(params)}", verify=False ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/xml; charset=utf-8" @@ -147,13 +149,13 @@ def test_oembed_endpoint_for_xml(): def test_image_license_filter_case_insensitivity(): - license_filter_case_insensitivity("images") + license_filter_case_insensitivity("image") def test_image_uuid_validation(): - uuid_validation("images", "123456789123456789123456789123456789") - uuid_validation("images", "12345678-1234-5678-1234-1234567891234") - uuid_validation("images", "abcd") + uuid_validation("image", "123456789123456789123456789123456789") + uuid_validation("image", "12345678-1234-5678-1234-1234567891234") + uuid_validation("image", "abcd") def test_image_related(image_fixture): diff --git a/api/test/media_integration.py b/api/test/media_integration.py index ad31182ec..745a82b5a 100644 --- a/api/test/media_integration.py +++ b/api/test/media_integration.py @@ -26,32 +26,34 @@ def search_by_category(media_path, category, fixture): assert all(audio_item["category"] == category for audio_item in results) -def search_all_excluded(media_path, excluded_source): +def search_all_excluded(media_type, excluded_source): response = requests.get( - f"{API_URL}/v1/{media_path}?q=test&excluded_source={','.join(excluded_source)}" + f"{API_URL}{reverse(f'{media_type}-list')}?q=test&excluded_source={','.join(excluded_source)}" ) data = json.loads(response.text) assert data["result_count"] == 0 -def search_source_and_excluded(media_path): +def search_source_and_excluded(media_type): response = requests.get( - f"{API_URL}/v1/{media_path}?q=test&source=x&excluded_source=y" + f"{API_URL}{reverse(f'{media_type}-list')}?q=test&source=x&excluded_source=y" ) assert response.status_code == 400 -def search_quotes(media_path, q="test"): +def search_quotes(media_type, q="test"): """Return a response when quote matching is messed up.""" - response = requests.get(f'{API_URL}/v1/{media_path}?q="{q}', verify=False) + response = requests.get( + f'{API_URL}{reverse(f"{media_type}-list")}?q="{q}', verify=False + ) assert response.status_code == 200 -def search_quotes_exact(media_path, q): +def search_quotes_exact(media_type, q): """Return only exact matches for the given query.""" - url_format = f"{API_URL}/v1/{media_path}?q={{q}}" + url_format = f"{API_URL}{reverse(f'{media_type}-list')}?q={{q}}" unquoted_response = requests.get(url_format.format(q=q), verify=False) assert unquoted_response.status_code == 200 unquoted_result_count = unquoted_response.json()["result_count"] @@ -69,15 +71,17 @@ def search_quotes_exact(media_path, q): assert quoted_result_count < unquoted_result_count -def search_special_chars(media_path, q="test"): +def search_special_chars(media_type, q="test"): """Return a response when query includes special characters.""" - response = requests.get(f"{API_URL}/v1/{media_path}?q={q}!", verify=False) + response = requests.get( + f"{API_URL}{reverse(f'{media_type}-list')}?q={q}!", verify=False + ) assert response.status_code == 200 def search_consistency( - media_path, + media_type, n_pages, ): """ @@ -90,7 +94,9 @@ def search_consistency( """ searches = { - requests.get(f"{API_URL}/v1/{media_path}?page={page}", verify=False) + requests.get( + f"{API_URL}{reverse(f'{media_type}-list')}?page={page}", verify=False + ) for page in range(1, n_pages) } @@ -105,12 +111,15 @@ def search_consistency( def detail(media_type, fixture): test_id = fixture["results"][0]["id"] - response = requests.get(f"{API_URL}/v1/{media_type}/{test_id}", verify=False) + response = requests.get( + f"{API_URL}{reverse(f'{media_type}-retrieve', identifier=test_id)}", + verify=False, + ) assert response.status_code == 200 def stats(media_type, count_key="media_count"): - response = requests.get(f"{API_URL}/v1/{media_type}/stats", verify=False) + response = requests.get(f"{API_URL}{reverse(f'{media_type}-stats')}", verify=False) parsed_response = json.loads(response.text) assert response.status_code == 200 num_media = 0 @@ -126,7 +135,7 @@ def stats(media_type, count_key="media_count"): def report(media_type, fixture): test_id = fixture["results"][0]["id"] response = requests.post( - f"{API_URL}/v1/{media_type}/{test_id}/report/", + f"{API_URL}{reverse(f'{media_type}-report', identifier=test_id)}", json={ "reason": "mature", "description": "This item contains sensitive content", @@ -139,13 +148,18 @@ def report(media_type, fixture): def license_filter_case_insensitivity(media_type): - response = requests.get(f"{API_URL}/v1/{media_type}?license=bY", verify=False) + response = requests.get( + f"{API_URL}{reverse(f'{media_type}-list')}?license=bY", verify=False + ) parsed = json.loads(response.text) assert parsed["result_count"] > 0 def uuid_validation(media_type, identifier): - response = requests.get(f"{API_URL}/v1/{media_type}/{identifier}", verify=False) + response = requests.get( + f"{API_URL}{reverse(f'{media_type}-retrieve', identifier=identifier)}", + verify=False, + ) assert response.status_code == 404 diff --git a/api/test/unit/views/image_views_test.py b/api/test/unit/views/image_views_test.py index a9580f01c..0b8debee5 100644 --- a/api/test/unit/views/image_views_test.py +++ b/api/test/unit/views/image_views_test.py @@ -49,7 +49,7 @@ def requests_get(url, **kwargs): @pytest.mark.django_db def test_oembed_sends_ua_header(api_client, requests): image = ImageFactory.create() - res = api_client.get("/v1/images/oembed/", data={"url": f"/{image.identifier}"}) + res = api_client.get(reverse("image-oembed"), data={"url": f"/{image.identifier}"}) assert res.status_code == 200 diff --git a/api/test/v1_integration_test.py b/api/test/v1_integration_test.py index d72723a41..6978ca328 100644 --- a/api/test/v1_integration_test.py +++ b/api/test/v1_integration_test.py @@ -8,6 +8,8 @@ import json from test.constants import API_URL +from django.urls import reverse + import pytest import requests @@ -18,7 +20,7 @@ @pytest.fixture def image_fixture(): - response = requests.get(f"{API_URL}/v1/images?q=dog", verify=False) + response = requests.get(f"{API_URL}{reverse('image-list')}?q=dog", verify=False) assert response.status_code == 200 parsed = json.loads(response.text) return parsed @@ -26,46 +28,17 @@ def image_fixture(): def test_link_shortener_create(): payload = {"full_url": "abcd"} - response = requests.post(f"{API_URL}/v1/link/", json=payload, verify=False) + response = requests.post( + f"{API_URL}{reverse('make-link')}", json=payload, verify=False + ) assert response.status_code == 410 def test_link_shortener_resolve(): - response = requests.get(f"{API_URL}/v1/link/abc", verify=False) + response = requests.get(f"{API_URL}{reverse('make-link')}abc", verify=False) assert response.status_code == 410 -@pytest.mark.skip(reason="Disabled feature") -@pytest.fixture -def test_list_create(image_fixture): - payload = { - "title": "INTEGRATION TEST", - "images": [image_fixture["results"][0]["id"]], - } - response = requests.post(f"{API_URL}/list", json=payload, verify=False) - parsed_response = json.loads(response.text) - assert response.status_code == 201 - return parsed_response - - -@pytest.mark.skip(reason="Disabled feature") -def test_list_detail(test_list_create): - list_slug = test_list_create["url"].split("/")[-1] - response = requests.get(f"{API_URL}/list/{list_slug}", verify=False) - assert response.status_code == 200 - - -@pytest.mark.skip(reason="Disabled feature") -def test_list_delete(test_list_create): - list_slug = test_list_create["url"].split("/")[-1] - token = test_list_create["auth"] - headers = {"Authorization": f"Token {token}"} - response = requests.delete( - f"{API_URL}/list/{list_slug}", headers=headers, verify=False - ) - assert response.status_code == 204 - - def test_license_type_filtering(): """Ensure that multiple license type filters interact together correctly.""" @@ -73,7 +46,8 @@ def test_license_type_filtering(): modification = LICENSE_GROUPS["modification"] commercial_and_modification = set.intersection(modification, commercial) response = requests.get( - f"{API_URL}/v1/images?q=dog&license_type=commercial,modification", verify=False + f"{API_URL}{reverse('image-list')}?q=dog&license_type=commercial,modification", + verify=False, ) parsed = json.loads(response.text) for result in parsed["results"]: @@ -83,7 +57,7 @@ def test_license_type_filtering(): def test_single_license_type_filtering(): commercial = LICENSE_GROUPS["commercial"] response = requests.get( - f"{API_URL}/v1/images?q=dog&license_type=commercial", verify=False + f"{API_URL}{reverse('image-list')}?q=dog&license_type=commercial", verify=False ) parsed = json.loads(response.text) for result in parsed["results"]: @@ -91,7 +65,9 @@ def test_single_license_type_filtering(): def test_specific_license_filter(): - response = requests.get(f"{API_URL}/v1/images?q=dog&license=by", verify=False) + response = requests.get( + f"{API_URL}{reverse('image-list')}?q=dog&license=by", verify=False + ) parsed = json.loads(response.text) for result in parsed["results"]: assert result["license"] == "by" @@ -101,11 +77,13 @@ def test_creator_quotation_grouping(): """Test that quotation marks can be used to narrow down search results.""" no_quotes = json.loads( - requests.get(f"{API_URL}/v1/images?creator=Steve%20Wedgwood", verify=False).text + requests.get( + f"{API_URL}{reverse('image-list')}?creator=Steve%20Wedgwood", verify=False + ).text ) quotes = json.loads( requests.get( - f'{API_URL}/v1/images?creator="Steve%20Wedgwood"', verify=False + f'{API_URL}{reverse("image-list")}?creator="Steve%20Wedgwood"', verify=False ).text ) # Did quotation marks actually narrow down the search? @@ -201,7 +179,9 @@ def test_license_override(): def test_source_search(): - response = requests.get(f"{API_URL}/v1/images?source=flickr", verify=False) + response = requests.get( + f"{API_URL}{reverse('image-list')}?source=flickr", verify=False + ) if response.status_code != 200: print(f"Request failed. Message: {response.body}") assert response.status_code == 200 @@ -210,7 +190,7 @@ def test_source_search(): def test_extension_filter(): - response = requests.get(f"{API_URL}/v1/images?q=dog&extension=jpg") + response = requests.get(f"{API_URL}{reverse('image-list')}?q=dog&extension=jpg") parsed = json.loads(response.text) for result in parsed["results"]: assert ".jpg" in result["url"] @@ -222,7 +202,7 @@ def recommendation_factory(): def _parameterized_search(identifier, **kwargs): response = requests.get( - f"{API_URL}/v1/recommendations?type=images&id={identifier}", + f"{API_URL}{reverse('related-images')}?type=images&id={identifier}", params=kwargs, verify=False, )