@@ -1472,5 +1472,197 @@ def test_brands_accepts_unique_and_none(self) -> None:
14721472 self .assertIsNone (Constraints ().brands )
14731473
14741474
1475+ class AdditionalPropertiesForbidFinderTest (unittest .TestCase ):
1476+ """additionalProperties:false objects map to generated class names."""
1477+
1478+ def test_root_titled_object (self ) -> None :
1479+ with tempfile .TemporaryDirectory () as tmp :
1480+ Path (tmp , "error_response.json" ).write_text (
1481+ json .dumps (
1482+ {
1483+ "title" : "Error Response" ,
1484+ "type" : "object" ,
1485+ "additionalProperties" : False ,
1486+ "properties" : {"messages" : {"type" : "array" }},
1487+ }
1488+ ),
1489+ encoding = "utf-8" ,
1490+ )
1491+ names = postprocess_models .find_extra_forbid_class_names (Path (tmp ))
1492+ self .assertEqual (names , {"ErrorResponse" })
1493+
1494+ def test_nested_untitled_object_uses_property_path (self ) -> None :
1495+ with tempfile .TemporaryDirectory () as tmp :
1496+ Path (tmp , "merchant_fulfillment_config.json" ).write_text (
1497+ json .dumps (
1498+ {
1499+ "title" : "Merchant Fulfillment Config" ,
1500+ "type" : "object" ,
1501+ "properties" : {
1502+ "allows_multi_destination" : {
1503+ "type" : "object" ,
1504+ "additionalProperties" : False ,
1505+ "properties" : {"shipping" : {"type" : "boolean" }},
1506+ }
1507+ },
1508+ }
1509+ ),
1510+ encoding = "utf-8" ,
1511+ )
1512+ names = postprocess_models .find_extra_forbid_class_names (Path (tmp ))
1513+ self .assertEqual (names , {"AllowsMultiDestination" })
1514+
1515+ def test_loose_and_map_objects_are_excluded (self ) -> None :
1516+ with tempfile .TemporaryDirectory () as tmp :
1517+ Path (tmp , "open.json" ).write_text (
1518+ json .dumps (
1519+ {
1520+ "title" : "Open Object" ,
1521+ "type" : "object" ,
1522+ "properties" : {"a" : {"type" : "string" }},
1523+ }
1524+ ),
1525+ encoding = "utf-8" ,
1526+ )
1527+ Path (tmp , "map.json" ).write_text (
1528+ json .dumps (
1529+ {
1530+ "title" : "Map Object" ,
1531+ "type" : "object" ,
1532+ "additionalProperties" : {"type" : "string" },
1533+ "properties" : {"a" : {"type" : "string" }},
1534+ }
1535+ ),
1536+ encoding = "utf-8" ,
1537+ )
1538+ names = postprocess_models .find_extra_forbid_class_names (Path (tmp ))
1539+ self .assertEqual (names , set ())
1540+
1541+
1542+ class AdditionalPropertiesForbidInjectorTest (unittest .TestCase ):
1543+ """The injector flips only the target class's model_config to forbid."""
1544+
1545+ SOURCE = '''\
1546+ class AllowsMultiDestination(BaseModel):
1547+ """
1548+ Permits multiple destinations per method type.
1549+ """
1550+
1551+ model_config = ConfigDict(
1552+ extra="allow",
1553+ )
1554+ shipping: bool | None = None
1555+
1556+
1557+ class MerchantFulfillmentConfig(BaseModel):
1558+ """
1559+ Merchant's fulfillment configuration.
1560+ """
1561+
1562+ model_config = ConfigDict(
1563+ extra="allow",
1564+ )
1565+ allows_multi_destination: AllowsMultiDestination | None = None
1566+ '''
1567+
1568+ def test_flips_only_target_class (self ) -> None :
1569+ updated = postprocess_models .inject_extra_forbid (
1570+ self .SOURCE , "AllowsMultiDestination"
1571+ )
1572+ # Target class body now forbids extra keys.
1573+ self .assertIn ('extra="forbid"' , updated )
1574+ # The sibling class in the same module keeps extra="allow".
1575+ sibling = """class MerchantFulfillmentConfig(BaseModel):
1576+ \" \" \"
1577+ Merchant's fulfillment configuration.
1578+ \" \" \"
1579+
1580+ model_config = ConfigDict(
1581+ extra="allow",
1582+ )"""
1583+ self .assertIn (sibling , updated )
1584+
1585+ def test_idempotent_after_flip (self ) -> None :
1586+ once = postprocess_models .inject_extra_forbid (
1587+ self .SOURCE , "AllowsMultiDestination"
1588+ )
1589+ twice = postprocess_models .inject_extra_forbid (
1590+ once , "AllowsMultiDestination"
1591+ )
1592+ self .assertEqual (once , twice )
1593+
1594+ def test_unknown_class_untouched (self ) -> None :
1595+ self .assertEqual (
1596+ postprocess_models .inject_extra_forbid (self .SOURCE , "Nope" ),
1597+ self .SOURCE ,
1598+ )
1599+
1600+
1601+ @unittest .skipUnless (
1602+ HAVE_SDK , "requires the installed package (pip install -e .)"
1603+ )
1604+ class AdditionalPropertiesForbidSemanticTest (unittest .TestCase ):
1605+ """Committed models reject unknown keys on additionalProperties:false."""
1606+
1607+ def test_error_response_rejects_unknown_keys (self ) -> None :
1608+ from ucp_sdk .models .schemas .shopping .types .error_response import (
1609+ ErrorResponse ,
1610+ )
1611+
1612+ with self .assertRaises (ValidationError ):
1613+ ErrorResponse .model_validate (
1614+ {
1615+ "ucp" : {"version" : "2026-04-08" , "status" : "error" },
1616+ "messages" : [
1617+ {
1618+ "type" : "error" ,
1619+ "code" : "not_found" ,
1620+ "severity" : "unrecoverable" ,
1621+ "content" : "boom" ,
1622+ }
1623+ ],
1624+ "bogus" : "x" ,
1625+ }
1626+ )
1627+
1628+ def test_error_response_accepts_declared_fields (self ) -> None :
1629+ from ucp_sdk .models .schemas .shopping .types .error_response import (
1630+ ErrorResponse ,
1631+ )
1632+
1633+ obj = ErrorResponse .model_validate (
1634+ {
1635+ "ucp" : {"version" : "2026-04-08" , "status" : "error" },
1636+ "messages" : [
1637+ {
1638+ "type" : "error" ,
1639+ "code" : "not_found" ,
1640+ "severity" : "unrecoverable" ,
1641+ "content" : "boom" ,
1642+ }
1643+ ],
1644+ }
1645+ )
1646+ self .assertEqual (obj .messages [0 ].content , "boom" )
1647+
1648+ def test_allows_multi_destination_rejects_unknown_keys (self ) -> None :
1649+ from ucp_sdk .models .schemas .shopping .types .merchant_fulfillment_config import (
1650+ AllowsMultiDestination ,
1651+ )
1652+
1653+ with self .assertRaises (ValidationError ):
1654+ AllowsMultiDestination .model_validate (
1655+ {"shipping" : True , "bogus" : "x" }
1656+ )
1657+
1658+ def test_sibling_config_keeps_extra_allow (self ) -> None :
1659+ from ucp_sdk .models .schemas .shopping .types .merchant_fulfillment_config import (
1660+ MerchantFulfillmentConfig ,
1661+ )
1662+
1663+ config = MerchantFulfillmentConfig .model_validate ({"bogus" : "x" })
1664+ self .assertEqual (config .model_extra , {"bogus" : "x" })
1665+
1666+
14751667if __name__ == "__main__" :
14761668 unittest .main ()
0 commit comments