dz1-spatial-query/stac-fastapi-pgstac/tests/api/test_api.py

951 lines
33 KiB
Python
Raw Normal View History

2025-07-03 20:29:02 +08:00
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