Repository: stac-utils/stac-fastapi
Pull request #675 (2024-04-25 09:59:40)

fix AsyncBaseCoreClient urls
same as #673 but for Async 🤦 

I think we can simply do a `post` release for this we don't have to got with 2.5.6 



jonhealy1: Do we update the version numbers for pypi? I guess we have to

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 77a7f2c..c0f7686 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,12 @@
 
 ## [Unreleased]
 
+## [2.5.5.post1] - 2024-04-25
+
+### Fixed
+
+* Fix `service-doc` and `service-desc` url in landing page when using router prefix for `AsyncBaseCoreClient`
+
 ## [2.5.5] - 2024-04-24
 
 ### Fixed
diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py
index c6227a5..05c3e10 100644
--- a/stac_fastapi/types/stac_fastapi/types/core.py
+++ b/stac_fastapi/types/stac_fastapi/types/core.py
@@ -612,7 +612,7 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
                 "rel": "service-desc",
                 "type": "application/vnd.oai.openapi+json;version=3.0",
                 "title": "OpenAPI service description",
-                "href": urljoin(base_url, request.app.openapi_url.lstrip("/")),
+                "href": str(request.url_for("openapi")),
             }
         )
 
@@ -622,7 +622,7 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
                 "rel": "service-doc",
                 "type": "text/html",
                 "title": "OpenAPI service documentation",
-                "href": urljoin(base_url, request.app.docs_url.lstrip("/")),
+                "href": str(request.url_for("swagger_ui_html")),
             }
         )
 


Test patch:
diff --git a/stac_fastapi/api/tests/conftest.py b/stac_fastapi/api/tests/conftest.py
index 01db30e..ed8c66d 100644
--- a/stac_fastapi/api/tests/conftest.py
+++ b/stac_fastapi/api/tests/conftest.py
@@ -116,3 +116,61 @@ def TestCoreClient(collection_dict, item_dict):
             )
 
     return CoreClient
+
+
+@pytest.fixture
+def AsyncTestCoreClient(collection_dict, item_dict):
+    class AsyncCoreClient(core.AsyncBaseCoreClient):
+        async def post_search(
+            self, search_request: BaseSearchPostRequest, **kwargs
+        ) -> stac.ItemCollection:
+            return stac.ItemCollection(
+                type="FeatureCollection", features=[stac.Item(**item_dict)]
+            )
+
+        async 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)]
+            )
+
+        async def get_item(
+            self, item_id: str, collection_id: str, **kwargs
+        ) -> stac.Item:
+            return stac.Item(**item_dict)
+
+        async 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"},
+                ],
+            )
+
+        async def get_collection(self, collection_id: str, **kwargs) -> stac.Collection:
+            return stac.Collection(**collection_dict)
+
+        async 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 AsyncCoreClient
diff --git a/stac_fastapi/api/tests/test_app_prefix.py b/stac_fastapi/api/tests/test_app_prefix.py
index 39f099c..0e76e45 100644
--- a/stac_fastapi/api/tests/test_app_prefix.py
+++ b/stac_fastapi/api/tests/test_app_prefix.py
@@ -46,9 +46,89 @@ def test_api_prefix(TestCoreClient, prefix):
         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"]
+        print(collections.json()["links"])
+        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
+
+
+@pytest.mark.parametrize("prefix", ["", "/a_prefix"])
+def test_async_api_prefix(AsyncTestCoreClient, prefix):
+    api_settings = ApiSettings(
+        openapi_url=f"{prefix}/api",
+        docs_url=f"{prefix}/api.html",
+    )
+
+    api = StacApi(
+        settings=api_settings,
+        client=AsyncTestCoreClient(),
+        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()
+
+        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"),
         ]