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 f04fd2d8f..7ab833f29 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/api") 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/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, ) 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: