Skip to content

Commit 0bfea66

Browse files
authored
Merge pull request #979 from RS-PYTHON/fix/rspy542-invalid-service-desc-link
fix-rspy542-invalid-service-desc-link
2 parents edb256f + 4a471a0 commit 0bfea66

File tree

5 files changed

+121
-8
lines changed

5 files changed

+121
-8
lines changed

services/catalog/rs_server_catalog/user_catalog.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"coordinates": [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]],
9696
}
9797
DEFAULT_BBOX = (-180.0, -90.0, 180.0, 90.0)
98+
QUERYABLES = "/queryables"
9899
# pylint: disable=too-many-lines
99100
logger = Logging.default(__name__)
100101

@@ -961,7 +962,7 @@ async def manage_get_response( # pylint: disable=too-many-locals, too-many-bran
961962
)
962963
# I don't know why but the STAC browser doesn't send authentication for the queryables endpoint.
963964
# So allow this endpoint without authentication in this specific case.
964-
and not (common_settings.request_from_stacbrowser(request) and request.url.path.endswith("/queryables"))
965+
and not (common_settings.request_from_stacbrowser(request) and request.url.path.endswith(QUERYABLES))
965966
):
966967
detail = {"error": "Unauthorized access."}
967968
return JSONResponse(content=detail, status_code=HTTP_401_UNAUTHORIZED)
@@ -1344,6 +1345,25 @@ async def dispatch(
13441345
response_content = json.loads(b"".join(body).decode()) # type:ignore
13451346
self.clear_catalog_bucket(response_content)
13461347

1348+
# GET: '/catalog/queryables' when no collections in the catalog
1349+
if (
1350+
request.method == "GET"
1351+
and request.scope["path"] == QUERYABLES
1352+
and not self.request_ids["collection_ids"]
1353+
and response_content["code"] == "NotFoundError"
1354+
):
1355+
# Return empty list of properties and additionalProperties set to true on /catalog/queryables
1356+
# when there are no collections in catalog.
1357+
empty_catalog_queryables_response = {
1358+
"$id": f"{request.base_url}queryables",
1359+
"type": "object",
1360+
"title": "STAC Queryables.",
1361+
"$schema": "https://json-schema.org/draft-07/schema#",
1362+
"properties": {},
1363+
"additionalProperties": True,
1364+
}
1365+
return JSONResponse(status_code=HTTP_200_OK, content=empty_catalog_queryables_response)
1366+
13471367
# Return a regular JSON response instead of StreamingResponse because the body cannot be read again.
13481368
return JSONResponse(status_code=response.status_code, content=response_content)
13491369

@@ -1356,7 +1376,7 @@ async def dispatch(
13561376
response = await self.manage_download_response(request, response)
13571377

13581378
elif request.method == "GET" and (
1359-
self.request_ids["owner_id"] or request.scope["path"] in ["/", "/collections", "/queryables"]
1379+
self.request_ids["owner_id"] or request.scope["path"] in ["/", "/collections", QUERYABLES]
13601380
):
13611381
# URL: GET: '/catalog/collections/{USER}:{COLLECTION}'
13621382
# URL: GET: '/catalog/'

services/catalog/rs_server_catalog/user_handler.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
COLLECTIONS_SEARCH_REGEX = r"/catalog/collections/((?P<owner_id>.+):)?(?P<collection_id>.+)/search"
3535
BULK_ITEMS_REGEX = r"/catalog/collections/((?P<owner_id>.+):)?(?P<collection_id>.+)/bulk_items"
3636
CATALOG_COLLECTION = "/catalog/collections"
37+
CATALOG_PREFIX = "/catalog"
3738

3839
# Regexp for search endpoints
3940
CATALOG_SEARCH = "/catalog/search"
@@ -83,19 +84,19 @@ def reroute_url( # type: ignore # pylint: disable=too-many-branches,too-many-st
8384
# Catch one endpoint of the following list
8485
regexp_list = [
8586
"/",
87+
"/catalog",
8688
"/catalog/",
8789
"/catalog/search",
8890
"/catalog/queryables",
89-
"catalog/api",
91+
"/catalog/api",
9092
"/catalog/api.html",
9193
"/catalog/docs/oauth2-redirect",
92-
"/catalog/queryables",
9394
"/catalog/conformance",
9495
]
9596
is_in_regexp_list = False
9697
for pattern in regexp_list:
9798
if re.fullmatch(pattern, path):
98-
path = path.replace("/catalog", "") if path != "/" else path
99+
path = path.replace(CATALOG_PREFIX, "") or "/"
99100
is_in_regexp_list = True
100101
break
101102
if is_in_regexp_list:
@@ -134,7 +135,7 @@ def reroute_url( # type: ignore # pylint: disable=too-many-branches,too-many-st
134135
ids_dict["owner_id"] = get_user(groups["owner_id"], ids_dict["user_login"])
135136
ids_dict["collection_ids"].append(f"{ids_dict['owner_id']}_{groups['collection_id']}")
136137
# This endpoint will be redirected to the "/search" endpoint later in the code
137-
path = path.replace("/catalog", "")
138+
path = path.replace(CATALOG_PREFIX, "")
138139

139140
# Catch all other endpoints.
140141
elif match := re.match(CATALOG_OWNER_ID_STAC_ENDPOINT_REGEX, path):

services/catalog/tests/conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,20 @@ def client_fixture(start_database): # pylint: disable=missing-function-docstrin
113113
yield client
114114

115115

116+
@pytest.mark.integration
117+
@pytest.fixture(scope="session", name="client_with_empty_catalog")
118+
def client_empty_catalog_fixture(start_database): # pylint: disable=missing-function-docstring, unused-argument
119+
"""Client with an empty catalog (no collections added)."""
120+
with TestClient(app, follow_redirects=False) as client:
121+
# Ensure the catalog is empty by deleting all collections
122+
response = client.get("/catalog/collections")
123+
if response.status_code == 200:
124+
for collection in response.json().get("collections", []):
125+
collection_id = collection["id"].replace("_", ":", 1)
126+
client.delete(f"/catalog/collections/{collection_id}")
127+
yield client # Does NOT trigger setup_database!
128+
129+
116130
@dataclass
117131
class Collection:
118132
"""A collection for test purpose."""
@@ -490,6 +504,7 @@ def setup_database(
490504
feature_titi_S2_L1_0 (_type_): a feature from the collection S2_L1 with the
491505
user id titi.
492506
"""
507+
493508
add_collection(client, toto_s1_l1)
494509
add_collection(client, toto_s2_l3)
495510
add_collection(client, titi_s2_l1)

services/catalog/tests/test_endpoints.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,3 +1725,21 @@ def test_catalog_catalogs_owner_id_is_disabled(client):
17251725

17261726
response = client.get("/catalog/catalogs/toto")
17271727
assert response.status_code == fastapi.status.HTTP_400_BAD_REQUEST
1728+
1729+
1730+
def test_queryables_with_empty_catalog(client_with_empty_catalog):
1731+
"""
1732+
Test Queryables feature endpoint when catalog has no collections in it
1733+
"""
1734+
response_empty = client_with_empty_catalog.get("/catalog/queryables")
1735+
1736+
assert response_empty.status_code == fastapi.status.HTTP_200_OK
1737+
expected_response = {
1738+
"$id": f"{client_with_empty_catalog.base_url}/queryables",
1739+
"type": "object",
1740+
"title": "STAC Queryables.",
1741+
"$schema": "https://json-schema.org/draft-07/schema#",
1742+
"properties": {},
1743+
"additionalProperties": True,
1744+
}
1745+
assert response_empty.json() == expected_response # JSON Content Check

services/catalog/tests/test_user_handler.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ def test_get_user():
120120
assert get_user(None, None) == getpass.getuser()
121121

122122

123-
class TestRemovePrefix: # pylint: disable=missing-function-docstring
124-
"""This Class contains unit tests for the function remove_user_prefix."""
123+
class TestRerouteURL: # pylint: disable=missing-function-docstring
124+
"""This Class contains unit tests for the function reroute_url."""
125125

126126
def test_root_url(self, request_ids):
127127
request = Request(
@@ -220,6 +220,43 @@ def test_reroute_oauth2(self, request_ids):
220220
reroute_url(request, request_ids)
221221
assert request.scope["path"] == "/docs/oauth2-redirect"
222222

223+
@pytest.mark.parametrize(
224+
"path, expected",
225+
[
226+
("/catalog/", "/"),
227+
("/catalog", "/"),
228+
],
229+
)
230+
def test_reroute_catalog(self, request_ids, path, expected):
231+
"""Test that reroute_url modifies "/catalog" and "/catalog/" to the root URL ("/")."""
232+
request = Request(
233+
scope={
234+
"type": "http",
235+
"method": "GET",
236+
"path": path,
237+
"query_string": "",
238+
"user": "",
239+
"headers": {},
240+
},
241+
)
242+
reroute_url(request, request_ids)
243+
assert request.scope["path"] == expected
244+
245+
def test_reroute_catalog_api(self, request_ids):
246+
"""Test that reroute_url modifies "/catalog/api" to the "/api"."""
247+
request = Request(
248+
scope={
249+
"type": "http",
250+
"method": "GET",
251+
"path": "/catalog/api",
252+
"query_string": "",
253+
"user": "",
254+
"headers": {},
255+
},
256+
)
257+
reroute_url(request, request_ids)
258+
assert request.scope["path"] == "/api"
259+
223260
def test_reroute_queryables(self, request_ids):
224261
request = Request(
225262
scope={
@@ -234,6 +271,28 @@ def test_reroute_queryables(self, request_ids):
234271
reroute_url(request, request_ids)
235272
assert request.scope["path"] == "/queryables"
236273

274+
@pytest.mark.parametrize(
275+
"path, expected",
276+
[
277+
("/whatever-test/health", "/health"),
278+
("/health", "/health"),
279+
],
280+
)
281+
def test_reroute_health(self, request_ids, path, expected):
282+
"""Test that reroute_url catch health endpoints "/health"."""
283+
request = Request(
284+
scope={
285+
"type": "http",
286+
"method": "GET",
287+
"path": path,
288+
"query_string": "",
289+
"user": "",
290+
"headers": {},
291+
},
292+
)
293+
reroute_url(request, request_ids)
294+
assert request.scope["path"] == expected
295+
237296
def test_search_collection(self, request_ids):
238297
request = Request(
239298
scope={

0 commit comments

Comments
 (0)