@@ -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,30 @@ 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
72+ # exact path matching
73+ self .assert_response_status (response , [200 , 201 ])
74+ checkout_data = response .json ()
75+ li_id = checkout_data ["line_items" ][0 ]["id" ]
76+ self .assert_checkout_status (
77+ checkout_data ,
78+ expected_status = "incomplete" ,
79+ valid_path_matchers = [
80+ "$.line_items[0]" ,
81+ f"$.line_items[?(@.id=='{ li_id } ')]" ,
82+ ],
83+ model_class = checkout .Checkout ,
7484 )
7585
7686 def test_update_inventory_validation (self ) -> None :
7787 """Test that inventory validation is enforced on update.
7888
89+ Reference: https://ucp.dev/specification/checkout/#error-handling
90+
7991 Given an existing checkout session with a valid quantity,
8092 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.
93+ Then the server should return a successful response (200/201) with
94+ status 'incomplete' and an error message indicating insufficient stock.
8395 """
8496 response_json = self .create_checkout_session ()
8597 checkout_obj = checkout .Checkout (** response_json )
@@ -119,9 +131,19 @@ def test_update_inventory_validation(self) -> None:
119131 headers = integration_test_utils .get_headers (),
120132 )
121133
122- self .assert_response_status (response , 400 )
123- self .assertIn (
124- "stock" , response .text .lower (), msg = "Expected 'stock' message"
134+ # Verify HTTP status and then using centralized utility with
135+ # exact path matching
136+ self .assert_response_status (response , [200 , 201 ])
137+ checkout_data = response .json ()
138+ li_id = checkout_data ["line_items" ][0 ]["id" ]
139+ self .assert_checkout_status (
140+ checkout_data ,
141+ expected_status = "incomplete" ,
142+ valid_path_matchers = [
143+ "$.line_items[0]" ,
144+ f"$.line_items[?(@.id=='{ li_id } ')]" ,
145+ ],
146+ model_class = checkout .Checkout ,
125147 )
126148
127149 def test_product_not_found (self ) -> None :
@@ -180,63 +202,62 @@ def test_payment_failure(self) -> None:
180202
181203 self .assert_response_status (response , 402 )
182204
183- def test_complete_without_fulfillment (self ) -> None :
184- """Test completion rejection when fulfillment is missing.
205+ def test_update_without_fulfillment (self ) -> None :
206+ """Test Soft-Fail when fulfillment info is missing during update.
207+
208+ Reference: https://ucp.dev/specification/checkout/#error-handling
185209
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.
210+ Given an existing checkout session,
211+ When an update request is sent with an empty fulfillment method,
212+ Then the server should return a 200 OK status with 'incomplete' status
213+ and an error message indicating missing fulfillment info.
189214 """
190215 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 ()
216+ checkout_obj = checkout .Checkout (** response_json )
194217
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 (),
218+ # Update with empty fulfillment using the helper to ensure valid structure
219+ response_json = self .update_checkout_session (
220+ checkout_obj , fulfillment = {"methods" : [{"type" : "shipping" }]}
199221 )
200222
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" ,
223+ # Get the method ID from the response for precise path matching
224+ method_id = response_json ["fulfillment" ]["methods" ][0 ]["id" ]
225+
226+ self .assert_checkout_status (
227+ response_json ,
228+ expected_status = "incomplete" ,
229+ valid_path_matchers = [
230+ "$.fulfillment" ,
231+ "$.fulfillment.methods" ,
232+ f"$.fulfillment.methods[?(@.id=='{ method_id } ')].destinations" ,
233+ ],
234+ model_class = checkout .Checkout ,
206235 )
207236
208237 def test_structured_error_messages (self ) -> None :
209- """Test that error responses conform to the Message schema.
238+ """Test that error responses conform to the UCP ErrorResponse schema.
210239
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- )
240+ Reference: https://ucp.dev/specification/checkout-rest/#error-responses
221241
222- create_payload = self .create_checkout_payload (
223- item_id = out_of_stock_item ["id" ],
224- )
242+ Given a request for a non-existent checkout ID,
243+ When the server responds with a 404 Not Found error,
244+ Then the response body should contain a structured UCP response with
245+ status 'requires_escalation' and a descriptive error message.
246+ """
247+ non_existent_id = "non-existent-session-id"
225248
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- ),
249+ response = self .client .get (
250+ self .get_shopping_url (f"/checkout-sessions/{ non_existent_id } " ),
231251 headers = integration_test_utils .get_headers (),
232252 )
233253
234- self .assert_response_status (response , 400 )
254+ # Verify HTTP status 404
255+ self .assert_response_status (response , 404 )
235256
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" ] )
257+ # Verify using centralized utility with 'requires_escalation' status
258+ self . assert_checkout_status (
259+ response . json ( ), expected_status = "requires_escalation"
260+ )
240261
241262
242263if __name__ == "__main__" :
0 commit comments