From 9a8ab844d9d3ceb4a258e112a4328a8ddabe27c1 Mon Sep 17 00:00:00 2001 From: rhysrevans3 <34507919+rhysrevans3@users.noreply.github.com> Date: Wed, 12 Jun 2024 08:43:31 +0100 Subject: [PATCH] Allow default route dependencies (#705) * Allowing for default route dependencies. * Running precommit hooks. * Adding pull request to CHANGELOG. * Update stac_fastapi/api/stac_fastapi/api/routes.py Co-authored-by: Anthony Lukach * Fixing indenting. --------- Co-authored-by: Jonathan Healy Co-authored-by: Anthony Lukach --- CHANGES.md | 1 + stac_fastapi/api/stac_fastapi/api/routes.py | 16 +- stac_fastapi/api/tests/test_api.py | 272 ++++++++++++++++++++ 3 files changed, 288 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d6499fb83..e1a7e29ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ ### Changed +* Added option for default route dependencies `*` can be used for `path` or `method` to match all allowed route. ([#705](https://github.com/stac-utils/stac-fastapi/pull/705)) * moved `AsyncBaseFiltersClient` and `BaseFiltersClient` classes in `stac_fastapi.extensions.core.filter.client` submodule ([#704](https://github.com/stac-utils/stac-fastapi/pull/704)) ## [3.0.0a2] - 2024-05-31 diff --git a/stac_fastapi/api/stac_fastapi/api/routes.py b/stac_fastapi/api/stac_fastapi/api/routes.py index df4a136eb..bd6f4d9cf 100644 --- a/stac_fastapi/api/stac_fastapi/api/routes.py +++ b/stac_fastapi/api/stac_fastapi/api/routes.py @@ -1,5 +1,6 @@ """Route factories.""" +import copy import functools import inspect import warnings @@ -100,15 +101,28 @@ def add_route_dependencies( Allows a developer to add dependencies to a route after the route has been defined. + "*" can be used for path or method to match all allowed routes. + Returns: None """ for scope in scopes: + _scope = copy.deepcopy(scope) for route in routes: - match, _ = route.matches({"type": "http", **scope}) + if scope["path"] == "*": + _scope["path"] = route.path + + if scope["method"] == "*": + _scope["method"] = list(route.methods)[0] + + match, _ = route.matches({"type": "http", **_scope}) if match != Match.FULL: continue + # Ignore paths without dependants, e.g. /api, /api.html, /docs/oauth2-redirect + if not hasattr(route, "dependant"): + continue + # Mimicking how APIRoute handles dependencies: # https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412 for depends in dependencies[::-1]: diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index f2d51f1db..deff0c070 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -49,6 +49,24 @@ def _assert_dependency_applied(api, routes): ), "Authenticated requests should be accepted" assert response.json() == "dummy response" + @staticmethod + def _assert_dependency_not_applied(api, routes): + with TestClient(api.app) as client: + for route in routes: + path = route["path"].format( + collectionId="test_collection", itemId="test_item" + ) + response = client.request( + method=route["method"].lower(), + url=path, + content=route["payload"], + headers={"content-type": "application/json"}, + ) + assert ( + 200 <= response.status_code < 300 + ), "Authenticated requests should be accepted" + assert response.json() == "dummy response" + def test_openapi_content_type(self): api = self._build_api() with TestClient(api.app) as client: @@ -116,6 +134,260 @@ def test_add_route_dependencies_after_building_api(self, collection, item): api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)]) self._assert_dependency_applied(api, routes) + def test_build_api_with_default_route_dependencies(self, collection, item): + routes = [{"path": "*", "method": "*"}] + test_routes = [ + {"path": "/collections", "method": "POST", "payload": collection}, + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + {"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""}, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + dependencies = [Depends(must_be_bob)] + api = self._build_api(route_dependencies=[(routes, dependencies)]) + self._assert_dependency_applied(api, test_routes) + + def test_build_api_with_default_path_route_dependencies(self, collection, item): + routes = [{"path": "*", "method": "POST"}] + test_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + ] + test_not_routes = [ + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + dependencies = [Depends(must_be_bob)] + api = self._build_api(route_dependencies=[(routes, dependencies)]) + self._assert_dependency_applied(api, test_routes) + self._assert_dependency_not_applied(api, test_not_routes) + + def test_build_api_with_default_method_route_dependencies(self, collection, item): + routes = [ + { + "path": "/collections/{collectionId}", + "method": "*", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "*", + }, + ] + test_routes = [ + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + test_not_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + ] + dependencies = [Depends(must_be_bob)] + api = self._build_api(route_dependencies=[(routes, dependencies)]) + self._assert_dependency_applied(api, test_routes) + self._assert_dependency_not_applied(api, test_not_routes) + + def test_add_default_route_dependencies_after_building_api(self, collection, item): + routes = [{"path": "*", "method": "*"}] + test_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + api = self._build_api() + api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)]) + self._assert_dependency_applied(api, test_routes) + + def test_add_default_path_route_dependencies_after_building_api( + self, collection, item + ): + routes = [{"path": "*", "method": "POST"}] + test_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + ] + test_not_routes = [ + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + api = self._build_api() + api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)]) + self._assert_dependency_applied(api, test_routes) + self._assert_dependency_not_applied(api, test_not_routes) + + def test_add_default_method_route_dependencies_after_building_api( + self, collection, item + ): + routes = [ + { + "path": "/collections/{collectionId}", + "method": "*", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "*", + }, + ] + test_routes = [ + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + test_not_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + ] + api = self._build_api() + api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)]) + self._assert_dependency_applied(api, test_routes) + self._assert_dependency_not_applied(api, test_not_routes) + class DummyCoreClient(core.BaseCoreClient): def all_collections(self, *args, **kwargs):