@@ -42,10 +42,12 @@ class ValidationTest(integration_test_utils.IntegrationTestBase):
4242 def test_out_of_stock (self ) -> None :
4343 """Test validation for out-of-stock items.
4444
45+ Reference: https://ucp.dev/specification/checkout/#error-handling
46+
4547 Given a product with 0 inventory,
4648 When a checkout creation request is made for this item,
47- Then the server should return a 400 Bad Request error indicating
48- insufficient stock.
49+ Then the server should return a successful response (201) with status
50+ 'incomplete' and an error message indicating insufficient stock.
4951 """
5052 # Get out of stock item from config
5153 out_of_stock_item = self .conformance_config .get (
@@ -66,20 +68,29 @@ def test_out_of_stock(self) -> None:
6668 headers = integration_test_utils .get_headers (),
6769 )
6870
69- self .assert_response_status (response , 400 )
70- self .assertIn (
71- "Insufficient stock" ,
72- response .text ,
73- msg = "Expected 'Insufficient stock' message" ,
71+ # Verify HTTP status and then using centralized utility with exact path matching
72+ self .assert_response_status (response , [200 , 201 ])
73+ checkout_data = response .json ()
74+ li_id = checkout_data ["line_items" ][0 ]["id" ]
75+ self .assert_checkout_status (
76+ checkout_data ,
77+ expected_status = "incomplete" ,
78+ valid_path_matchers = [
79+ "$.line_items[0]" ,
80+ f"$.line_items[?(@.id=='{ li_id } ')]" ,
81+ ],
82+ model_class = checkout .Checkout ,
7483 )
7584
7685 def test_update_inventory_validation (self ) -> None :
7786 """Test that inventory validation is enforced on update.
7887
88+ Reference: https://ucp.dev/specification/checkout/#error-handling
89+
7990 Given an existing checkout session with a valid quantity,
8091 When the line item quantity is updated to exceed available stock,
81- Then the server should return a 400 Bad Request error indicating
82- insufficient stock.
92+ Then the server should return a successful response (200/201) with
93+ status 'incomplete' and an error message indicating insufficient stock.
8394 """
8495 response_json = self .create_checkout_session ()
8596 checkout_obj = checkout .Checkout (** response_json )
@@ -119,9 +130,18 @@ def test_update_inventory_validation(self) -> None:
119130 headers = integration_test_utils .get_headers (),
120131 )
121132
122- self .assert_response_status (response , 400 )
123- self .assertIn (
124- "stock" , response .text .lower (), msg = "Expected 'stock' message"
133+ # Verify HTTP status and then using centralized utility with exact path matching
134+ self .assert_response_status (response , [200 , 201 ])
135+ checkout_data = response .json ()
136+ li_id = checkout_data ["line_items" ][0 ]["id" ]
137+ self .assert_checkout_status (
138+ checkout_data ,
139+ expected_status = "incomplete" ,
140+ valid_path_matchers = [
141+ "$.line_items[0]" ,
142+ f"$.line_items[?(@.id=='{ li_id } ')]" ,
143+ ],
144+ model_class = checkout .Checkout ,
125145 )
126146
127147 def test_product_not_found (self ) -> None :
@@ -180,63 +200,63 @@ def test_payment_failure(self) -> None:
180200
181201 self .assert_response_status (response , 402 )
182202
183- def test_complete_without_fulfillment (self ) -> None :
184- """Test completion rejection when fulfillment is missing.
203+ def test_update_without_fulfillment (self ) -> None :
204+ """Test Soft-Fail when fulfillment info is missing during update .
185205
186- Given a newly created checkout session without fulfillment details,
187- When a completion request is submitted,
188- Then the server should return a 400 Bad Request error.
206+ Reference: https://ucp.dev/specification/checkout/#error-handling
207+
208+ Given an existing checkout session,
209+ When an update request is sent with an empty fulfillment method,
210+ Then the server should return a 200 OK status with 'incomplete' status
211+ and an error message indicating missing fulfillment info.
189212 """
190213 response_json = self .create_checkout_session (select_fulfillment = False )
191- checkout_id = response_json ["id" ]
192-
193- payment_payload = integration_test_utils .get_valid_payment_payload ()
214+ checkout_obj = checkout .Checkout (** response_json )
215+ checkout_id = checkout_obj .id
194216
195- response = self .client .post (
196- self .get_shopping_url (f"/checkout-sessions/{ checkout_id } /complete" ),
197- json = payment_payload ,
198- headers = integration_test_utils .get_headers (),
217+ # Update with empty fulfillment using the helper to ensure valid structure
218+ response_json = self .update_checkout_session (
219+ checkout_obj , fulfillment = {"methods" : [{"type" : "shipping" }]}
199220 )
200221
201- self .assert_response_status (response , 400 )
202- self .assertIn (
203- "Fulfillment address and option must be selected" ,
204- response .text ,
205- msg = "Expected error message for missing fulfillment" ,
222+ # Get the method ID from the response for precise path matching
223+ method_id = response_json ["fulfillment" ]["methods" ][0 ]["id" ]
224+
225+ self .assert_checkout_status (
226+ response_json ,
227+ expected_status = "incomplete" ,
228+ valid_path_matchers = [
229+ "$.fulfillment" ,
230+ "$.fulfillment.methods" ,
231+ f"$.fulfillment.methods[?(@.id=='{ method_id } ')].destinations" ,
232+ ],
233+ model_class = checkout .Checkout ,
206234 )
207235
208236 def test_structured_error_messages (self ) -> None :
209- """Test that error responses conform to the Message schema.
237+ """Test that error responses conform to the UCP ErrorResponse schema.
210238
211- Given a request that triggers an error (e.g., out of stock),
212- When the server responds with an error code (400),
213- Then the response body should contain a structured 'detail' field describing
214- the error.
215- """
216- # Get out of stock item from config
217- out_of_stock_item = self .conformance_config .get (
218- "out_of_stock_item" ,
219- {"id" : "out_of_stock_item_1" , "title" : "Out of Stock Item" },
220- )
239+ Reference: https://ucp.dev/specification/checkout-rest/#error-responses
221240
222- create_payload = self .create_checkout_payload (
223- item_id = out_of_stock_item ["id" ],
224- )
241+ Given a request for a non-existent checkout ID,
242+ When the server responds with a 404 Not Found error,
243+ Then the response body should contain a structured UCP response with
244+ status 'requires_escalation' and a descriptive error message.
245+ """
246+ non_existent_id = "non-existent-session-id"
225247
226- response = self .client .post (
227- self .get_shopping_url ("/checkout-sessions" ),
228- json = create_payload .model_dump (
229- mode = "json" , by_alias = True , exclude_none = True
230- ),
248+ response = self .client .get (
249+ self .get_shopping_url (f"/checkout-sessions/{ non_existent_id } " ),
231250 headers = integration_test_utils .get_headers (),
232251 )
233252
234- self .assert_response_status (response , 400 )
253+ # Verify HTTP status 404
254+ self .assert_response_status (response , 404 )
235255
236- # Check for structured error
237- data = response . json ()
238- self . assertTrue ( data . get ( "detail" ), "Error response missing 'detail' field" )
239- self . assertIn ( "Insufficient stock" , data [ "detail" ] )
256+ # Verify using centralized utility with 'requires_escalation' status
257+ self . assert_checkout_status (
258+ response . json ( ), expected_status = "requires_escalation"
259+ )
240260
241261
242262if __name__ == "__main__" :
0 commit comments