Skip to content

Commit ae3ab0b

Browse files
authored
Use a distinct error for requests with invalid identifiers (#612)
* Use a distinct error for requests with invalid identifiers This is more informative than 'not found', and as a bonus avoids doing database roundtrips. * Improve the error message if the id might be of another type
1 parent b4e67f5 commit ae3ab0b

File tree

4 files changed

+59
-19
lines changed

4 files changed

+59
-19
lines changed

src/routers/resource_router.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from dependencies.pagination import Pagination, PaginationParams
3131
from error_handling import as_http_exception
3232
from database.model.ai_asset.distribution import Distribution
33+
from routers.helper_functions import get_asset_type_by_abbreviation
3334
from versioning import Version, VersionedResource
3435

3536
from http import HTTPStatus
@@ -412,6 +413,7 @@ def get_resource(
412413
schema: self._possible_schemas_type = "aiod", # type: ignore
413414
user: KeycloakUser | None = Depends(get_user_or_none),
414415
):
416+
self._raise_if_identifier_is_wrong_type(identifier)
415417
resource = self.get_resource(
416418
identifier=identifier, schema=schema, user=user, platform=None
417419
)
@@ -420,6 +422,20 @@ def get_resource(
420422

421423
return get_resource
422424

425+
def _raise_if_identifier_is_wrong_type(self, identifier: str):
426+
if not identifier.startswith(self.resource_class.__abbreviation__):
427+
hint = ""
428+
if other_type := get_asset_type_by_abbreviation().get(identifier.split("_")[0]):
429+
hint = f" Did you mean to request a {other_type.__tablename__!r} instead?"
430+
raise HTTPException(
431+
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
432+
detail=(
433+
f"{identifier!r} is not a valid {self.resource_name} identifier, "
434+
f"valid {self.resource_name} identifiers start with "
435+
f"{self.resource_class.__abbreviation__!r}." + hint
436+
),
437+
)
438+
423439
def get_platform_resource_func(self):
424440
"""
425441
Return a function that can be used to retrieve a single resource of a platform.
@@ -541,6 +557,7 @@ def put_resource(
541557
resource_create_instance: clz_create, # type: ignore
542558
user: KeycloakUser = Depends(get_user_or_raise),
543559
):
560+
self._raise_if_identifier_is_wrong_type(identifier)
544561
with DbSession() as session:
545562
try:
546563
resource: Any = self._retrieve_resource(session, identifier)
@@ -592,6 +609,7 @@ def delete_resource(
592609
identifier: str,
593610
user: KeycloakUser = Depends(get_user_or_raise),
594611
):
612+
self._raise_if_identifier_is_wrong_type(identifier)
595613
with DbSession() as session:
596614
try:
597615
# Raise error if it does not exist
@@ -627,6 +645,7 @@ def submit_resource(
627645
submission: SubmissionCreateV2 | None = None,
628646
user: KeycloakUser = Depends(get_user_or_raise),
629647
):
648+
self._raise_if_identifier_is_wrong_type(identifier)
630649
with DbSession() as session:
631650
resource = self._retrieve_resource(identifier=identifier, session=session) # type: ignore
632651

src/tests/routers/generic/test_router_delete.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,27 +64,26 @@ def test_delete_requires_admin(
6464
assert try_delete().status_code == HTTPStatus.OK
6565

6666

67-
@pytest.mark.parametrize("identifier", [3, 4])
6867
def test_non_existent(
6968
client_test_resource: TestClient,
70-
identifier: int,
7169
mocked_privileged_token: Mock,
7270
):
73-
with DbSession() as session:
74-
session.add_all(
75-
[
76-
factory_test_resource(title="my_test_resource", status=EntryStatus.DRAFT,
77-
platform="example", platform_resource_identifier=1),
78-
factory_test_resource(title="second_test_resource", status=EntryStatus.DRAFT,
79-
platform="example", platform_resource_identifier=2),
80-
]
81-
)
82-
session.commit()
8371
response = client_test_resource.delete(
84-
f"/test_resources/{identifier}", headers={"Authorization": "Fake token"}
72+
f"/test_resources/test_44", headers={"Authorization": "Fake token"}
73+
)
74+
assert response.status_code == HTTPStatus.NOT_FOUND, response.json()
75+
assert response.json()["detail"] == f"Test_resource 'test_44' not found in the database."
76+
77+
78+
def test_not_valid(
79+
client_test_resource: TestClient,
80+
mocked_privileged_token: Mock,
81+
):
82+
response = client_test_resource.delete(
83+
f"/test_resources/data_44", headers={"Authorization": "Fake token"}
8584
)
86-
assert response.status_code == 404, response.json()
87-
assert response.json()["detail"] == f"Test_resource '{identifier}' not found in the database."
85+
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, response.json()
86+
assert "is not a valid" in response.json()["detail"]
8887

8988

9089
def test_add_after_deletion(

src/tests/routers/generic/test_router_get.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@ def test_get_happy_path(client_test_resource: TestClient, engine_test_resource_f
1919

2020

2121
def test_not_found(client_test_resource: TestClient, engine_test_resource_filled: str):
22-
response = client_test_resource.get("/test_resources/99")
22+
response = client_test_resource.get("/test_resources/test_99")
2323
assert response.status_code == HTTPStatus.NOT_FOUND, response.json()
24-
assert response.json()["detail"] == "Test_resource '99' not found in the database."
24+
assert response.json()["detail"] == "Test_resource 'test_99' not found in the database."
25+
26+
27+
def test_not_valid(client_test_resource: TestClient, engine_test_resource_filled: str):
28+
response = client_test_resource.get("/test_resources/99")
29+
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, response.json()
30+
assert "is not a valid" in response.json()["detail"]
2531

2632

2733
def test_get_draft_unauthenticated_not_allowed(client_test_resource: TestClient):

src/tests/routers/generic/test_router_put.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from http import HTTPStatus
12
from unittest.mock import Mock
23

34
import pytest
@@ -37,13 +38,28 @@ def test_non_existent(
3738
mocked_privileged_token: Mock,
3839
):
3940
response = client_test_resource.put(
40-
"/test_resources/2",
41+
"/test_resources/test_2",
4142
json={"title": "new_title", "platform": "other", "platform_resource_identifier": "2"},
4243
headers={"Authorization": "Fake token"},
4344
)
4445
assert response.status_code == 404, response.json()
4546
response_json = response.json()
46-
assert response_json["detail"] == "Test_resource '2' not found in the database."
47+
assert response_json["detail"] == "Test_resource 'test_2' not found in the database."
48+
49+
50+
def test_wrong_identifier_type(
51+
client_test_resource: TestClient,
52+
engine_test_resource_filled: Engine,
53+
mocked_privileged_token: Mock,
54+
):
55+
response = client_test_resource.put(
56+
"/test_resources/data_2",
57+
json={"title": "new_title", "platform": "other", "platform_resource_identifier": "2"},
58+
headers={"Authorization": "Fake token"},
59+
)
60+
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, response.json()
61+
response_json = response.json()
62+
assert "is not a valid" in response_json["detail"]
4763

4864

4965
def test_too_long_name(

0 commit comments

Comments
 (0)