dz1-spatial-query/stac-fastapi-pgstac/tests/resources/test_collection.py

542 lines
17 KiB
Python
Raw Normal View History

2025-07-03 20:29:02 +08:00
from typing import Callable, Optional
import pystac
import pytest
from stac_pydantic import Collection
from ..conftest import requires_pgstac_0_9_2
async def test_create_collection(app_client, load_test_data: Callable):
in_json = load_test_data("test_collection.json")
in_coll = Collection.model_validate(in_json)
resp = await app_client.post(
"/collections",
json=in_json,
)
assert resp.status_code == 201
post_coll = Collection.model_validate(resp.json())
assert in_coll.model_dump(exclude={"links"}) == post_coll.model_dump(
exclude={"links"}
)
resp = await app_client.get(f"/collections/{post_coll.id}")
assert resp.status_code == 200
get_coll = Collection.model_validate(resp.json())
assert post_coll.model_dump(exclude={"links"}) == get_coll.model_dump(
exclude={"links"}
)
post_coll = post_coll.model_dump(mode="json")
get_coll = get_coll.model_dump(mode="json")
post_self_link = next(
(link for link in post_coll["links"] if link["rel"] == "self"), None
)
get_self_link = next(
(link for link in get_coll["links"] if link["rel"] == "self"), None
)
assert post_self_link is not None and get_self_link is not None
assert post_self_link["href"] == get_self_link["href"]
async def test_update_collection(app_client, load_test_data, load_test_collection):
in_coll = load_test_collection
in_coll["keywords"].append("newkeyword")
resp = await app_client.put(f"/collections/{in_coll['id']}", json=in_coll)
assert resp.status_code == 200
put_coll = Collection.model_validate(resp.json())
resp = await app_client.get(f"/collections/{in_coll['id']}")
assert resp.status_code == 200
get_coll = Collection.model_validate(resp.json())
in_coll = Collection(**in_coll)
assert in_coll.model_dump(exclude={"links"}) == get_coll.model_dump(exclude={"links"})
assert "newkeyword" in get_coll.keywords
get_coll = get_coll.model_dump(mode="json")
put_coll = put_coll.model_dump(mode="json")
put_self_link = next(
(link for link in put_coll["links"] if link["rel"] == "self"), None
)
get_self_link = next(
(link for link in get_coll["links"] if link["rel"] == "self"), None
)
assert put_self_link is not None and get_self_link is not None
assert put_self_link["href"] == get_self_link["href"]
async def test_delete_collection(
app_client, load_test_data: Callable, load_test_collection
):
in_coll = load_test_collection
resp = await app_client.delete(f"/collections/{in_coll['id']}")
assert resp.status_code == 200
resp = await app_client.get(f"/collections/{in_coll['id']}")
assert resp.status_code == 404
async def test_create_collection_conflict(app_client, load_test_data: Callable):
in_json = load_test_data("test_collection.json")
Collection.model_validate(in_json)
resp = await app_client.post(
"/collections",
json=in_json,
)
assert resp.status_code == 201
Collection.model_validate(resp.json())
resp = await app_client.post(
"/collections",
json=in_json,
)
assert resp.status_code == 409
async def test_delete_missing_collection(
app_client,
):
resp = await app_client.delete("/collections")
assert resp.status_code == 405
async def test_update_new_collection(app_client, load_test_collection):
in_coll = load_test_collection
in_coll["id"] = "test-updatenew"
resp = await app_client.put(f"/collections/{in_coll['id']}", json=in_coll)
assert resp.status_code == 404
async def test_nocollections(
app_client,
):
resp = await app_client.get("/collections")
assert resp.status_code == 200
assert resp.json()["numberReturned"] == 0
async def test_returns_valid_collection(app_client, load_test_data):
"""Test updating a collection which already exists"""
in_json = load_test_data("test_collection.json")
resp = await app_client.post(
"/collections",
json=in_json,
)
assert resp.status_code == 201
resp = await app_client.get(f"/collections/{in_json['id']}")
assert resp.status_code == 200
resp_json = resp.json()
# Mock root to allow validation
mock_root = pystac.Catalog(
id="test", description="test desc", href="https://example.com"
)
collection = pystac.Collection.from_dict(
resp_json, root=mock_root, preserve_dict=False
)
collection.validate()
async def test_returns_valid_links_in_collections(app_client, load_test_data):
"""Test links from listing collections"""
in_json = load_test_data("test_collection.json")
resp = await app_client.post(
"/collections",
json=in_json,
)
assert resp.status_code == 201
# Get collection by ID
resp = await app_client.get(f"/collections/{in_json['id']}")
assert resp.status_code == 200
resp_json = resp.json()
# Mock root to allow validation
mock_root = pystac.Catalog(
id="test", description="test desc", href="https://example.com"
)
collection = pystac.Collection.from_dict(
resp_json, root=mock_root, preserve_dict=False
)
assert collection.validate()
# List collections
resp = await app_client.get("/collections")
assert resp.status_code == 200
resp_json = resp.json()
assert resp.json()["numberReturned"]
assert resp.json()["numberMatched"]
collections = resp_json["collections"]
# Find collection in list by ID
single_coll = next(coll for coll in collections if coll["id"] == in_json["id"])
is_coll_from_list_valid = False
single_coll_mocked_link: Optional[pystac.Collection] = None
if single_coll is not None:
single_coll_mocked_link = pystac.Collection.from_dict(
single_coll, root=mock_root, preserve_dict=False
)
is_coll_from_list_valid = single_coll_mocked_link.validate()
assert is_coll_from_list_valid
# Check links from the collection GET and list
assert [
i
for i in collection.to_dict()["links"]
if i not in single_coll_mocked_link.to_dict()["links"]
] == []
async def test_returns_license_link(app_client, load_test_collection):
coll = load_test_collection
resp = await app_client.get(f"/collections/{coll['id']}")
assert resp.status_code == 200
resp_json = resp.json()
link_rel_types = [link["rel"] for link in resp_json["links"]]
assert "license" in link_rel_types
@pytest.mark.asyncio
async def test_get_collection_forwarded_header(app_client, load_test_collection):
coll = load_test_collection
resp = await app_client.get(
f"/collections/{coll['id']}",
headers={"Forwarded": "proto=https;host=test:1234"},
)
for link in [
link
for link in resp.json()["links"]
if link["rel"] in ["items", "parent", "root", "self"]
]:
assert link["href"].startswith("https://test:1234/")
@pytest.mark.asyncio
async def test_get_collection_x_forwarded_headers(app_client, load_test_collection):
coll = load_test_collection
resp = await app_client.get(
f"/collections/{coll['id']}",
headers={
"X-Forwarded-Port": "1234",
"X-Forwarded-Proto": "https",
},
)
for link in [
link
for link in resp.json()["links"]
if link["rel"] in ["items", "parent", "root", "self"]
]:
assert link["href"].startswith("https://test:1234/")
@pytest.mark.asyncio
async def test_get_collection_duplicate_forwarded_headers(
app_client, load_test_collection
):
coll = load_test_collection
resp = await app_client.get(
f"/collections/{coll['id']}",
headers={
"Forwarded": "proto=https;host=test:1234",
"X-Forwarded-Port": "4321",
"X-Forwarded-Proto": "http",
},
)
for link in [
link
for link in resp.json()["links"]
if link["rel"] in ["items", "parent", "root", "self"]
]:
assert link["href"].startswith("https://test:1234/")
@pytest.mark.asyncio
async def test_get_collections_forwarded_header(app_client, load_test_collection):
resp = await app_client.get(
"/collections",
headers={"Forwarded": "proto=https;host=test:1234"},
)
for link in resp.json()["links"]:
assert link["href"].startswith("https://test:1234/")
@pytest.mark.asyncio
async def test_get_collections_queryables_links(app_client, load_test_collection):
resp = await app_client.get(
"/collections",
)
assert "Queryables" in [
link.get("title") for link in resp.json()["collections"][0]["links"]
]
collection_id = resp.json()["collections"][0]["id"]
resp = await app_client.get(
f"/collections/{collection_id}",
)
assert "Queryables" in [link.get("title") for link in resp.json()["links"]]
@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
@requires_pgstac_0_9_2
@pytest.mark.asyncio
async def test_collection_search_freetext(
app_client, load_test_collection, load_test2_collection
):
# free-text
resp = await app_client.get(
"/collections",
params={"q": "temperature"},
)
assert resp.json()["numberReturned"] == 1
assert resp.json()["numberMatched"] == 1
assert len(resp.json()["collections"]) == 1
assert resp.json()["collections"][0]["id"] == load_test2_collection.id
resp = await app_client.get(
"/collections",
params={"q": "nosuchthing"},
)
assert len(resp.json()["collections"]) == 0
@requires_pgstac_0_9_2
@pytest.mark.asyncio
async def test_all_collections_with_pagination(app_client, load_test_data):
data = load_test_data("test_collection.json")
collection_id = data["id"]
for ii in range(0, 12):
data["id"] = collection_id + f"_{ii}"
resp = await app_client.post(
"/collections",
json=data,
)
assert resp.status_code == 201
resp = await app_client.get("/collections")
assert resp.json()["numberReturned"] == 10
assert resp.json()["numberMatched"] == 12
cols = resp.json()["collections"]
assert len(cols) == 10
links = resp.json()["links"]
assert len(links) == 3
assert {"root", "self", "next"} == {link["rel"] for link in links}
resp = await app_client.get("/collections", params={"limit": 12})
assert resp.json()["numberReturned"] == 12
assert resp.json()["numberMatched"] == 12
cols = resp.json()["collections"]
assert len(cols) == 12
links = resp.json()["links"]
assert len(links) == 2
assert {"root", "self"} == {link["rel"] for link in links}
@requires_pgstac_0_9_2
@pytest.mark.asyncio
async def test_all_collections_without_pagination(app_client_no_ext, load_test_data):
data = load_test_data("test_collection.json")
collection_id = data["id"]
for ii in range(0, 12):
data["id"] = collection_id + f"_{ii}"
resp = await app_client_no_ext.post(
"/collections",
json=data,
)
assert resp.status_code == 201
resp = await app_client_no_ext.get("/collections")
assert resp.json()["numberReturned"] == 12
assert resp.json()["numberMatched"] == 12
cols = resp.json()["collections"]
assert len(cols) == 12
links = resp.json()["links"]
assert len(links) == 2
assert {"root", "self"} == {link["rel"] for link in links}
@requires_pgstac_0_9_2
@pytest.mark.asyncio
async def test_get_collections_search_pagination(
app_client, load_test_collection, load_test2_collection
):
resp = await app_client.get("/collections")
assert resp.json()["numberReturned"] == 2
assert resp.json()["numberMatched"] == 2
cols = resp.json()["collections"]
assert len(cols) == 2
links = resp.json()["links"]
assert len(links) == 2
assert {"root", "self"} == {link["rel"] for link in links}
###################
# limit should be positive
resp = await app_client.get("/collections", params={"limit": 0})
assert resp.status_code == 400
###################
# limit=1, should have a `next` link
resp = await app_client.get(
"/collections",
params={"limit": 1},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 1
assert cols[0]["id"] == load_test_collection["id"]
assert len(links) == 3
assert {"root", "self", "next"} == {link["rel"] for link in links}
next_link = list(filter(lambda link: link["rel"] == "next", links))[0]
assert next_link["href"].endswith("?limit=1&offset=1")
###################
# limit=2, there should not be a next link
resp = await app_client.get(
"/collections",
params={"limit": 2},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 2
assert cols[0]["id"] == load_test_collection["id"]
assert cols[1]["id"] == load_test2_collection.id
assert len(links) == 2
assert {"root", "self"} == {link["rel"] for link in links}
###################
# limit=3, there should not be a next/previous link
resp = await app_client.get(
"/collections",
params={"limit": 3},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 2
assert cols[0]["id"] == load_test_collection["id"]
assert cols[1]["id"] == load_test2_collection.id
assert len(links) == 2
assert {"root", "self"} == {link["rel"] for link in links}
###################
# offset=3, because there are 2 collections, we should not have `next` or `prev` links
resp = await app_client.get(
"/collections",
params={"offset": 3},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 0
assert len(links) == 2
assert {"root", "self"} == {link["rel"] for link in links}
###################
# offset=3,limit=1
resp = await app_client.get(
"/collections",
params={"limit": 1, "offset": 3},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 0
assert len(links) == 3
assert {"root", "self", "previous"} == {link["rel"] for link in links}
prev_link = list(filter(lambda link: link["rel"] == "previous", links))[0]
assert prev_link["href"].endswith("?limit=1&offset=2")
###################
# limit=2, offset=3, there should not be a next link
resp = await app_client.get(
"/collections",
params={"limit": 2, "offset": 3},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 0
assert len(links) == 3
assert {"root", "self", "previous"} == {link["rel"] for link in links}
prev_link = list(filter(lambda link: link["rel"] == "previous", links))[0]
assert prev_link["href"].endswith("?limit=2&offset=1")
###################
# offset=1,limit=1 should have a `previous` link
resp = await app_client.get(
"/collections",
params={"offset": 1, "limit": 1},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 1
assert cols[0]["id"] == load_test2_collection.id
assert len(links) == 3
assert {"root", "self", "previous"} == {link["rel"] for link in links}
prev_link = list(filter(lambda link: link["rel"] == "previous", links))[0]
assert "offset" in prev_link["href"]
###################
# offset=0, should not have next/previous link
resp = await app_client.get(
"/collections",
params={"offset": 0},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 2
assert len(links) == 2
assert {"root", "self"} == {link["rel"] for link in links}
@requires_pgstac_0_9_2
@pytest.mark.xfail(strict=False)
@pytest.mark.asyncio
async def test_get_collections_search_offset_1(
app_client, load_test_collection, load_test2_collection
):
# BUG: pgstac doesn't return a `prev` link when limit is not set
# offset=1, should have a `previous` link
resp = await app_client.get(
"/collections",
params={"offset": 1},
)
cols = resp.json()["collections"]
links = resp.json()["links"]
assert len(cols) == 1
assert cols[0]["id"] == load_test2_collection.id
assert len(links) == 3
assert {"root", "self", "previous"} == {link["rel"] for link in links}
prev_link = list(filter(lambda link: link["rel"] == "previous", links))[0]
# offset=0 should not be in the previous link (because it's useless)
assert "offset" not in prev_link["href"]