diff --git a/CHANGES.md b/CHANGES.md index ad626bc5c..abef58e2d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,12 @@ ## [Unreleased] +## [2.5.5] - 2024-04-24 + +### Fixed + +* Fix `service-doc` and `service-desc` url in landing page when using router prefix + ## [2.5.4] - 2024-04-24 ### Fixed diff --git a/stac_fastapi/api/tests/conftest.py b/stac_fastapi/api/tests/conftest.py new file mode 100644 index 000000000..01db30ec2 --- /dev/null +++ b/stac_fastapi/api/tests/conftest.py @@ -0,0 +1,118 @@ +from datetime import datetime +from typing import List, Optional, Union + +import pytest +from stac_pydantic import Collection, Item +from stac_pydantic.api.utils import link_factory + +from stac_fastapi.types import core, stac +from stac_fastapi.types.core import NumType +from stac_fastapi.types.search import BaseSearchPostRequest + +collection_links = link_factory.CollectionLinks("/", "test").create_links() +item_links = link_factory.ItemLinks("/", "test", "test").create_links() + + +@pytest.fixture +def _collection(): + return Collection( + id="test_collection", + title="Test Collection", + description="A test collection", + keywords=["test"], + license="proprietary", + extent={ + "spatial": {"bbox": [[-180, -90, 180, 90]]}, + "temporal": {"interval": [["2000-01-01T00:00:00Z", None]]}, + }, + links=collection_links, + ) + + +@pytest.fixture +def collection(_collection: Collection): + return _collection.json() + + +@pytest.fixture +def collection_dict(_collection: Collection): + return _collection.dict() + + +@pytest.fixture +def _item(): + return Item( + id="test_item", + type="Feature", + geometry={"type": "Point", "coordinates": [0, 0]}, + bbox=[-180, -90, 180, 90], + properties={"datetime": "2000-01-01T00:00:00Z"}, + links=item_links, + assets={}, + ) + + +@pytest.fixture +def item(_item: Item): + return _item.json() + + +@pytest.fixture +def item_dict(_item: Item): + return _item.dict() + + +@pytest.fixture +def TestCoreClient(collection_dict, item_dict): + class CoreClient(core.BaseCoreClient): + def post_search( + self, search_request: BaseSearchPostRequest, **kwargs + ) -> stac.ItemCollection: + return stac.ItemCollection( + type="FeatureCollection", features=[stac.Item(**item_dict)] + ) + + def get_search( + self, + collections: Optional[List[str]] = None, + ids: Optional[List[str]] = None, + bbox: Optional[List[NumType]] = None, + intersects: Optional[str] = None, + datetime: Optional[Union[str, datetime]] = None, + limit: Optional[int] = 10, + **kwargs, + ) -> stac.ItemCollection: + return stac.ItemCollection( + type="FeatureCollection", features=[stac.Item(**item_dict)] + ) + + def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item: + return stac.Item(**item_dict) + + def all_collections(self, **kwargs) -> stac.Collections: + return stac.Collections( + collections=[stac.Collection(**collection_dict)], + links=[ + {"href": "test", "rel": "root"}, + {"href": "test", "rel": "self"}, + {"href": "test", "rel": "parent"}, + ], + ) + + def get_collection(self, collection_id: str, **kwargs) -> stac.Collection: + return stac.Collection(**collection_dict) + + def item_collection( + self, + collection_id: str, + bbox: Optional[List[Union[float, int]]] = None, + datetime: Optional[Union[str, datetime]] = None, + limit: int = 10, + token: str = None, + **kwargs, + ) -> stac.ItemCollection: + return stac.ItemCollection( + type="FeatureCollection", features=[stac.Item(**item_dict)] + ) + + return CoreClient diff --git a/stac_fastapi/api/tests/test_app_prefix.py b/stac_fastapi/api/tests/test_app_prefix.py new file mode 100644 index 000000000..39f099c50 --- /dev/null +++ b/stac_fastapi/api/tests/test_app_prefix.py @@ -0,0 +1,66 @@ +import urllib +from typing import Optional + +import pytest +from fastapi import APIRouter +from starlette.testclient import TestClient + +from stac_fastapi.api.app import StacApi +from stac_fastapi.types.config import ApiSettings + + +def get_link(landing_page, rel_type, method: Optional[str] = None): + return next( + filter( + lambda link: link["rel"] == rel_type + and (not method or link.get("method") == method), + landing_page["links"], + ), + None, + ) + + +@pytest.mark.parametrize("prefix", ["", "/a_prefix"]) +def test_api_prefix(TestCoreClient, prefix): + api_settings = ApiSettings( + openapi_url=f"{prefix}/api", + docs_url=f"{prefix}/api.html", + ) + + api = StacApi( + settings=api_settings, + client=TestCoreClient(), + router=APIRouter(prefix=prefix), + ) + + with TestClient(api.app, base_url="http://stac.io") as client: + landing = client.get(f"{prefix}/") + assert landing.status_code == 200, landing.json() + + 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() + + link_tests = [ + ("root", "application/json", "/"), + ("conformance", "application/json", "/conformance"), + ("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/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 2c18649fa..c6227a573 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -404,9 +404,7 @@ def landing_page(self, **kwargs) -> stac_types.LandingPage: "rel": "service-desc", "type": "application/vnd.oai.openapi+json;version=3.0", "title": "OpenAPI service description", - "href": urljoin( - str(request.base_url), request.app.openapi_url.lstrip("/") - ), + "href": str(request.url_for("openapi")), } ) @@ -416,9 +414,7 @@ def landing_page(self, **kwargs) -> stac_types.LandingPage: "rel": "service-doc", "type": "text/html", "title": "OpenAPI service documentation", - "href": urljoin( - str(request.base_url), request.app.docs_url.lstrip("/") - ), + "href": str(request.url_for("swagger_ui_html")), } )