From 4c32e19d1219e9bd8dc2dec0c995e2705d094619 Mon Sep 17 00:00:00 2001 From: Vaithee Baskaran Date: Tue, 21 Apr 2026 18:04:19 -0700 Subject: [PATCH 1/2] fix: validate payment handler structure from profile instead of hardcoded list Replace hardcoded expected handler IDs (google_pay, mock_payment_handler, shop_pay) with structural validation that: - Discovers handlers from the business profile dynamically - Validates required fields (id, version) are present - Validates handler group names follow reverse-DNS convention - Works against any UCP merchant, not just the Flower Shop This makes the protocol conformance tests server-agnostic. Addresses #13 --- protocol_test.py | 61 +++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/protocol_test.py b/protocol_test.py index 2b4d589..4fd5e77 100644 --- a/protocol_test.py +++ b/protocol_test.py @@ -17,7 +17,7 @@ from absl.testing import absltest import integration_test_utils import httpx -from ucp_sdk.models.schemas.ucp import BusinessSchema +from ucp_sdk.models.schemas.ucp import BusinessSchema, ReverseDomainName from ucp_sdk.models.schemas.shopping import checkout as checkout from ucp_sdk.models.schemas.shopping.payment import ( Payment, @@ -180,32 +180,39 @@ def test_discovery(self): f"Missing expected capabilities in discovery: {missing_caps}", ) - # Verify Payment Handlers - handlers = { - h.get("id") - for handlers in data.get("payment_handlers", {}).values() - for h in (handlers if isinstance(handlers, list) else [handlers]) - } - expected_handlers = {"google_pay", "mock_payment_handler", "shop_pay"} - missing_handlers = expected_handlers - handlers - self.assertFalse( - missing_handlers, - f"Missing expected payment handlers: {missing_handlers}", - ) - - # Specific check for Shop Pay config - shop_pay = next( - ( - h - for handlers in data.get("payment_handlers", {}).values() - for h in (handlers if isinstance(handlers, list) else [handlers]) - if h.get("id") == "shop_pay" - ), - None, - ) - self.assertIsNotNone(shop_pay, "Shop Pay handler not found") - self.assertEqual(shop_pay.get("name"), "com.shopify.shop_pay") - self.assertIn("shop_id", shop_pay.get("config")) + # Verify Payment Handlers - structural validation (server-agnostic) + if data.get("payment_handlers"): + handler_count = 0 + for handler_name, handler_list in data.get( + "payment_handlers", {} + ).items(): + # Validate handler group name using the SDK's ReverseDomainName + # model, which enforces the pattern defined in the UCP spec. + try: + ReverseDomainName(root=str(handler_name)) + except Exception as e: + self.fail( + f"Payment handler group name '{handler_name}' " + f"does not follow reverse-DNS convention: {e}" + ) + for h in ( + handler_list if isinstance(handler_list, list) else [handler_list] + ): + handler_count += 1 + # Validate required fields are present and non-empty + self.assertTrue( + h.get("id"), + "Payment handler missing 'id'", + ) + self.assertTrue( + h.get("version"), + f"Payment handler '{h.get('id')}' missing 'version'", + ) + self.assertGreater( + handler_count, + 0, + "payment_handlers is present but contains no handlers", + ) # Verify shopping capability shopping_services = data.get("services", {}).get("dev.ucp.shopping") From ac2ab0b38ad541a3364d4355e2a6c2698fa2621f Mon Sep 17 00:00:00 2001 From: Vaithee Baskaran Date: Mon, 27 Apr 2026 10:04:36 -0700 Subject: [PATCH 2/2] Catch ValidationError specifically when validating handler group names --- protocol_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocol_test.py b/protocol_test.py index 4fd5e77..4ac7d47 100644 --- a/protocol_test.py +++ b/protocol_test.py @@ -17,6 +17,7 @@ from absl.testing import absltest import integration_test_utils import httpx +from pydantic import ValidationError from ucp_sdk.models.schemas.ucp import BusinessSchema, ReverseDomainName from ucp_sdk.models.schemas.shopping import checkout as checkout from ucp_sdk.models.schemas.shopping.payment import ( @@ -190,7 +191,7 @@ def test_discovery(self): # model, which enforces the pattern defined in the UCP spec. try: ReverseDomainName(root=str(handler_name)) - except Exception as e: + except ValidationError as e: self.fail( f"Payment handler group name '{handler_name}' " f"does not follow reverse-DNS convention: {e}"