From 7f1b5e5c17d898cd519bedb2dfcfff715d5c6041 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 21 Oct 2023 13:01:30 +0800 Subject: [PATCH 1/3] fix GET sortby, tests --- .../stac_fastapi/elasticsearch/core.py | 3 +- .../elasticsearch/tests/api/test_api.py | 98 ++++++++++++++++++- stac_fastapi/elasticsearch/tests/conftest.py | 4 +- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py index e2af91a6..84d47a2a 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py @@ -329,9 +329,10 @@ async def get_search( sort_param.append( { "field": sort[1:], - "direction": "asc" if sort[0] == "+" else "desc", + "direction": "desc" if sort[0] == "-" else "asc", } ) + print(sort_param) base_args["sortby"] = sort_param # todo: requires fastapi > 2.3 unreleased diff --git a/stac_fastapi/elasticsearch/tests/api/test_api.py b/stac_fastapi/elasticsearch/tests/api/test_api.py index 7dbf3996..14a3b1b3 100644 --- a/stac_fastapi/elasticsearch/tests/api/test_api.py +++ b/stac_fastapi/elasticsearch/tests/api/test_api.py @@ -2,6 +2,8 @@ import uuid from datetime import datetime, timedelta +import pytest + from ..conftest import create_collection, create_item ROUTES = { @@ -31,17 +33,20 @@ } +@pytest.mark.asyncio async def test_post_search_content_type(app_client, ctx): params = {"limit": 1} resp = await app_client.post("/search", json=params) assert resp.headers["content-type"] == "application/geo+json" +@pytest.mark.asyncio async def test_get_search_content_type(app_client, ctx): resp = await app_client.get("/search") assert resp.headers["content-type"] == "application/geo+json" +@pytest.mark.asyncio async def test_api_headers(app_client): resp = await app_client.get("/api") assert ( @@ -50,11 +55,13 @@ async def test_api_headers(app_client): assert resp.status_code == 200 +@pytest.mark.asyncio async def test_router(app): api_routes = set([f"{list(route.methods)[0]} {route.path}" for route in app.routes]) assert len(api_routes - ROUTES) == 0 +@pytest.mark.asyncio async def test_app_transaction_extension(app_client, ctx): item = copy.deepcopy(ctx.item) item["id"] = str(uuid.uuid4()) @@ -64,6 +71,7 @@ async def test_app_transaction_extension(app_client, ctx): await app_client.delete(f"/collections/{item['collection']}/items/{item['id']}") +@pytest.mark.asyncio async def test_app_search_response(app_client, ctx): resp = await app_client.get("/search", params={"ids": ["test-item"]}) assert resp.status_code == 200 @@ -75,6 +83,7 @@ async def test_app_search_response(app_client, ctx): assert resp_json.get("stac_extensions") is None +@pytest.mark.asyncio async def test_app_context_extension(app_client, ctx, txn_client): test_item = ctx.item test_item["id"] = "test-item-2" @@ -108,6 +117,7 @@ async def test_app_context_extension(app_client, ctx, txn_client): assert matched == 1 +@pytest.mark.asyncio async def test_app_fields_extension(app_client, ctx, txn_client): resp = await app_client.get("/search", params={"collections": ["test-collection"]}) assert resp.status_code == 200 @@ -115,6 +125,7 @@ async def test_app_fields_extension(app_client, ctx, txn_client): assert list(resp_json["features"][0]["properties"]) == ["datetime"] +@pytest.mark.asyncio async def test_app_fields_extension_query(app_client, ctx, txn_client): resp = await app_client.post( "/search", @@ -128,6 +139,7 @@ async def test_app_fields_extension_query(app_client, ctx, txn_client): assert list(resp_json["features"][0]["properties"]) == ["datetime", "proj:epsg"] +@pytest.mark.asyncio async def test_app_fields_extension_no_properties_get(app_client, ctx, txn_client): resp = await app_client.get( "/search", params={"collections": ["test-collection"], "fields": "-properties"} @@ -137,6 +149,7 @@ async def test_app_fields_extension_no_properties_get(app_client, ctx, txn_clien assert "properties" not in resp_json["features"][0] +@pytest.mark.asyncio async def test_app_fields_extension_no_properties_post(app_client, ctx, txn_client): resp = await app_client.post( "/search", @@ -150,6 +163,7 @@ async def test_app_fields_extension_no_properties_post(app_client, ctx, txn_clie assert "properties" not in resp_json["features"][0] +@pytest.mark.asyncio async def test_app_fields_extension_return_all_properties(app_client, ctx, txn_client): item = ctx.item resp = await app_client.get( @@ -166,6 +180,7 @@ async def test_app_fields_extension_return_all_properties(app_client, ctx, txn_c assert feature["properties"][expected_prop] == expected_value +@pytest.mark.asyncio async def test_app_query_extension_gt(app_client, ctx): params = {"query": {"proj:epsg": {"gt": ctx.item["properties"]["proj:epsg"]}}} resp = await app_client.post("/search", json=params) @@ -174,6 +189,7 @@ async def test_app_query_extension_gt(app_client, ctx): assert len(resp_json["features"]) == 0 +@pytest.mark.asyncio async def test_app_query_extension_gte(app_client, ctx): params = {"query": {"proj:epsg": {"gte": ctx.item["properties"]["proj:epsg"]}}} resp = await app_client.post("/search", json=params) @@ -182,21 +198,95 @@ async def test_app_query_extension_gte(app_client, ctx): assert len(resp.json()["features"]) == 1 +@pytest.mark.asyncio async def test_app_query_extension_limit_lt0(app_client): assert (await app_client.post("/search", json={"limit": -1})).status_code == 400 +@pytest.mark.asyncio async def test_app_query_extension_limit_gt10000(app_client): assert (await app_client.post("/search", json={"limit": 10001})).status_code == 400 +@pytest.mark.asyncio async def test_app_query_extension_limit_10000(app_client): params = {"limit": 10000} resp = await app_client.post("/search", json=params) assert resp.status_code == 200 -async def test_app_sort_extension(app_client, txn_client, ctx): +@pytest.mark.asyncio +async def test_app_sort_extension_get_asc(app_client, txn_client, ctx): + first_item = ctx.item + item_date = datetime.strptime( + first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ" + ) + + second_item = dict(first_item) + 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" + ) + await create_item(txn_client, second_item) + + resp = await app_client.get("/search?sortby=+properties.datetime") + 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"] + + +@pytest.mark.asyncio +async def test_app_sort_extension_get_desc(app_client, txn_client, ctx): + first_item = ctx.item + item_date = datetime.strptime( + first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ" + ) + + second_item = dict(first_item) + 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" + ) + await create_item(txn_client, second_item) + + resp = await app_client.get("/search?sortby=-properties.datetime") + 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"] + + +@pytest.mark.asyncio +async def test_app_sort_extension_post_asc(app_client, txn_client, ctx): + first_item = ctx.item + item_date = datetime.strptime( + first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ" + ) + + second_item = dict(first_item) + 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" + ) + await create_item(txn_client, second_item) + + params = { + "collections": [first_item["collection"]], + "sortby": [{"field": "properties.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"] + + +@pytest.mark.asyncio +async def test_app_sort_extension_post_desc(app_client, txn_client, ctx): first_item = ctx.item item_date = datetime.strptime( first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ" @@ -221,6 +311,7 @@ async def test_app_sort_extension(app_client, txn_client, ctx): assert resp_json["features"][1]["id"] == second_item["id"] +@pytest.mark.asyncio async def test_search_invalid_date(app_client, ctx): params = { "datetime": "2020-XX-01/2020-10-30", @@ -231,6 +322,7 @@ async def test_search_invalid_date(app_client, ctx): assert resp.status_code == 400 +@pytest.mark.asyncio async def test_search_point_intersects(app_client, ctx): point = [150.04, -33.14] intersects = {"type": "Point", "coordinates": point} @@ -246,6 +338,7 @@ async def test_search_point_intersects(app_client, ctx): assert len(resp_json["features"]) == 1 +@pytest.mark.asyncio async def test_search_point_does_not_intersect(app_client, ctx): point = [15.04, -3.14] intersects = {"type": "Point", "coordinates": point} @@ -261,6 +354,7 @@ async def test_search_point_does_not_intersect(app_client, ctx): assert len(resp_json["features"]) == 0 +@pytest.mark.asyncio async def test_datetime_non_interval(app_client, ctx): dt_formats = [ "2020-02-12T12:30:22+00:00", @@ -282,6 +376,7 @@ async def test_datetime_non_interval(app_client, ctx): assert resp_json["features"][0]["properties"]["datetime"][0:19] == dt[0:19] +@pytest.mark.asyncio async def test_bbox_3d(app_client, ctx): australia_bbox = [106.343365, -47.199523, 0.1, 168.218365, -19.437288, 0.1] params = { @@ -294,6 +389,7 @@ async def test_bbox_3d(app_client, ctx): assert len(resp_json["features"]) == 1 +@pytest.mark.asyncio async def test_search_line_string_intersects(app_client, ctx): line = [[150.04, -33.14], [150.22, -33.89]] intersects = {"type": "LineString", "coordinates": line} diff --git a/stac_fastapi/elasticsearch/tests/conftest.py b/stac_fastapi/elasticsearch/tests/conftest.py index b755425c..c5de7a69 100644 --- a/stac_fastapi/elasticsearch/tests/conftest.py +++ b/stac_fastapi/elasticsearch/tests/conftest.py @@ -61,7 +61,9 @@ class Config: @pytest.fixture(scope="session") def event_loop(): - return asyncio.get_event_loop() + loop = asyncio.new_event_loop() + yield loop + loop.close() def _load_file(filename: str) -> Dict: From 2573cf1dbbe527eab8c03755459344c1a8febeab Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 21 Oct 2023 13:42:20 +0800 Subject: [PATCH 2/3] update changelog, tests, mark tests --- CHANGELOG.md | 1 + .../tests/clients/test_elasticsearch.py | 15 +++ .../tests/extensions/test_filter.py | 7 ++ .../tests/resources/test_collection.py | 8 ++ .../tests/resources/test_conformance.py | 3 + .../tests/resources/test_item.py | 99 +++++++++---------- .../tests/resources/test_mgmt.py | 4 + 7 files changed, 83 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d2701b..b2559365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Corrected the closing of client connections in ES index management functions [#132](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/132) - Corrected the automatic converstion of float values to int when building Filter Clauses [#135](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/135) - Remove unsupported characters from Elasticsearch index names [#153](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/153) +- Fixed GET /search sortby requests https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/25 ## [v0.3.0] diff --git a/stac_fastapi/elasticsearch/tests/clients/test_elasticsearch.py b/stac_fastapi/elasticsearch/tests/clients/test_elasticsearch.py index e46d4d1f..3da8f86d 100644 --- a/stac_fastapi/elasticsearch/tests/clients/test_elasticsearch.py +++ b/stac_fastapi/elasticsearch/tests/clients/test_elasticsearch.py @@ -11,6 +11,7 @@ from ..conftest import MockRequest, create_item +@pytest.mark.asyncio async def test_create_collection(app_client, ctx, core_client, txn_client): in_coll = deepcopy(ctx.collection) in_coll["id"] = str(uuid.uuid4()) @@ -20,6 +21,7 @@ async def test_create_collection(app_client, ctx, core_client, txn_client): await txn_client.delete_collection(in_coll["id"]) +@pytest.mark.asyncio async def test_create_collection_already_exists(app_client, ctx, txn_client): data = deepcopy(ctx.collection) @@ -32,6 +34,7 @@ async def test_create_collection_already_exists(app_client, ctx, txn_client): await txn_client.delete_collection(data["id"]) +@pytest.mark.asyncio async def test_update_collection( core_client, txn_client, @@ -49,6 +52,7 @@ async def test_update_collection( await txn_client.delete_collection(data["id"]) +@pytest.mark.asyncio async def test_delete_collection( core_client, txn_client, @@ -63,6 +67,7 @@ async def test_delete_collection( await core_client.get_collection(data["id"], request=MockRequest) +@pytest.mark.asyncio async def test_get_collection( core_client, txn_client, @@ -76,6 +81,7 @@ async def test_get_collection( await txn_client.delete_collection(data["id"]) +@pytest.mark.asyncio async def test_get_item(app_client, ctx, core_client): got_item = await core_client.get_item( item_id=ctx.item["id"], @@ -86,6 +92,7 @@ async def test_get_item(app_client, ctx, core_client): assert got_item["collection"] == ctx.item["collection"] +@pytest.mark.asyncio async def test_get_collection_items(app_client, ctx, core_client, txn_client): coll = ctx.collection num_of_items_to_create = 5 @@ -106,6 +113,7 @@ async def test_get_collection_items(app_client, ctx, core_client, txn_client): assert item["collection"] == coll["id"] +@pytest.mark.asyncio async def test_create_item(ctx, core_client, txn_client): resp = await core_client.get_item( ctx.item["id"], ctx.item["collection"], request=MockRequest @@ -115,6 +123,7 @@ async def test_create_item(ctx, core_client, txn_client): ) == Item(**resp).dict(exclude={"links": ..., "properties": {"created", "updated"}}) +@pytest.mark.asyncio async def test_create_item_already_exists(ctx, txn_client): with pytest.raises(ConflictError): await txn_client.create_item( @@ -125,6 +134,7 @@ async def test_create_item_already_exists(ctx, txn_client): ) +@pytest.mark.asyncio async def test_update_item(ctx, core_client, txn_client): ctx.item["properties"]["foo"] = "bar" collection_id = ctx.item["collection"] @@ -139,6 +149,7 @@ async def test_update_item(ctx, core_client, txn_client): assert updated_item["properties"]["foo"] == "bar" +@pytest.mark.asyncio async def test_update_geometry(ctx, core_client, txn_client): new_coordinates = [ [ @@ -163,6 +174,7 @@ async def test_update_geometry(ctx, core_client, txn_client): assert updated_item["geometry"]["coordinates"] == new_coordinates +@pytest.mark.asyncio async def test_delete_item(ctx, core_client, txn_client): await txn_client.delete_item(ctx.item["id"], ctx.item["collection"]) @@ -172,6 +184,7 @@ async def test_delete_item(ctx, core_client, txn_client): ) +@pytest.mark.asyncio async def test_bulk_item_insert(ctx, core_client, txn_client, bulk_txn_client): items = {} for _ in range(10): @@ -193,6 +206,7 @@ async def test_bulk_item_insert(ctx, core_client, txn_client, bulk_txn_client): # ) +@pytest.mark.asyncio async def test_feature_collection_insert( core_client, txn_client, @@ -212,6 +226,7 @@ async def test_feature_collection_insert( assert len(fc["features"]) >= 10 +@pytest.mark.asyncio async def test_landing_page_no_collection_title(ctx, core_client, txn_client, app): ctx.collection["id"] = "new_id" del ctx.collection["title"] diff --git a/stac_fastapi/elasticsearch/tests/extensions/test_filter.py b/stac_fastapi/elasticsearch/tests/extensions/test_filter.py index 43aadf18..f20ab4ea 100644 --- a/stac_fastapi/elasticsearch/tests/extensions/test_filter.py +++ b/stac_fastapi/elasticsearch/tests/extensions/test_filter.py @@ -3,9 +3,12 @@ from os import listdir from os.path import isfile, join +import pytest + THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +@pytest.mark.asyncio async def test_search_filters(app_client, ctx): filters = [] @@ -19,6 +22,7 @@ async def test_search_filters(app_client, ctx): assert resp.status_code == 200 +@pytest.mark.asyncio async def test_search_filter_extension_eq(app_client, ctx): params = {"filter": {"op": "=", "args": [{"property": "id"}, ctx.item["id"]]}} resp = await app_client.post("/search", json=params) @@ -27,6 +31,7 @@ async def test_search_filter_extension_eq(app_client, ctx): assert len(resp_json["features"]) == 1 +@pytest.mark.asyncio async def test_search_filter_extension_gte(app_client, ctx): # there's one item that can match, so one of these queries should match it and the other shouldn't params = { @@ -58,6 +63,7 @@ async def test_search_filter_extension_gte(app_client, ctx): assert len(resp.json()["features"]) == 0 +@pytest.mark.asyncio async def test_search_filter_ext_and(app_client, ctx): params = { "filter": { @@ -80,6 +86,7 @@ async def test_search_filter_ext_and(app_client, ctx): assert len(resp.json()["features"]) == 1 +@pytest.mark.asyncio async def test_search_filter_extension_floats(app_client, ctx): sun_elevation = ctx.item["properties"]["view:sun_elevation"] diff --git a/stac_fastapi/elasticsearch/tests/resources/test_collection.py b/stac_fastapi/elasticsearch/tests/resources/test_collection.py index f37b36b0..22c35221 100644 --- a/stac_fastapi/elasticsearch/tests/resources/test_collection.py +++ b/stac_fastapi/elasticsearch/tests/resources/test_collection.py @@ -1,6 +1,8 @@ import pystac +import pytest +@pytest.mark.asyncio async def test_create_and_delete_collection(app_client, load_test_data): """Test creation and deletion of a collection""" test_collection = load_test_data("test_collection.json") @@ -13,6 +15,7 @@ async def test_create_and_delete_collection(app_client, load_test_data): assert resp.status_code == 204 +@pytest.mark.asyncio async def test_create_collection_conflict(app_client, ctx): """Test creation of a collection which already exists""" # This collection ID is created in the fixture, so this should be a conflict @@ -20,12 +23,14 @@ async def test_create_collection_conflict(app_client, ctx): assert resp.status_code == 409 +@pytest.mark.asyncio async def test_delete_missing_collection(app_client): """Test deletion of a collection which does not exist""" resp = await app_client.delete("/collections/missing-collection") assert resp.status_code == 404 +@pytest.mark.asyncio async def test_update_collection_already_exists(ctx, app_client): """Test updating a collection which already exists""" ctx.collection["keywords"].append("test") @@ -38,6 +43,7 @@ async def test_update_collection_already_exists(ctx, app_client): assert "test" in resp_json["keywords"] +@pytest.mark.asyncio async def test_update_new_collection(app_client, load_test_data): """Test updating a collection which does not exist (same as creation)""" test_collection = load_test_data("test_collection.json") @@ -47,12 +53,14 @@ async def test_update_new_collection(app_client, load_test_data): assert resp.status_code == 404 +@pytest.mark.asyncio async def test_collection_not_found(app_client): """Test read a collection which does not exist""" resp = await app_client.get("/collections/does-not-exist") assert resp.status_code == 404 +@pytest.mark.asyncio async def test_returns_valid_collection(ctx, app_client): """Test validates fetched collection with jsonschema""" resp = await app_client.put("/collections", json=ctx.collection) diff --git a/stac_fastapi/elasticsearch/tests/resources/test_conformance.py b/stac_fastapi/elasticsearch/tests/resources/test_conformance.py index ab70a00b..d93d8b81 100644 --- a/stac_fastapi/elasticsearch/tests/resources/test_conformance.py +++ b/stac_fastapi/elasticsearch/tests/resources/test_conformance.py @@ -20,6 +20,7 @@ def get_link(landing_page, rel_type): ) +@pytest.mark.asyncio async def test_landing_page_health(response): """Test landing page""" assert response.status_code == 200 @@ -39,6 +40,7 @@ async def test_landing_page_health(response): ] +@pytest.mark.asyncio @pytest.mark.parametrize("rel_type,expected_media_type,expected_path", link_tests) async def test_landing_page_links( response_json, app_client, rel_type, expected_media_type, expected_path @@ -59,6 +61,7 @@ async def test_landing_page_links( # code here seems meaningless since it would be the same as if the endpoint did not exist. Once # https://github.com/stac-utils/stac-fastapi/pull/227 has been merged we can add this to the # parameterized tests above. +@pytest.mark.asyncio async def test_search_link(response_json): search_link = get_link(response_json, "search") diff --git a/stac_fastapi/elasticsearch/tests/resources/test_item.py b/stac_fastapi/elasticsearch/tests/resources/test_item.py index 76f38f79..5b382873 100644 --- a/stac_fastapi/elasticsearch/tests/resources/test_item.py +++ b/stac_fastapi/elasticsearch/tests/resources/test_item.py @@ -23,6 +23,7 @@ def rfc3339_str_to_datetime(s: str) -> datetime: return ciso8601.parse_rfc3339(s) +@pytest.mark.asyncio async def test_create_and_delete_item(app_client, ctx, txn_client): """Test creation and deletion of a single item (transactions extension)""" @@ -46,6 +47,7 @@ async def test_create_and_delete_item(app_client, ctx, txn_client): assert resp.status_code == 404 +@pytest.mark.asyncio async def test_create_item_conflict(app_client, ctx): """Test creation of an item which already exists (transactions extension)""" @@ -57,6 +59,7 @@ async def test_create_item_conflict(app_client, ctx): assert resp.status_code == 409 +@pytest.mark.asyncio async def test_delete_missing_item(app_client, load_test_data): """Test deletion of an item which does not exist (transactions extension)""" test_item = load_test_data("test_item.json") @@ -66,6 +69,7 @@ async def test_delete_missing_item(app_client, load_test_data): assert resp.status_code == 404 +@pytest.mark.asyncio async def test_create_item_missing_collection(app_client, ctx): """Test creation of an item without a parent collection (transactions extension)""" ctx.item["collection"] = "stac_is_cool" @@ -75,6 +79,7 @@ async def test_create_item_missing_collection(app_client, ctx): assert resp.status_code == 404 +@pytest.mark.asyncio async def test_create_uppercase_collection_with_item(app_client, ctx, txn_client): """Test creation of a collection and item with uppercase collection ID (transactions extension)""" collection_id = "UPPERCASE" @@ -87,6 +92,7 @@ async def test_create_uppercase_collection_with_item(app_client, ctx, txn_client assert resp.status_code == 200 +@pytest.mark.asyncio async def test_update_item_already_exists(app_client, ctx): """Test updating an item which already exists (transactions extension)""" @@ -106,6 +112,7 @@ async def test_update_item_already_exists(app_client, ctx): ) +@pytest.mark.asyncio async def test_update_new_item(app_client, ctx): """Test updating an item which does not exist (transactions extension)""" test_item = ctx.item @@ -118,6 +125,7 @@ async def test_update_new_item(app_client, ctx): assert resp.status_code == 404 +@pytest.mark.asyncio async def test_update_item_missing_collection(app_client, ctx): """Test updating an item without a parent collection (transactions extension)""" # Try to update collection of the item @@ -128,6 +136,7 @@ async def test_update_item_missing_collection(app_client, ctx): assert resp.status_code == 404 +@pytest.mark.asyncio async def test_update_item_geometry(app_client, ctx): ctx.item["id"] = "update_test_item_1" @@ -162,6 +171,7 @@ async def test_update_item_geometry(app_client, ctx): assert resp.json()["geometry"]["coordinates"] == new_coordinates +@pytest.mark.asyncio async def test_get_item(app_client, ctx): """Test read an item by id (core)""" get_item = await app_client.get( @@ -170,6 +180,7 @@ async def test_get_item(app_client, ctx): assert get_item.status_code == 200 +@pytest.mark.asyncio async def test_returns_valid_item(app_client, ctx): """Test validates fetched item with jsonschema""" test_item = ctx.item @@ -186,6 +197,7 @@ async def test_returns_valid_item(app_client, ctx): item.validate() +@pytest.mark.asyncio async def test_get_item_collection(app_client, ctx, txn_client): """Test read an item collection (core)""" item_count = randint(1, 4) @@ -202,6 +214,7 @@ async def test_get_item_collection(app_client, ctx, txn_client): assert matched == item_count + 1 +@pytest.mark.asyncio async def test_item_collection_filter_bbox(app_client, ctx): item = ctx.item collection = item["collection"] @@ -223,6 +236,7 @@ async def test_item_collection_filter_bbox(app_client, ctx): assert len(resp_json["features"]) == 0 +@pytest.mark.asyncio async def test_item_collection_filter_datetime(app_client, ctx): item = ctx.item collection = item["collection"] @@ -244,6 +258,7 @@ async def test_item_collection_filter_datetime(app_client, ctx): assert len(resp_json["features"]) == 0 +@pytest.mark.asyncio @pytest.mark.skip(reason="Pagination extension not implemented") async def test_pagination(app_client, load_test_data): """Test item collection pagination (paging extension)""" @@ -272,6 +287,7 @@ async def test_pagination(app_client, load_test_data): assert second_page["context"]["returned"] == 3 +@pytest.mark.asyncio async def test_item_timestamps(app_client, ctx): """Test created and updated timestamps (common metadata)""" # start_time = now_to_rfc3339_str() @@ -300,6 +316,7 @@ async def test_item_timestamps(app_client, ctx): ) +@pytest.mark.asyncio async def test_item_search_by_id_post(app_client, ctx, txn_client): """Test POST search by item id (core)""" ids = ["test1", "test2", "test3"] @@ -315,6 +332,7 @@ async def test_item_search_by_id_post(app_client, ctx, txn_client): assert set([feat["id"] for feat in resp_json["features"]]) == set(ids) +@pytest.mark.asyncio async def test_item_search_spatial_query_post(app_client, ctx): """Test POST search with spatial query (core)""" test_item = ctx.item @@ -329,6 +347,7 @@ async def test_item_search_spatial_query_post(app_client, ctx): assert resp_json["features"][0]["id"] == test_item["id"] +@pytest.mark.asyncio async def test_item_search_temporal_query_post(app_client, ctx): """Test POST search with single-tailed spatio-temporal query (core)""" @@ -347,6 +366,7 @@ async def test_item_search_temporal_query_post(app_client, ctx): assert resp_json["features"][0]["id"] == test_item["id"] +@pytest.mark.asyncio async def test_item_search_temporal_window_post(app_client, ctx): """Test POST search with two-tailed spatio-temporal query (core)""" test_item = ctx.item @@ -365,6 +385,7 @@ async def test_item_search_temporal_window_post(app_client, ctx): assert resp_json["features"][0]["id"] == test_item["id"] +@pytest.mark.asyncio @pytest.mark.skip(reason="KeyError: 'features") async def test_item_search_temporal_open_window(app_client, ctx): """Test POST search with open spatio-temporal query (core)""" @@ -379,39 +400,7 @@ async def test_item_search_temporal_open_window(app_client, ctx): assert resp_json["features"][0]["id"] == test_item["id"] -@pytest.mark.skip(reason="sortby date not implemented") -async def test_item_search_sort_post(app_client, load_test_data): - """Test POST search with sorting (sort extension)""" - first_item = load_test_data("test_item.json") - item_date = rfc3339_str_to_datetime(first_item["properties"]["datetime"]) - resp = await app_client.post( - f"/collections/{first_item['collection']}/items", json=first_item - ) - assert resp.status_code == 200 - - 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"] = datetime_to_str(another_item_date) - resp = await app_client.post( - f"/collections/{second_item['collection']}/items", json=second_item - ) - assert resp.status_code == 200 - - params = { - "collections": [first_item["collection"]], - "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"] - await app_client.delete( - f"/collections/{first_item['collection']}/items/{first_item['id']}" - ) - - +@pytest.mark.asyncio async def test_item_search_by_id_get(app_client, ctx, txn_client): """Test GET search by item id (core)""" ids = ["test1", "test2", "test3"] @@ -427,6 +416,7 @@ async def test_item_search_by_id_get(app_client, ctx, txn_client): assert set([feat["id"] for feat in resp_json["features"]]) == set(ids) +@pytest.mark.asyncio async def test_item_search_bbox_get(app_client, ctx): """Test GET search with spatial query (core)""" params = { @@ -439,6 +429,7 @@ async def test_item_search_bbox_get(app_client, ctx): assert resp_json["features"][0]["id"] == ctx.item["id"] +@pytest.mark.asyncio async def test_item_search_get_without_collections(app_client, ctx): """Test GET search without specifying collections""" @@ -449,6 +440,7 @@ async def test_item_search_get_without_collections(app_client, ctx): assert resp.status_code == 200 +@pytest.mark.asyncio async def test_item_search_get_with_non_existent_collections(app_client, ctx): """Test GET search with non-existent collections""" @@ -457,6 +449,7 @@ async def test_item_search_get_with_non_existent_collections(app_client, ctx): assert resp.status_code == 200 +@pytest.mark.asyncio async def test_item_search_temporal_window_get(app_client, ctx): """Test GET search with spatio-temporal query (core)""" test_item = ctx.item @@ -474,27 +467,7 @@ async def test_item_search_temporal_window_get(app_client, ctx): assert resp_json["features"][0]["id"] == test_item["id"] -@pytest.mark.skip(reason="sorting not fully implemented") -async def test_item_search_sort_get(app_client, ctx, txn_client): - """Test GET search with sorting (sort extension)""" - first_item = ctx.item - item_date = rfc3339_str_to_datetime(first_item["properties"]["datetime"]) - await create_item(txn_client, ctx.item) - - second_item = ctx.item.copy() - second_item["id"] = "another-item" - another_item_date = item_date - timedelta(days=1) - second_item.update({"properties": {"datetime": datetime_to_str(another_item_date)}}) - await create_item(txn_client, second_item) - - params = {"collections": [first_item["collection"]], "sortby": "-datetime"} - resp = await app_client.get("/search", params=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"] - - +@pytest.mark.asyncio async def test_item_search_post_without_collection(app_client, ctx): """Test POST search without specifying a collection""" test_item = ctx.item @@ -505,6 +478,7 @@ async def test_item_search_post_without_collection(app_client, ctx): assert resp.status_code == 200 +@pytest.mark.asyncio async def test_item_search_properties_es(app_client, ctx): """Test POST search with JSONB query (query extension)""" @@ -517,6 +491,7 @@ async def test_item_search_properties_es(app_client, ctx): assert len(resp_json["features"]) == 0 +@pytest.mark.asyncio async def test_item_search_properties_field(app_client): """Test POST search indexed field with query (query extension)""" @@ -528,6 +503,7 @@ async def test_item_search_properties_field(app_client): assert len(resp_json["features"]) == 0 +@pytest.mark.asyncio async def test_item_search_get_query_extension(app_client, ctx): """Test GET search with JSONB query (query extension)""" @@ -554,12 +530,14 @@ async def test_item_search_get_query_extension(app_client, ctx): ) +@pytest.mark.asyncio async def test_get_missing_item_collection(app_client): """Test reading a collection which does not exist""" resp = await app_client.get("/collections/invalid-collection/items") assert resp.status_code == 404 +@pytest.mark.asyncio async def test_pagination_item_collection(app_client, ctx, txn_client): """Test item collection pagination links (paging extension)""" ids = [ctx.item["id"]] @@ -596,6 +574,7 @@ async def test_pagination_item_collection(app_client, ctx, txn_client): assert not set(item_ids) - set(ids) +@pytest.mark.asyncio async def test_pagination_post(app_client, ctx, txn_client): """Test POST pagination (paging extension)""" ids = [ctx.item["id"]] @@ -631,6 +610,7 @@ async def test_pagination_post(app_client, ctx, txn_client): assert not set(item_ids) - set(ids) +@pytest.mark.asyncio async def test_pagination_token_idempotent(app_client, ctx, txn_client): """Test that pagination tokens are idempotent (paging extension)""" ids = [ctx.item["id"]] @@ -661,6 +641,7 @@ async def test_pagination_token_idempotent(app_client, ctx, txn_client): ] +@pytest.mark.asyncio async def test_field_extension_get_includes(app_client, ctx): """Test GET search with included fields (fields extension)""" test_item = ctx.item @@ -673,6 +654,7 @@ async def test_field_extension_get_includes(app_client, ctx): assert not set(feat_properties) - {"proj:epsg", "gsd", "datetime"} +@pytest.mark.asyncio async def test_field_extension_get_excludes(app_client, ctx): """Test GET search with included fields (fields extension)""" test_item = ctx.item @@ -686,6 +668,7 @@ async def test_field_extension_get_excludes(app_client, ctx): assert "gsd" not in resp_json["features"][0]["properties"].keys() +@pytest.mark.asyncio async def test_field_extension_post(app_client, ctx): """Test POST search with included and excluded fields (fields extension)""" test_item = ctx.item @@ -707,6 +690,7 @@ async def test_field_extension_post(app_client, ctx): } +@pytest.mark.asyncio async def test_field_extension_exclude_and_include(app_client, ctx): """Test POST search including/excluding same field (fields extension)""" test_item = ctx.item @@ -723,6 +707,7 @@ async def test_field_extension_exclude_and_include(app_client, ctx): assert "eo:cloud_cover" not in resp_json["features"][0]["properties"] +@pytest.mark.asyncio async def test_field_extension_exclude_default_includes(app_client, ctx): """Test POST search excluding a forbidden field (fields extension)""" test_item = ctx.item @@ -733,6 +718,7 @@ async def test_field_extension_exclude_default_includes(app_client, ctx): assert "gsd" not in resp_json["features"][0] +@pytest.mark.asyncio async def test_search_intersects_and_bbox(app_client): """Test POST search intersects and bbox are mutually exclusive (core)""" bbox = [-118, 34, -117, 35] @@ -742,6 +728,7 @@ async def test_search_intersects_and_bbox(app_client): assert resp.status_code == 400 +@pytest.mark.asyncio async def test_get_missing_item(app_client, load_test_data): """Test read item which does not exist (transactions extension)""" test_coll = load_test_data("test_collection.json") @@ -749,6 +736,7 @@ async def test_get_missing_item(app_client, load_test_data): assert resp.status_code == 404 +@pytest.mark.asyncio @pytest.mark.skip(reason="invalid queries not implemented") async def test_search_invalid_query_field(app_client): body = {"query": {"gsd": {"lt": 100}, "invalid-field": {"eq": 50}}} @@ -756,6 +744,7 @@ async def test_search_invalid_query_field(app_client): assert resp.status_code == 400 +@pytest.mark.asyncio async def test_search_bbox_errors(app_client): body = {"query": {"bbox": [0]}} resp = await app_client.post("/search", json=body) @@ -770,6 +759,7 @@ async def test_search_bbox_errors(app_client): assert resp.status_code == 400 +@pytest.mark.asyncio async def test_conformance_classes_configurable(): """Test conformance class configurability""" landing = LandingPageMixin() @@ -787,6 +777,7 @@ async def test_conformance_classes_configurable(): assert client.conformance_classes()[0] == "this is a test" +@pytest.mark.asyncio async def test_search_datetime_validation_errors(app_client): bad_datetimes = [ "37-01-01T12:00:27.87Z", diff --git a/stac_fastapi/elasticsearch/tests/resources/test_mgmt.py b/stac_fastapi/elasticsearch/tests/resources/test_mgmt.py index 9d2bc3dc..2b7d9728 100644 --- a/stac_fastapi/elasticsearch/tests/resources/test_mgmt.py +++ b/stac_fastapi/elasticsearch/tests/resources/test_mgmt.py @@ -1,3 +1,7 @@ +import pytest + + +@pytest.mark.asyncio async def test_ping_no_param(app_client): """ Test ping endpoint with a mocked client. From 9bad4c5f84a9639bd64a0eb217bc2a0ef3fd68d3 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 21 Oct 2023 11:23:28 +0300 Subject: [PATCH 3/3] fix link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2559365..3cf6552d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Corrected the closing of client connections in ES index management functions [#132](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/132) - Corrected the automatic converstion of float values to int when building Filter Clauses [#135](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/135) - Remove unsupported characters from Elasticsearch index names [#153](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/153) -- Fixed GET /search sortby requests https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/25 +- Fixed GET /search sortby requests [#25](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/25) ## [v0.3.0]