diff --git a/CHANGES.md b/CHANGES.md index f10a637fd..44f256a96 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ * Add `numberMatched` and `numberReturned` properties in `types.stac.ItemCollection` model * Add `numberMatched` and `numberReturned` properties in `types.stac.Collections` model +* Add `root_path` to `stac_fastapi.types.config.ApiSettings` and use it in the default FastAPI application * Add `python3.13` support ## Changed diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py index 007dde95e..22c85f6e2 100644 --- a/stac_fastapi/api/stac_fastapi/api/app.py +++ b/stac_fastapi/api/stac_fastapi/api/app.py @@ -77,6 +77,7 @@ class StacApi: openapi_url=self.settings.openapi_url, docs_url=self.settings.docs_url, redoc_url=None, + root_path=self.settings.root_path, ), takes_self=True, ), diff --git a/stac_fastapi/api/tests/test_app_prefix.py b/stac_fastapi/api/tests/test_app_prefix.py index 5d604477c..2b52873f6 100644 --- a/stac_fastapi/api/tests/test_app_prefix.py +++ b/stac_fastapi/api/tests/test_app_prefix.py @@ -144,3 +144,67 @@ def test_async_api_prefix(AsyncTestCoreClient, prefix): resp = client.get(prefix + expected_path) assert resp.status_code == 200 + + +@pytest.mark.parametrize("prefix", ["", "/a_prefix"]) +def test_api_prefix_with_root_path(TestCoreClient, prefix): + api_settings = ApiSettings( + openapi_url=f"{prefix}/api", docs_url=f"{prefix}/api.html", root_path="/api/v1" + ) + + api = StacApi( + settings=api_settings, + client=TestCoreClient(), + router=APIRouter(prefix=prefix), + ) + + prefix = "/api/v1" + prefix + with TestClient(api.app, base_url="http://stac.io", root_path="/api/v1") as client: + landing = client.get(f"{prefix}/") + assert landing.status_code == 200, landing.json() + print(landing.text) + service_doc = client.get(f"{prefix}/api.html") + assert service_doc.status_code == 200, service_doc.text + + service_desc = client.get(f"{prefix}/api") + assert service_desc.status_code == 200, service_desc.json() + + conformance = client.get(f"{prefix}/conformance") + assert conformance.status_code == 200, conformance.json() + + # NOTE: The collections/collection/items/item links do not have the prefix + # because they are created in the fixtures + collections = client.get(f"{prefix}/collections") + assert collections.status_code == 200, collections.json() + collection_id = collections.json()["collections"][0]["id"] + + collection = client.get(f"{prefix}/collections/{collection_id}") + assert collection.status_code == 200, collection.json() + + items = client.get(f"{prefix}/collections/{collection_id}/items") + assert items.status_code == 200, items.json() + + item_id = items.json()["features"][0]["id"] + item = client.get(f"{prefix}/collections/{collection_id}/items/{item_id}") + assert item.status_code == 200, item.json() + + link_tests = [ + ("root", "application/json", "/"), + ("conformance", "application/json", "/conformance"), + ("data", "application/json", "/collections"), + ("search", "application/geo+json", "/search"), + ("service-doc", "text/html", "/api.html"), + ("service-desc", "application/vnd.oai.openapi+json;version=3.0", "/api"), + ] + + for rel_type, expected_media_type, expected_path in link_tests: + link = get_link(landing.json(), rel_type) + + assert link is not None, f"Missing {rel_type} link in landing page" + assert link.get("type") == expected_media_type + + link_path = urllib.parse.urlsplit(link.get("href")).path + assert link_path == prefix + expected_path + + resp = client.get(prefix + expected_path) + assert resp.status_code == 200 diff --git a/stac_fastapi/types/stac_fastapi/types/config.py b/stac_fastapi/types/stac_fastapi/types/config.py index 75d0bd399..773ff3646 100644 --- a/stac_fastapi/types/stac_fastapi/types/config.py +++ b/stac_fastapi/types/stac_fastapi/types/config.py @@ -31,6 +31,7 @@ class ApiSettings(BaseSettings): openapi_url: str = "/api" docs_url: str = "/api.html" + root_path: str = "" model_config = SettingsConfigDict(env_file=".env", extra="allow")