Skip to content

Commit dceadca

Browse files
authored
basic artifact identifier validation (eiffel-community#119)
1 parent 6b575b8 commit dceadca

File tree

3 files changed

+129
-6
lines changed

3 files changed

+129
-6
lines changed

python/src/etos_api/routers/v0/schemas.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class StartEtosRequest(EtosRequest):
4949

5050
@field_validator("artifact_id")
5151
def validate_id_or_identity(cls, artifact_id, info):
52-
"""Validate that at least one and only one of id and identity are set.
52+
"""Validate that id/identity is set correctly.
5353
5454
:param artifact_id: The value of 'artifact_id' to validate.
5555
:value artifact_id: str or None
@@ -59,10 +59,23 @@ def validate_id_or_identity(cls, artifact_id, info):
5959
:rtype: str or None
6060
"""
6161
values = info.data
62-
if values.get("artifact_identity") is None and not artifact_id:
62+
artifact_identity = values.get("artifact_identity")
63+
64+
# Check that at least one is provided
65+
if artifact_identity is None and not artifact_id:
6366
raise ValueError("At least one of 'artifact_identity' or 'artifact_id' is required.")
64-
if values.get("artifact_identity") is not None and artifact_id:
67+
68+
# Check that only one is provided
69+
if artifact_identity is not None and artifact_id:
6570
raise ValueError("Only one of 'artifact_identity' or 'artifact_id' is required.")
71+
72+
# Validate artifact_identity format if provided
73+
if artifact_identity is not None:
74+
if not isinstance(artifact_identity, str) or not artifact_identity.startswith("pkg:"):
75+
raise ValueError("artifact_identity must be a string starting with 'pkg:'")
76+
77+
# Note: artifact_id UUID validation is handled by Pydantic's built-in UUID type validation
78+
6679
return artifact_id
6780

6881

python/src/etos_api/routers/v1alpha/schemas.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class StartTestrunRequest(TestrunRequest):
4848

4949
@field_validator("artifact_id")
5050
def validate_id_or_identity(cls, artifact_id, info):
51-
"""Validate that at least one and only one of id and identity are set.
51+
"""Validate that id/identity is set correctly.
5252
5353
:param artifact_id: The value of 'artifact_id' to validate.
5454
:value artifact_id: str or None
@@ -58,10 +58,23 @@ def validate_id_or_identity(cls, artifact_id, info):
5858
:rtype: str or None
5959
"""
6060
values = info.data
61-
if values.get("artifact_identity") is None and not artifact_id:
61+
artifact_identity = values.get("artifact_identity")
62+
63+
# Check that at least one is provided
64+
if artifact_identity is None and not artifact_id:
6265
raise ValueError("At least one of 'artifact_identity' or 'artifact_id' is required.")
63-
if values.get("artifact_identity") is not None and artifact_id:
66+
67+
# Check that only one is provided
68+
if artifact_identity is not None and artifact_id:
6469
raise ValueError("Only one of 'artifact_identity' or 'artifact_id' is required.")
70+
71+
# Validate artifact_identity format if provided
72+
if artifact_identity is not None:
73+
if not isinstance(artifact_identity, str) or not artifact_identity.startswith("pkg:"):
74+
raise ValueError("artifact_identity must be a string starting with 'pkg:'")
75+
76+
# Note: artifact_id UUID validation is handled by Pydantic's built-in UUID type validation
77+
6578
return artifact_id
6679

6780

python/tests/test_routers.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,103 @@ def test_start_etos_empty_suite(self, download_suite_mock, digest_mock):
232232
break
233233
assert tercc is None
234234

235+
def test_start_etos_missing_artifact_identity_and_id(self):
236+
"""Test that POST requests to /etos with missing artifact identity and ID fail validation.
237+
238+
Approval criteria:
239+
- POST requests to ETOS with missing artifact_identity and artifact_id shall return 422.
240+
- The error message shall indicate that at least one is required.
241+
242+
Test steps::
243+
1. Send a POST request to etos without artifact_identity or artifact_id.
244+
2. Verify that the status code is 422.
245+
3. Verify that the error message indicates missing required field.
246+
"""
247+
self.logger.info(
248+
"STEP: Send a POST request to etos without artifact_identity or artifact_id."
249+
)
250+
response = self.client.post(
251+
"/api/etos",
252+
json={
253+
"test_suite_url": "http://localhost/my_test.json",
254+
"artifact_id": None, # Explicitly set to None to trigger validation
255+
},
256+
)
257+
self.logger.info("STEP: Verify that the status code is 422.")
258+
assert response.status_code == 422
259+
260+
self.logger.info("STEP: Verify that the error message indicates missing required field.")
261+
error_detail = response.json()
262+
assert "detail" in error_detail
263+
error_messages = [error["msg"] for error in error_detail["detail"]]
264+
expected_message = "At least one of 'artifact_identity' or 'artifact_id' is required."
265+
assert any(expected_message in msg for msg in error_messages)
266+
267+
def test_start_etos_empty_artifact_identity_and_none_artifact_id(self):
268+
"""Test that POST requests to /etos with empty artifact_identity fail validation.
269+
270+
Approval criteria:
271+
- POST requests to ETOS with empty artifact_identity shall return 422.
272+
- The error message shall indicate invalid format (empty doesn't start with 'pkg:').
273+
274+
Test steps::
275+
1. Send a POST request to etos with empty artifact_identity and None artifact_id.
276+
2. Verify that the status code is 422.
277+
3. Verify that the error message indicates invalid format.
278+
"""
279+
self.logger.info(
280+
"STEP: Send a POST request to etos with empty artifact_identity and None artifact_id."
281+
)
282+
response = self.client.post(
283+
"/api/etos",
284+
json={
285+
"artifact_identity": "",
286+
"artifact_id": None,
287+
"test_suite_url": "http://localhost/my_test.json",
288+
},
289+
)
290+
self.logger.info("STEP: Verify that the status code is 422.")
291+
assert response.status_code == 422
292+
293+
self.logger.info("STEP: Verify that the error message indicates invalid format.")
294+
error_detail = response.json()
295+
assert "detail" in error_detail
296+
error_messages = [error["msg"] for error in error_detail["detail"]]
297+
expected_message = "artifact_identity must be a string starting with 'pkg:'"
298+
assert any(expected_message in msg for msg in error_messages)
299+
300+
def test_start_etos_both_artifact_identity_and_id_provided(self):
301+
"""
302+
Approval criteria:
303+
- POST requests to ETOS with both artifact_identity and artifact_id returns 422.
304+
- The error message shall indicate that only one is required.
305+
306+
Test steps::
307+
1. Send a POST request to etos with both artifact_identity and artifact_id.
308+
2. Verify that the status code is 422.
309+
3. Verify that the error message indicates only one is required.
310+
"""
311+
self.logger.info(
312+
"STEP: Send a POST request to etos with both artifact_identity and artifact_id."
313+
)
314+
response = self.client.post(
315+
"/api/etos",
316+
json={
317+
"artifact_identity": "pkg:testing/etos",
318+
"artifact_id": "123e4567-e89b-12d3-a456-426614174000",
319+
"test_suite_url": "http://localhost/my_test.json",
320+
},
321+
)
322+
self.logger.info("STEP: Verify that the status code is 422.")
323+
assert response.status_code == 422
324+
325+
self.logger.info("STEP: Verify that the error message indicates only one is required.")
326+
error_detail = response.json()
327+
assert "detail" in error_detail
328+
error_messages = [error["msg"] for error in error_detail["detail"]]
329+
expected_message = "Only one of 'artifact_identity' or 'artifact_id' is required."
330+
assert any(expected_message in msg for msg in error_messages)
331+
235332
def test_selftest_get_ping(self):
236333
"""Test that selftest ping with HTTP GET pings the system.
237334

0 commit comments

Comments
 (0)