From 8f89ac5dd8319d71306fa528c0294d9621bdfd5f Mon Sep 17 00:00:00 2001 From: damaz91 Date: Fri, 24 Jul 2026 14:20:06 +0000 Subject: [PATCH 1/4] test(conformance): accept compliant 4xx error responses in validation test Update test_structured_error_messages to accept UcpErrorResponse shape (with 'messages') in addition to legacy/default FastAPI 'detail' shape. TAG=agy CONV=66cef4ea-c19e-4693-8c23-b155a5e0cddc --- validation_test.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/validation_test.py b/validation_test.py index 9a292f8..8864655 100644 --- a/validation_test.py +++ b/validation_test.py @@ -373,10 +373,22 @@ def test_structured_error_messages(self) -> None: if 400 <= response.status_code < 500: # 4xx posture: the body must be structured, not free text. data = response.json() - self.assertTrue( - data.get("detail"), "Error response missing 'detail' field" - ) - self.assertIn("stock", str(data["detail"]).lower()) + if "messages" in data: + # Compliant UCP error shape + errors = [ + m for m in data.get("messages", []) if m.get("type") == "error" + ] + self.assertTrue( + errors, "Compliant 4xx response must have at least one error message" + ) + self.assertIn("stock", errors[0].get("content", "").lower()) + else: + # Legacy/Default FastAPI error shape + self.assertTrue( + data.get("detail"), + "Error response missing 'detail' or 'messages' field", + ) + self.assertIn("stock", str(data["detail"]).lower()) return # In-band posture: the message envelope IS the structured error; the From cda30d3397af2545fa8d53aff8a45d793aadd78a Mon Sep 17 00:00:00 2001 From: damaz91 Date: Mon, 27 Jul 2026 09:20:29 +0000 Subject: [PATCH 2/4] test(conformance): use any() to check for stock error in messages Applying code review recommendation to check all errors instead of just the first one. TAG=agy CONV=46bf6a0b-350d-4fc9-8de0-233576567b08 --- validation_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/validation_test.py b/validation_test.py index 8864655..3742de5 100644 --- a/validation_test.py +++ b/validation_test.py @@ -381,7 +381,10 @@ def test_structured_error_messages(self) -> None: self.assertTrue( errors, "Compliant 4xx response must have at least one error message" ) - self.assertIn("stock", errors[0].get("content", "").lower()) + self.assertTrue( + any("stock" in e.get("content", "").lower() for e in errors), + "Expected stock-related error message", + ) else: # Legacy/Default FastAPI error shape self.assertTrue( From 1082a7689302ee8ca885c6ce86bceac580a6d3ce Mon Sep 17 00:00:00 2001 From: damaz91 Date: Mon, 27 Jul 2026 09:32:08 +0000 Subject: [PATCH 3/4] test(conformance): use ErrorResponse model for validation Cast the response data to `ErrorResponse` model in `validation_test.py` to ensure it conforms to the UCP schema, rather than just checking raw JSON. Catch `ValidationError` and fail the test if it occurs. TAG=agy CONV=46bf6a0b-350d-4fc9-8de0-233576567b08 --- validation_test.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/validation_test.py b/validation_test.py index 3742de5..593287c 100644 --- a/validation_test.py +++ b/validation_test.py @@ -26,6 +26,8 @@ ) from ucp_sdk.models.schemas.shopping.types import item_update_request from ucp_sdk.models.schemas.shopping.types import line_item_update_request +from ucp_sdk.models.schemas.shopping.types.error_response import ErrorResponse +from pydantic import ValidationError # Rebuild models to resolve forward references @@ -375,16 +377,19 @@ def test_structured_error_messages(self) -> None: data = response.json() if "messages" in data: # Compliant UCP error shape - errors = [ - m for m in data.get("messages", []) if m.get("type") == "error" - ] - self.assertTrue( - errors, "Compliant 4xx response must have at least one error message" - ) - self.assertTrue( - any("stock" in e.get("content", "").lower() for e in errors), - "Expected stock-related error message", - ) + try: + error_resp = ErrorResponse(**data) + errors = [m for m in error_resp.messages if m.type == "error"] + self.assertTrue( + errors, + "Compliant 4xx response must have at least one error message", + ) + self.assertTrue( + any("stock" in e.content.lower() for e in errors), + "Expected stock-related error message", + ) + except ValidationError as e: + self.fail(f"Failed to parse ErrorResponse: {e}") else: # Legacy/Default FastAPI error shape self.assertTrue( From aeafd43832f6cb3de506b187785b475f73e7540b Mon Sep 17 00:00:00 2001 From: damaz91 Date: Mon, 27 Jul 2026 09:37:55 +0000 Subject: [PATCH 4/4] test(conformance): refactor 4xx validation to use ErrorResponse helper Refactors `validation_test.py` to use a shared helper `_assert_structured_4xx_error` which casts 4xx responses to the SDK's `ErrorResponse` model. Updated `assert_business_error` and simplified `test_structured_error_messages` to use it. Added `assert_4xx_error` helper and used it in `test_complete_without_fulfillment` and `test_payment_failure` to validate the error response structure of direct 4xx rejections. TAG=agy CONV=46bf6a0b-350d-4fc9-8de0-233576567b08 --- validation_test.py | 94 ++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/validation_test.py b/validation_test.py index 593287c..baac374 100644 --- a/validation_test.py +++ b/validation_test.py @@ -81,11 +81,7 @@ def assert_business_error( ``order``). """ if 400 <= response.status_code < 500: - self.assertIn( - error_4xx_substring.lower(), - response.text.lower(), - msg=f"Expected '{error_4xx_substring}' in the 4xx error body", - ) + self._assert_structured_4xx_error(response, error_4xx_substring) return self.assert_response_status(response, [200, 201]) @@ -153,6 +149,50 @@ def assert_business_error( "not carry an order", ) + def _assert_structured_4xx_error(self, response, substring: str) -> None: + """Assert that a 4xx response is structured correctly (UCP or legacy).""" + try: + data = response.json() + if "messages" in data: + try: + error_resp = ErrorResponse(**data) + errors = [m for m in error_resp.messages if m.type == "error"] + self.assertTrue( + errors, + "Compliant 4xx response must have at least one error message", + ) + self.assertTrue( + any(substring.lower() in e.content.lower() for e in errors), + f"Expected '{substring}' in error messages", + ) + except ValidationError as e: + self.fail(f"Failed to parse ErrorResponse: {e}") + else: + self.assertTrue( + data.get("detail"), + "Error response missing 'detail' or 'messages' field", + ) + self.assertIn( + substring.lower(), + str(data["detail"]).lower(), + ) + except ValueError: + self.assertIn( + substring.lower(), + response.text.lower(), + msg=f"Expected '{substring}' in the 4xx error body", + ) + + def assert_4xx_error( + self, + response, + expected_status: int, + substring: str, + ) -> None: + """Assert a 4xx rejection with a specific status and error message.""" + self.assert_response_status(response, expected_status) + self._assert_structured_4xx_error(response, substring) + def test_out_of_stock(self) -> None: """Test validation for out-of-stock items. @@ -318,7 +358,11 @@ def test_payment_failure(self) -> None: headers=integration_test_utils.get_headers(), ) - self.assert_response_status(response, 402) + self.assert_4xx_error( + response, + expected_status=402, + substring="Payment Failed", + ) def test_complete_without_fulfillment(self) -> None: """Test completion rejection when fulfillment is missing. @@ -338,11 +382,10 @@ def test_complete_without_fulfillment(self) -> None: headers=integration_test_utils.get_headers(), ) - self.assert_response_status(response, 400) - self.assertIn( - "Fulfillment address and option must be selected", - response.text, - msg="Expected error message for missing fulfillment", + self.assert_4xx_error( + response, + expected_status=400, + substring="Fulfillment address and option must be selected", ) def test_structured_error_messages(self) -> None: @@ -372,35 +415,6 @@ def test_structured_error_messages(self) -> None: headers=integration_test_utils.get_headers(), ) - if 400 <= response.status_code < 500: - # 4xx posture: the body must be structured, not free text. - data = response.json() - if "messages" in data: - # Compliant UCP error shape - try: - error_resp = ErrorResponse(**data) - errors = [m for m in error_resp.messages if m.type == "error"] - self.assertTrue( - errors, - "Compliant 4xx response must have at least one error message", - ) - self.assertTrue( - any("stock" in e.content.lower() for e in errors), - "Expected stock-related error message", - ) - except ValidationError as e: - self.fail(f"Failed to parse ErrorResponse: {e}") - else: - # Legacy/Default FastAPI error shape - self.assertTrue( - data.get("detail"), - "Error response missing 'detail' or 'messages' field", - ) - self.assertIn("stock", str(data["detail"]).lower()) - return - - # In-band posture: the message envelope IS the structured error; the - # shared assertion validates every required envelope field. self.assert_business_error( response, accepted_codes={"out_of_stock", "item_unavailable"},