diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ccf64763..689855d76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,8 +199,9 @@ jobs: matrix: include: # Core also builds and installs the reporting wheel/sdist fixtures. + # It runs 25-29 minutes on main, so 30 cancelled it on slow runners. - lane: core - timeout_minutes: 30 + timeout_minutes: 45 - lane: process timeout_minutes: 15 services: diff --git a/src/adcp/types/_forward_compat.py b/src/adcp/types/_forward_compat.py index a3e8a4f92..42851a573 100644 --- a/src/adcp/types/_forward_compat.py +++ b/src/adcp/types/_forward_compat.py @@ -58,7 +58,7 @@ from pydantic_core import CoreSchema, InitErrorDetails, PydanticCustomError, core_schema from pydantic_core.core_schema import ErrorType -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types.canonical_creative import CreateMediaBuyRequest as _PublicCreateMediaBuyRequest from adcp.types.canonical_creative import PackageRequest as PublicPackageRequest from adcp.types.canonical_creative import PackageUpdate as PublicPackageUpdate @@ -156,6 +156,9 @@ "identical wire shape, so confirm seller support before relying on it." ), ), + # Optional, not nullable: a JSON null is refused, as on the generated + # push_notification_config.operation_id this field mirrors (#1547). + NoExplicitNull, ] if TYPE_CHECKING: diff --git a/tests/test_reporting_webhook_operation_id.py b/tests/test_reporting_webhook_operation_id.py index 6727ce410..07747ae79 100644 --- a/tests/test_reporting_webhook_operation_id.py +++ b/tests/test_reporting_webhook_operation_id.py @@ -220,7 +220,12 @@ def test_reporting_operation_id_constraints_match_push_notification_config() -> assert reporting.annotation == push.annotation assert [repr(item) for item in reporting.metadata] == [repr(item) for item in push.metadata] schema = ReportingWebhook.model_json_schema()["properties"]["operation_id"] - assert schema["anyOf"][0] == { + push_schema = PushNotificationConfig.model_json_schema()["properties"]["operation_id"] + wire_keys = ("type", "minLength", "maxLength", "pattern", "anyOf") + assert {key: schema.get(key) for key in wire_keys} == { + key: push_schema.get(key) for key in wire_keys + } + assert {key: schema[key] for key in ("type", "minLength", "maxLength", "pattern")} == { "type": "string", "minLength": 1, "maxLength": 255, @@ -229,6 +234,18 @@ def test_reporting_operation_id_constraints_match_push_notification_config() -> assert "MUST NOT emit `media_buy_delivery`" in schema["description"] +def test_reporting_operation_id_refuses_an_explicit_json_null() -> None: + """Omitted is fine; a JSON null is not, matching the generated push field.""" + base = { + "url": "https://buyer.example/reporting", + "authentication": _AUTHENTICATION, + "reporting_frequency": "daily", + } + assert ReportingWebhook.model_validate_json(json.dumps(base)).operation_id is None + with pytest.raises(ValidationError): + ReportingWebhook.model_validate_json(json.dumps({**base, "operation_id": None})) + + @pytest.mark.asyncio async def test_stored_registration_correlates_delivery_and_retry(tmp_path: Path) -> None: registration = ReportingWebhook(