diff --git a/swh/web/api/views/origin.py b/swh/web/api/views/origin.py --- a/swh/web/api/views/origin.py +++ b/swh/web/api/views/origin.py @@ -446,7 +446,7 @@ @api_route( - r"/origin/(?P.+)" "/intrinsic-metadata", "api-origin-intrinsic-metadata" + r"/origin/(?P.+)/intrinsic-metadata/", "api-origin-intrinsic-metadata" ) @api_doc("/origin/intrinsic-metadata/") @format_docstring() diff --git a/swh/web/auth/mailmap.py b/swh/web/auth/mailmap.py --- a/swh/web/auth/mailmap.py +++ b/swh/web/auth/mailmap.py @@ -109,10 +109,10 @@ urlpatterns = [ - url(r"^profile/mailmap/list$", profile_list_mailmap, name="profile-mailmap-list",), - url(r"^profile/mailmap/add$", profile_add_mailmap, name="profile-mailmap-add",), + url(r"^profile/mailmap/list/$", profile_list_mailmap, name="profile-mailmap-list",), + url(r"^profile/mailmap/add/$", profile_add_mailmap, name="profile-mailmap-add",), url( - r"^profile/mailmap/update$", + r"^profile/mailmap/update/$", profile_update_mailmap, name="profile-mailmap-update", ), diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py --- a/swh/web/browse/views/content.py +++ b/swh/web/browse/views/content.py @@ -67,7 +67,7 @@ @browse_route( - r"content/(?P.*)/diff/(?P.*)", + r"content/(?P.*)/diff/(?P.*)/", view_name="diff-contents", ) def _contents_diff(request, from_query_string, to_query_string): diff --git a/swh/web/misc/fundraising.py b/swh/web/misc/fundraising.py --- a/swh/web/misc/fundraising.py +++ b/swh/web/misc/fundraising.py @@ -58,5 +58,5 @@ urlpatterns = [ - url(r"^fundraising/banner", fundraising_banner, name="swh-fundraising-banner"), + url(r"^fundraising/banner/$", fundraising_banner, name="swh-fundraising-banner"), ] diff --git a/swh/web/misc/iframe.py b/swh/web/misc/iframe.py --- a/swh/web/misc/iframe.py +++ b/swh/web/misc/iframe.py @@ -330,7 +330,7 @@ urlpatterns = [ url( - r"^embed/(?Pswh:[0-9]+:[a-z]+:[0-9a-f]+.*)$", + r"^embed/(?Pswh:[0-9]+:[a-z]+:[0-9a-f]+.*)/$", swhid_iframe, name="swhid-iframe", ), diff --git a/swh/web/misc/origin_save.py b/swh/web/misc/origin_save.py --- a/swh/web/misc/origin_save.py +++ b/swh/web/misc/origin_save.py @@ -92,7 +92,7 @@ name="origin-save-requests-list", ), url( - r"^save/task/info/(?P.+)/", + r"^save/task/info/(?P.+)/$", _save_origin_task_info, name="origin-save-task-info", ), diff --git a/swh/web/misc/urls.py b/swh/web/misc/urls.py --- a/swh/web/misc/urls.py +++ b/swh/web/misc/urls.py @@ -48,7 +48,7 @@ url(r"^", include("swh.web.misc.coverage")), url(r"^jslicenses/$", _jslicenses, name="jslicenses"), url(r"^", include("swh.web.misc.origin_save")), - url(r"^stat_counters/", _stat_counters, name="stat-counters"), + url(r"^stat_counters/$", _stat_counters, name="stat-counters"), url(r"^", include("swh.web.misc.badges")), url(r"^metrics/prometheus/$", prometheus_metrics, name="metrics-prometheus"), url(r"^", include("swh.web.misc.iframe")), diff --git a/swh/web/tests/api/test_throttling.py b/swh/web/tests/api/test_throttling.py --- a/swh/web/tests/api/test_throttling.py +++ b/swh/web/tests/api/test_throttling.py @@ -70,10 +70,10 @@ urlpatterns += [ - url(r"^scope1_class$", MockViewScope1.as_view()), - url(r"^scope2_func$", mock_view_scope2), - url(r"^scope3_class$", MockViewScope3.as_view()), - url(r"^scope3_func$", mock_view_scope3), + url(r"^scope1_class/$", MockViewScope1.as_view()), + url(r"^scope2_func/$", mock_view_scope2), + url(r"^scope3_class/$", MockViewScope3.as_view()), + url(r"^scope3_func/$", mock_view_scope3), ] @@ -95,19 +95,19 @@ Ensure request rate is limited in scope1 """ for i in range(scope1_limiter_rate): - response = api_client.get("/scope1_class") + response = api_client.get("/scope1_class/") check_response(response, 200, scope1_limiter_rate, scope1_limiter_rate - i - 1) - response = api_client.get("/scope1_class") + response = api_client.get("/scope1_class/") check_response(response, 429, scope1_limiter_rate, 0) for i in range(scope1_limiter_rate_post): - response = api_client.post("/scope1_class") + response = api_client.post("/scope1_class/") check_response( response, 200, scope1_limiter_rate_post, scope1_limiter_rate_post - i - 1 ) - response = api_client.post("/scope1_class") + response = api_client.post("/scope1_class/") check_response(response, 429, scope1_limiter_rate_post, 0) @@ -117,19 +117,19 @@ Ensure request rate is limited in scope2 """ for i in range(scope2_limiter_rate): - response = api_client.get("/scope2_func") + response = api_client.get("/scope2_func/") check_response(response, 200, scope2_limiter_rate, scope2_limiter_rate - i - 1) - response = api_client.get("/scope2_func") + response = api_client.get("/scope2_func/") check_response(response, 429, scope2_limiter_rate, 0) for i in range(scope2_limiter_rate_post): - response = api_client.post("/scope2_func") + response = api_client.post("/scope2_func/") check_response( response, 200, scope2_limiter_rate_post, scope2_limiter_rate_post - i - 1 ) - response = api_client.post("/scope2_func") + response = api_client.post("/scope2_func/") check_response(response, 429, scope2_limiter_rate_post, 0) @@ -140,19 +140,19 @@ requests coming from localhost are exempted from rate limit. """ for _ in range(scope3_limiter_rate + 1): - response = api_client.get("/scope3_class") + response = api_client.get("/scope3_class/") check_response(response, 200) for _ in range(scope3_limiter_rate_post + 1): - response = api_client.post("/scope3_class") + response = api_client.post("/scope3_class/") check_response(response, 200) for _ in range(scope3_limiter_rate + 1): - response = api_client.get("/scope3_func") + response = api_client.get("/scope3_func/") check_response(response, 200) for _ in range(scope3_limiter_rate_post + 1): - response = api_client.post("/scope3_func") + response = api_client.post("/scope3_func/") check_response(response, 200) @@ -163,11 +163,11 @@ api_client.force_login(staff_user) for _ in range(scope2_limiter_rate + 1): - response = api_client.get("/scope2_func") + response = api_client.get("/scope2_func/") check_response(response, 200) for _ in range(scope2_limiter_rate_post + 1): - response = api_client.post("/scope2_func") + response = api_client.post("/scope2_func/") check_response(response, 200) @@ -182,12 +182,12 @@ ) for i in range(scope2_limiter_rate_user): - response = api_client.get("/scope2_func") + response = api_client.get("/scope2_func/") check_response( response, 200, scope2_limiter_rate_user, scope2_limiter_rate_user - i - 1 ) - response = api_client.get("/scope2_func") + response = api_client.get("/scope2_func/") check_response(response, 429, scope2_limiter_rate_user, 0) scope2_limiter_rate_post_user = ( @@ -195,7 +195,7 @@ ) for i in range(scope2_limiter_rate_post_user): - response = api_client.post("/scope2_func") + response = api_client.post("/scope2_func/") check_response( response, 200, @@ -203,7 +203,7 @@ scope2_limiter_rate_post_user - i - 1, ) - response = api_client.post("/scope2_func") + response = api_client.post("/scope2_func/") check_response(response, 429, scope2_limiter_rate_post_user, 0) @@ -222,9 +222,9 @@ api_client.force_login(regular_user) for _ in range(scope2_limiter_rate + 1): - response = api_client.get("/scope2_func") + response = api_client.get("/scope2_func/") check_response(response, 200) for _ in range(scope2_limiter_rate_post + 1): - response = api_client.post("/scope2_func") + response = api_client.post("/scope2_func/") check_response(response, 200) diff --git a/swh/web/tests/test_urls.py b/swh/web/tests/test_urls.py new file mode 100644 --- /dev/null +++ b/swh/web/tests/test_urls.py @@ -0,0 +1,13 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU Affero General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from django.urls import get_resolver + + +def test_swh_web_urls_have_trailing_slash(): + urls = set(value[1] for value in get_resolver().reverse_dict.values()) + for url in urls: + if url != "$": + assert url.endswith("/$") diff --git a/swh/web/urls.py b/swh/web/urls.py --- a/swh/web/urls.py +++ b/swh/web/urls.py @@ -43,7 +43,7 @@ urlpatterns = [ url(r"^admin/", include("swh.web.admin.urls")), - url(r"^favicon\.ico$", favicon_view), + url(r"^favicon\.ico/$", favicon_view), url(r"^api/", include("swh.web.api.urls")), url(r"^browse/", include("swh.web.browse.urls")), url(r"^$", _default_view, name="swh-web-homepage"), @@ -73,7 +73,7 @@ # enable to serve compressed assets through django development server if swh_web_config["serve_assets"]: - static_pattern = r"^%s(?P.*)$" % settings.STATIC_URL[1:] + static_pattern = r"^%s(?P.*)/$" % settings.STATIC_URL[1:] urlpatterns.append(url(static_pattern, insecure_serve))