import os from datetime import datetime, timedelta from typing import Any, Callable, Coroutine, Dict, List, Optional, TypeVar from urllib.parse import quote_plus import orjson import pytest from fastapi import Request from httpx import ASGITransport, AsyncClient from pypgstac.db import PgstacDB from pypgstac.load import Loader from pystac import Collection, Extent, Item, SpatialExtent, TemporalExtent from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model from stac_fastapi.extensions.core import ( CollectionSearchExtension, FieldsExtension, TransactionExtension, ) from stac_fastapi.extensions.core.fields import FieldsConformanceClasses from stac_fastapi.types import stac as stac_types from stac_fastapi.pgstac.config import PostgresSettings from stac_fastapi.pgstac.core import CoreCrudClient, Settings from stac_fastapi.pgstac.db import close_db_connection, connect_to_db from stac_fastapi.pgstac.transactions import TransactionsClient from stac_fastapi.pgstac.types.search import PgstacSearch DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") STAC_CORE_ROUTES = [ "GET /", "GET /collections", "GET /collections/{collection_id}", "GET /collections/{collection_id}/items", "GET /collections/{collection_id}/items/{item_id}", "GET /conformance", "GET /search", "POST /search", ] STAC_TRANSACTION_ROUTES = [ "DELETE /collections/{collection_id}", "DELETE /collections/{collection_id}/items/{item_id}", "POST /collections", "POST /collections/{collection_id}/items", "PUT /collections/{collection_id}", "PUT /collections/{collection_id}/items/{item_id}", ] GLOBAL_BBOX = [-180.0, -90.0, 180.0, 90.0] GLOBAL_GEOMETRY = { "type": "Polygon", "coordinates": ( ( (180.0, -90.0), (180.0, 90.0), (-180.0, 90.0), (-180.0, -90.0), (180.0, -90.0), ), ), } DEFAULT_EXTENT = Extent( SpatialExtent(GLOBAL_BBOX), TemporalExtent([[datetime.now(), None]]), ) async def test_default_app_no_transactions( app_client_no_transaction, load_test_data, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client_no_transaction.post( f"/collections/{coll['id']}/items", json=item ) # the default application does not have the transaction extensions enabled! assert resp.status_code == 405 async def test_post_search_content_type(app_client): params = {"limit": 1} resp = await app_client.post("search", json=params) assert resp.headers["content-type"] == "application/geo+json" async def test_get_search_content_type(app_client): resp = await app_client.get("search") assert resp.headers["content-type"] == "application/geo+json" async def test_landing_links(app_client): """test landing page links.""" landing = await app_client.get("/") assert landing.status_code == 200, landing.text assert "Queryables available for this Catalog" in [ link.get("title") for link in landing.json()["links"] ] async def test_get_queryables_content_type(app_client, load_test_collection): resp = await app_client.get("queryables") assert resp.status_code == 200 assert resp.headers["content-type"] == "application/schema+json" coll = load_test_collection resp = await app_client.get(f"collections/{coll['id']}/queryables") assert resp.status_code == 200 assert resp.headers["content-type"] == "application/schema+json" async def test_get_features_content_type(app_client, load_test_collection): coll = load_test_collection resp = await app_client.get(f"collections/{coll['id']}/items") assert resp.headers["content-type"] == "application/geo+json" async def test_get_features_self_link(app_client, load_test_collection): # https://github.com/stac-utils/stac-fastapi/issues/483 resp = await app_client.get(f"collections/{load_test_collection['id']}/items") assert resp.status_code == 200 resp_json = resp.json() self_link = next((link for link in resp_json["links"] if link["rel"] == "self"), None) assert self_link is not None assert self_link["href"].endswith("/items") async def test_get_feature_content_type(app_client, load_test_collection, load_test_item): resp = await app_client.get( f"collections/{load_test_collection['id']}/items/{load_test_item['id']}" ) assert resp.headers["content-type"] == "application/geo+json" async def test_api_headers(app_client): resp = await app_client.get("/api") assert resp.headers["content-type"] == "application/vnd.oai.openapi+json;version=3.0" assert resp.status_code == 200 async def test_core_router(api_client, app): core_routes = set() for core_route in STAC_CORE_ROUTES: method, path = core_route.split(" ") core_routes.add("{} {}".format(method, app.state.router_prefix + path)) api_routes = { f"{list(route.methods)[0]} {route.path}" for route in api_client.app.routes } assert not core_routes - api_routes async def test_landing_page_stac_extensions(app_client): resp = await app_client.get("/") assert resp.status_code == 200 resp_json = resp.json() assert not resp_json["stac_extensions"] async def test_transactions_router(api_client, app): transaction_routes = set() for transaction_route in STAC_TRANSACTION_ROUTES: method, path = transaction_route.split(" ") transaction_routes.add("{} {}".format(method, app.state.router_prefix + path)) api_routes = { f"{list(route.methods)[0]} {route.path}" for route in api_client.app.routes } assert not transaction_routes - api_routes async def test_app_transaction_extension( app_client, load_test_data, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 async def test_app_query_extension(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"query": {"proj:epsg": {"eq": item["properties"]["proj:epsg"]}}} resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 params["query"] = quote_plus(orjson.dumps(params["query"])) resp = await app_client.get("/search", params=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 async def test_app_query_extension_limit_1( load_test_data, app_client, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"limit": 1} resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 async def test_app_query_extension_limit_eq0(app_client): params = {"limit": 0} resp = await app_client.post("/search", json=params) assert resp.status_code == 400 async def test_app_query_extension_limit_lt0( load_test_data, app_client, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"limit": -1} resp = await app_client.post("/search", json=params) assert resp.status_code == 400 async def test_app_query_extension_limit_gt10000( load_test_data, app_client, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"limit": 10001} resp = await app_client.post("/search", json=params) assert resp.status_code == 200 async def test_app_query_extension_gt(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"query": {"proj:epsg": {"gt": item["properties"]["proj:epsg"]}}} resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 0 async def test_app_query_extension_gte(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"query": {"proj:epsg": {"gte": item["properties"]["proj:epsg"]}}} resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 async def test_app_sort_extension(load_test_data, app_client, load_test_collection): coll = load_test_collection first_item = load_test_data("test_item.json") item_date = datetime.strptime( first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ" ) resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 second_item = load_test_data("test_item.json") second_item["id"] = "another-item" another_item_date = item_date - timedelta(days=1) second_item["properties"]["datetime"] = another_item_date.strftime( "%Y-%m-%dT%H:%M:%SZ" ) resp = await app_client.post(f"/collections/{coll['id']}/items", json=second_item) assert resp.status_code == 201 params = { "collections": [coll["id"]], "sortby": [{"field": "datetime", "direction": "desc"}], } resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert resp_json["features"][0]["id"] == first_item["id"] assert resp_json["features"][1]["id"] == second_item["id"] params = { "collections": [coll["id"]], "sortby": [{"field": "datetime", "direction": "asc"}], } resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert resp_json["features"][1]["id"] == first_item["id"] assert resp_json["features"][0]["id"] == second_item["id"] async def test_search_invalid_date(load_test_data, app_client, load_test_collection): coll = load_test_collection first_item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 params = { "datetime": "2020-XX-01/2020-10-30", "collections": [coll["id"]], } resp = await app_client.post("/search", json=params) assert resp.status_code == 400 async def test_bbox_3d(load_test_data, app_client, load_test_collection): coll = load_test_collection first_item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 australia_bbox = [106.343365, -47.199523, 0.1, 168.218365, -19.437288, 0.1] params = { "bbox": australia_bbox, "collections": [coll["id"]], } resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 async def test_app_search_response(load_test_data, app_client, load_test_collection): coll = load_test_collection params = { "collections": [coll["id"]], } resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert resp_json.get("type") == "FeatureCollection" # stac_version and stac_extensions were removed in v1.0.0-beta.3 assert resp_json.get("stac_version") is None assert resp_json.get("stac_extensions") is None async def test_search_point_intersects(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 new_coordinates = [] for coordinate in item["geometry"]["coordinates"][0]: new_coordinates.append([coordinate[0] * -1, coordinate[1] * -1]) item["id"] = "test-item-other-hemispheres" item["geometry"]["coordinates"] = [new_coordinates] item["bbox"] = [value * -1 for value in item["bbox"]] resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 point = [150.04, -33.14] intersects = {"type": "Point", "coordinates": point} params = { "intersects": intersects, "collections": [item["collection"]], } resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 params["intersects"] = orjson.dumps(params["intersects"]).decode("utf-8") resp = await app_client.get("/search", params=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 async def test_search_line_string_intersects( load_test_data, app_client, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 line = [[150.04, -33.14], [150.22, -33.89]] intersects = {"type": "LineString", "coordinates": line} params = { "intersects": intersects, "collections": [item["collection"]], } resp = await app_client.post("/search", json=params) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 @pytest.mark.asyncio async def test_landing_forwarded_header(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") await app_client.post(f"/collections/{coll['id']}/items", json=item) response = ( await app_client.get( "/", headers={ "Forwarded": "proto=https;host=test:1234", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "4321", }, ) ).json() for link in response["links"]: assert link["href"].startswith("https://test:1234/") @pytest.mark.asyncio async def test_search_forwarded_header(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") await app_client.post(f"/collections/{coll['id']}/items", json=item) resp = await app_client.post( "/search", json={ "collections": [item["collection"]], }, headers={"Forwarded": "proto=https;host=test:1234"}, ) features = resp.json()["features"] assert len(features) > 0 for feature in features: for link in feature["links"]: assert link["href"].startswith("https://test:1234/") @pytest.mark.asyncio async def test_search_x_forwarded_headers( load_test_data, app_client, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") await app_client.post(f"/collections/{coll['id']}/items", json=item) resp = await app_client.post( "/search", json={ "collections": [item["collection"]], }, headers={ "X-Forwarded-Proto": "https", "X-Forwarded-Port": "1234", }, ) features = resp.json()["features"] assert len(features) > 0 for feature in features: for link in feature["links"]: assert link["href"].startswith("https://test:1234/") @pytest.mark.asyncio async def test_search_duplicate_forward_headers( load_test_data, app_client, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") await app_client.post(f"/collections/{coll['id']}/items", json=item) resp = await app_client.post( "/search", json={ "collections": [item["collection"]], }, headers={ "Forwarded": "proto=https;host=test:1234", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "4321", }, ) features = resp.json()["features"] assert len(features) > 0 for feature in features: for link in feature["links"]: assert link["href"].startswith("https://test:1234/") @pytest.mark.asyncio async def test_base_queryables(load_test_data, app_client, load_test_collection): resp = await app_client.get("/queryables") assert resp.status_code == 200 assert resp.headers["Content-Type"] == "application/schema+json" q = resp.json() assert q["$id"].endswith("/queryables") assert q["type"] == "object" assert "properties" in q assert "id" in q["properties"] @pytest.mark.asyncio async def test_collection_queryables(load_test_data, app_client, load_test_collection): resp = await app_client.get("/collections/test-collection/queryables") assert resp.status_code == 200 assert resp.headers["Content-Type"] == "application/schema+json" q = resp.json() assert q["$id"].endswith("/collections/test-collection/queryables") assert q["type"] == "object" assert "properties" in q assert "id" in q["properties"] @pytest.mark.asyncio async def test_get_collections_search( app_client, load_test_collection, load_test2_collection ): # this search should only return a single collection resp = await app_client.get( "/collections", params={"datetime": "2010-01-01T00:00:00Z/2010-01-02T00:00:00Z"}, ) assert len(resp.json()["collections"]) == 1 assert resp.json()["collections"][0]["id"] == load_test2_collection.id # same with this one resp = await app_client.get( "/collections", params={"datetime": "2020-01-01T00:00:00Z/.."}, ) assert len(resp.json()["collections"]) == 1 assert resp.json()["collections"][0]["id"] == load_test_collection["id"] # no params should return both collections resp = await app_client.get( "/collections", ) assert len(resp.json()["collections"]) == 2 # this search should return test collection 1 first resp = await app_client.get( "/collections", params={"sortby": "title"}, ) assert resp.json()["collections"][0]["id"] == load_test_collection["id"] assert resp.json()["collections"][1]["id"] == load_test2_collection.id # this search should return test collection 2 first resp = await app_client.get( "/collections", params={"sortby": "-title"}, ) assert resp.json()["collections"][1]["id"] == load_test_collection["id"] assert resp.json()["collections"][0]["id"] == load_test2_collection.id @pytest.mark.asyncio async def test_item_collection_filter_bbox( load_test_data, app_client, load_test_collection ): coll = load_test_collection first_item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 bbox = "100,-50,170,-20" resp = await app_client.get(f"/collections/{coll['id']}/items", params={"bbox": bbox}) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 bbox = "1,2,3,4" resp = await app_client.get(f"/collections/{coll['id']}/items", params={"bbox": bbox}) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 0 @pytest.mark.asyncio async def test_item_collection_filter_datetime( load_test_data, app_client, load_test_collection ): coll = load_test_collection first_item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 datetime_range = "2020-01-01T00:00:00.00Z/.." resp = await app_client.get( f"/collections/{coll['id']}/items", params={"datetime": datetime_range} ) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 datetime_range = "2018-01-01T00:00:00.00Z/2019-01-01T00:00:00.00Z" resp = await app_client.get( f"/collections/{coll['id']}/items", params={"datetime": datetime_range} ) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 0 @pytest.mark.asyncio async def test_bad_collection_queryables( load_test_data, app_client, load_test_collection ): resp = await app_client.get("/collections/bad-collection/queryables") assert resp.status_code == 404 async def test_deleting_items_with_identical_ids(app_client): collection_a = Collection("collection-a", "The first collection", DEFAULT_EXTENT) collection_b = Collection("collection-b", "The second collection", DEFAULT_EXTENT) item = Item("the-item", GLOBAL_GEOMETRY, GLOBAL_BBOX, datetime.now(), {}) for collection in (collection_a, collection_b): response = await app_client.post( "/collections", json=collection.to_dict(include_self_link=False) ) assert response.status_code == 201 item_as_dict = item.to_dict(include_self_link=False) item_as_dict["collection"] = collection.id response = await app_client.post( f"/collections/{collection.id}/items", json=item_as_dict ) assert response.status_code == 201 response = await app_client.get(f"/collections/{collection.id}/items") assert response.status_code == 200, response.json() assert len(response.json()["features"]) == 1 for collection in (collection_a, collection_b): response = await app_client.delete( f"/collections/{collection.id}/items/{item.id}" ) assert response.status_code == 200, response.json() response = await app_client.get(f"/collections/{collection.id}/items") assert response.status_code == 200, response.json() assert not response.json()["features"] @pytest.mark.parametrize("direction", ("asc", "desc")) async def test_sorting_and_paging(app_client, load_test_collection, direction: str): collection_id = load_test_collection["id"] for i in range(10): item = Item( id=f"item-{i}", geometry={"type": "Point", "coordinates": [-105.1019, 40.1672]}, bbox=[-105.1019, 40.1672, -105.1019, 40.1672], datetime=datetime.now(), properties={ "eo:cloud_cover": 42 + i if i % 3 != 0 else None, }, ) item.collection_id = collection_id response = await app_client.post( f"/collections/{collection_id}/items", json=item.to_dict(include_self_link=False, transform_hrefs=False), ) assert response.status_code == 201 async def search(query: Dict[str, Any]) -> List[Item]: items: List[Item] = [] while True: response = await app_client.post("/search", json=query) json = response.json() assert response.status_code == 200, json items.extend((Item.from_dict(d) for d in json["features"])) next_link = next( (link for link in json["links"] if link["rel"] == "next"), None ) if next_link is None: return items else: query = next_link["body"] query = { "collections": [collection_id], "sortby": [{"field": "properties.eo:cloud_cover", "direction": direction}], "limit": 5, } items = await search(query) assert len(items) == 10, items @pytest.mark.asyncio async def test_wrapped_function(load_test_data, database) -> None: # Ensure wrappers, e.g. Planetary Computer's rate limiting, work. # https://github.com/gadomski/planetary-computer-apis/blob/2719ccf6ead3e06de0784c39a2918d4d1811368b/pccommon/pccommon/redis.py#L205-L238 T = TypeVar("T") def wrap() -> ( Callable[ [Callable[..., Coroutine[Any, Any, T]]], Callable[..., Coroutine[Any, Any, T]], ] ): def decorator( fn: Callable[..., Coroutine[Any, Any, T]], ) -> Callable[..., Coroutine[Any, Any, T]]: async def _wrapper(*args: Any, **kwargs: Any) -> T: request: Optional[Request] = kwargs.get("request") if request: pass # This is where rate limiting would be applied else: raise ValueError(f"Missing request in {fn.__name__}") return await fn(*args, **kwargs) return _wrapper return decorator class Client(CoreCrudClient): @wrap() async def get_collection( self, collection_id: str, request: Request, **kwargs ) -> stac_types.Item: return await super().get_collection(collection_id, request=request, **kwargs) settings = Settings( testing=True, ) postgres_settings = PostgresSettings( pguser=database.user, pgpassword=database.password, pghost=database.host, pgport=database.port, pgdatabase=database.dbname, ) extensions = [ TransactionExtension(client=TransactionsClient(), settings=settings), FieldsExtension(), ] post_request_model = create_post_request_model(extensions, base_model=PgstacSearch) get_request_model = create_get_request_model(extensions) collection_search_extension = CollectionSearchExtension.from_extensions( extensions=[ FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]), ] ) api = StacApi( client=Client(pgstac_search_model=post_request_model), settings=settings, extensions=extensions, search_post_request_model=post_request_model, search_get_request_model=get_request_model, collections_get_request_model=collection_search_extension.GET, ) app = api.app await connect_to_db( app, postgres_settings=postgres_settings, add_write_connection_pool=True, ) try: async with AsyncClient(transport=ASGITransport(app=app)) as client: response = await client.post( "http://test/collections", json=load_test_data("test_collection.json"), ) assert response.status_code == 201 response = await client.post( "http://test/collections/test-collection/items", json=load_test_data("test_item.json"), ) assert response.status_code == 201 response = await client.get( "http://test/collections/test-collection/items/test-item" ) assert response.status_code == 200 finally: await close_db_connection(app) @pytest.mark.asyncio @pytest.mark.parametrize("validation", [True, False]) @pytest.mark.parametrize("hydrate", [True, False]) async def test_no_extension( hydrate, validation, load_test_data, database, pgstac ) -> None: """test PgSTAC with no extension.""" connection = f"postgresql://{database.user}:{quote_plus(database.password)}@{database.host}:{database.port}/{database.dbname}" with PgstacDB(dsn=connection) as db: loader = Loader(db=db) loader.load_collections(os.path.join(DATA_DIR, "test_collection.json")) loader.load_items(os.path.join(DATA_DIR, "test_item.json")) settings = Settings( testing=True, use_api_hydrate=hydrate, enable_response_models=validation, ) postgres_settings = PostgresSettings( pguser=database.user, pgpassword=database.password, pghost=database.host, pgport=database.port, pgdatabase=database.dbname, ) extensions = [] post_request_model = create_post_request_model(extensions, base_model=PgstacSearch) api = StacApi( client=CoreCrudClient(pgstac_search_model=post_request_model), settings=settings, extensions=extensions, search_post_request_model=post_request_model, ) app = api.app await connect_to_db( app, postgres_settings=postgres_settings, add_write_connection_pool=True, ) try: async with AsyncClient(transport=ASGITransport(app=app)) as client: landing = await client.get("http://test/") assert landing.status_code == 200, landing.text assert "Queryables" not in [ link.get("title") for link in landing.json()["links"] ] collection = await client.get("http://test/collections/test-collection") assert collection.status_code == 200, collection.text collections = await client.get("http://test/collections") assert collections.status_code == 200, collections.text # datetime should be ignored collection_datetime = await client.get( "http://test/collections/test-collection", params={ "datetime": "2000-01-01T00:00:00Z/2000-12-31T00:00:00Z", }, ) assert collection_datetime.text == collection.text item = await client.get( "http://test/collections/test-collection/items/test-item" ) assert item.status_code == 200, item.text item_collection = await client.get( "http://test/collections/test-collection/items", params={"limit": 10}, ) assert item_collection.status_code == 200, item_collection.text get_search = await client.get( "http://test/search", params={ "collections": ["test-collection"], }, ) assert get_search.status_code == 200, get_search.text post_search = await client.post( "http://test/search", json={ "collections": ["test-collection"], }, ) assert post_search.status_code == 200, post_search.text get_search = await client.get( "http://test/search", params={ "collections": ["test-collection"], "fields": "properties.datetime", }, ) # fields should be ignored assert get_search.status_code == 200, get_search.text props = get_search.json()["features"][0]["properties"] assert len(props) > 1 post_search = await client.post( "http://test/search", json={ "collections": ["test-collection"], "fields": { "include": ["properties.datetime"], }, }, ) # fields should be ignored assert post_search.status_code == 200, post_search.text props = get_search.json()["features"][0]["properties"] assert len(props) > 1 finally: await close_db_connection(app) async def test_default_app(default_client, default_app, load_test_data): api_routes = { f"{list(route.methods)[0]} {route.path}" for route in default_app.routes } assert set(STAC_CORE_ROUTES).issubset(api_routes) assert set(STAC_TRANSACTION_ROUTES).issubset(api_routes) # Load collections col = load_test_data("test_collection.json") resp = await default_client.post("/collections", json=col) assert resp.status_code == 201 # Load items item = load_test_data("test_item.json") resp = await default_client.post(f"/collections/{col['id']}/items", json=item) assert resp.status_code == 201 resp = await default_client.get("/conformance") assert resp.status_code == 200 conf = resp.json()["conformsTo"] assert ( "https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction" in conf ) assert "https://api.stacspec.org/v1.0.0/collections/extensions/transaction" in conf assert "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2" in conf assert "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query" in conf assert "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core" in conf assert ( "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter" in conf ) assert "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter" in conf assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search" in conf assert "https://api.stacspec.org/v1.0.0/collections" in conf assert "https://api.stacspec.org/v1.0.0/ogcapi-features#query" in conf assert "https://api.stacspec.org/v1.0.0/ogcapi-features#sort" in conf