Loading src/srm/api/databus/schemas.py +14 −10 Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ from datetime import datetime from typing import Literal from uuid import UUID from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator from srm.domain.models.canonical_parameters.parameters import ( CapabilityParameters, Loading @@ -10,13 +10,17 @@ from srm.domain.models.canonical_parameters.parameters import ( ) class CommandSchema(BaseModel): model_config = ConfigDict(extra="forbid") class InboundMessage(BaseModel): subject: str payload: bytes headers: dict[str, str] class CommandEnvelopeV1(BaseModel): class CommandEnvelopeV1(CommandSchema): schema_version: Literal["1.0"] operation_id: UUID correlation_id: str Loading @@ -33,16 +37,16 @@ class CommandEnvelopeV1(BaseModel): return self class PlacementConstraintsV1(BaseModel): model_config = {"extra": "allow"} class PlacementConstraintsV1(CommandSchema): model_config = ConfigDict(extra="allow") class DeployPayloadV1(BaseModel): class DeployPayloadV1(CommandSchema): instance_name: str | None = None placement_constraints: PlacementConstraintsV1 | None = None class DeployTargetV1(BaseModel): class DeployTargetV1(CommandSchema): app_instance_id: UUID zone_id: UUID | None = None domain_id: UUID | None = None Loading @@ -61,7 +65,7 @@ class SrmServiceDeployV1(CommandEnvelopeV1): deploy: DeployPayloadV1 class ScalePayloadV1(BaseModel): class ScalePayloadV1(CommandSchema): replicas: int = Field(ge=0) Loading @@ -71,7 +75,7 @@ class SrmServiceScaleV1(CommandEnvelopeV1): scale: ScalePayloadV1 class TerminatePayloadV1(BaseModel): class TerminatePayloadV1(CommandSchema): grace_period_seconds: int = Field(default=0, ge=0) Loading @@ -81,7 +85,7 @@ class SrmServiceTerminateV1(CommandEnvelopeV1): terminate: TerminatePayloadV1 class NetworkCapabilityPayloadV1(BaseModel): class NetworkCapabilityPayloadV1(CommandSchema): capability_type: str target: CapabilityTarget profile_ref: str | None = None Loading @@ -102,7 +106,7 @@ class SrmNetworkCapabilityActivateV1(CommandEnvelopeV1): return self class NetworkCapabilityRealizationRefV1(BaseModel): class NetworkCapabilityRealizationRefV1(CommandSchema): capability_type: str external_ref: str | None = None service_instance_id: UUID | None = None Loading tests/api/databus/test_schemas.py +48 −6 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ from srm.api.databus.schemas import ( NetworkCapabilityDeactivatePayloadV1, NetworkCapabilityPayloadV1, NetworkCapabilityUpdatePayloadV1, PlacementConstraintsV1, ScalePayloadV1, SrmNetworkCapabilityActivateV1, SrmNetworkCapabilityDeactivateV1, Loading Loading @@ -113,6 +114,48 @@ class TestFederationContext: SrmServiceDeployV1.model_validate(payload) class TestUnknownFields: def test_envelope_rejects_unknown_fields(self) -> None: with pytest.raises(ValidationError, match="Extra inputs are not permitted"): CommandEnvelopeV1.model_validate(_envelope(unexpected="value")) def test_deploy_target_rejects_unknown_pin_typo(self) -> None: with pytest.raises(ValidationError, match="zone_ID"): DeployTargetV1.model_validate( { "app_instance_id": str(uuid4()), "zone_ID": str(uuid4()), } ) def test_deploy_v1_rejects_unknown_nested_target_field(self) -> None: payload = _envelope( service_specification_id=str(uuid4()), targets=[{"app_instance_id": str(uuid4()), "zone_ID": str(uuid4())}], deploy={}, ) with pytest.raises(ValidationError, match="zone_ID"): SrmServiceDeployV1.model_validate(payload) def test_deploy_payload_rejects_unknown_fields(self) -> None: with pytest.raises(ValidationError, match="metadata"): DeployPayloadV1.model_validate({"metadata": {"owner": "test"}}) def test_placement_constraints_allow_open_hints(self) -> None: constraints = PlacementConstraintsV1.model_validate( { "preferred_zone_ref": "athens-edge", "latency_budget_ms": 20, } ) assert constraints.model_extra == { "preferred_zone_ref": "athens-edge", "latency_budget_ms": 20, } def test_deploy_v1_parses_minimal_valid_payload() -> None: payload = _envelope( service_specification_id=str(uuid4()), Loading Loading @@ -190,9 +233,9 @@ class TestPinShape: with pytest.raises(ValidationError, match="domain_id requires zone_id"): SrmNetworkCapabilityActivateV1.model_validate(payload) def test_update_ignores_pins_and_does_not_apply_the_rule(self) -> None: """§B.6: zone_id/domain_id are ignored when identifying an existing realization, so a bare domain_id must not be rejected on update.""" def test_update_rejects_activate_only_pin_fields(self) -> None: """§B.6 update identifies an existing realization; zone/domain pins belong only to activate and are rejected as unknown command fields.""" payload = _envelope( domain_id=str(uuid4()), network_capability={ Loading @@ -202,9 +245,8 @@ class TestPinShape: }, ) command = SrmNetworkCapabilityUpdateV1.model_validate(payload) assert command.network_capability.external_ref == "sess-123" with pytest.raises(ValidationError, match="domain_id"): SrmNetworkCapabilityUpdateV1.model_validate(payload) def test_scale_payload_requires_replicas() -> None: Loading Loading
src/srm/api/databus/schemas.py +14 −10 Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ from datetime import datetime from typing import Literal from uuid import UUID from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator from srm.domain.models.canonical_parameters.parameters import ( CapabilityParameters, Loading @@ -10,13 +10,17 @@ from srm.domain.models.canonical_parameters.parameters import ( ) class CommandSchema(BaseModel): model_config = ConfigDict(extra="forbid") class InboundMessage(BaseModel): subject: str payload: bytes headers: dict[str, str] class CommandEnvelopeV1(BaseModel): class CommandEnvelopeV1(CommandSchema): schema_version: Literal["1.0"] operation_id: UUID correlation_id: str Loading @@ -33,16 +37,16 @@ class CommandEnvelopeV1(BaseModel): return self class PlacementConstraintsV1(BaseModel): model_config = {"extra": "allow"} class PlacementConstraintsV1(CommandSchema): model_config = ConfigDict(extra="allow") class DeployPayloadV1(BaseModel): class DeployPayloadV1(CommandSchema): instance_name: str | None = None placement_constraints: PlacementConstraintsV1 | None = None class DeployTargetV1(BaseModel): class DeployTargetV1(CommandSchema): app_instance_id: UUID zone_id: UUID | None = None domain_id: UUID | None = None Loading @@ -61,7 +65,7 @@ class SrmServiceDeployV1(CommandEnvelopeV1): deploy: DeployPayloadV1 class ScalePayloadV1(BaseModel): class ScalePayloadV1(CommandSchema): replicas: int = Field(ge=0) Loading @@ -71,7 +75,7 @@ class SrmServiceScaleV1(CommandEnvelopeV1): scale: ScalePayloadV1 class TerminatePayloadV1(BaseModel): class TerminatePayloadV1(CommandSchema): grace_period_seconds: int = Field(default=0, ge=0) Loading @@ -81,7 +85,7 @@ class SrmServiceTerminateV1(CommandEnvelopeV1): terminate: TerminatePayloadV1 class NetworkCapabilityPayloadV1(BaseModel): class NetworkCapabilityPayloadV1(CommandSchema): capability_type: str target: CapabilityTarget profile_ref: str | None = None Loading @@ -102,7 +106,7 @@ class SrmNetworkCapabilityActivateV1(CommandEnvelopeV1): return self class NetworkCapabilityRealizationRefV1(BaseModel): class NetworkCapabilityRealizationRefV1(CommandSchema): capability_type: str external_ref: str | None = None service_instance_id: UUID | None = None Loading
tests/api/databus/test_schemas.py +48 −6 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ from srm.api.databus.schemas import ( NetworkCapabilityDeactivatePayloadV1, NetworkCapabilityPayloadV1, NetworkCapabilityUpdatePayloadV1, PlacementConstraintsV1, ScalePayloadV1, SrmNetworkCapabilityActivateV1, SrmNetworkCapabilityDeactivateV1, Loading Loading @@ -113,6 +114,48 @@ class TestFederationContext: SrmServiceDeployV1.model_validate(payload) class TestUnknownFields: def test_envelope_rejects_unknown_fields(self) -> None: with pytest.raises(ValidationError, match="Extra inputs are not permitted"): CommandEnvelopeV1.model_validate(_envelope(unexpected="value")) def test_deploy_target_rejects_unknown_pin_typo(self) -> None: with pytest.raises(ValidationError, match="zone_ID"): DeployTargetV1.model_validate( { "app_instance_id": str(uuid4()), "zone_ID": str(uuid4()), } ) def test_deploy_v1_rejects_unknown_nested_target_field(self) -> None: payload = _envelope( service_specification_id=str(uuid4()), targets=[{"app_instance_id": str(uuid4()), "zone_ID": str(uuid4())}], deploy={}, ) with pytest.raises(ValidationError, match="zone_ID"): SrmServiceDeployV1.model_validate(payload) def test_deploy_payload_rejects_unknown_fields(self) -> None: with pytest.raises(ValidationError, match="metadata"): DeployPayloadV1.model_validate({"metadata": {"owner": "test"}}) def test_placement_constraints_allow_open_hints(self) -> None: constraints = PlacementConstraintsV1.model_validate( { "preferred_zone_ref": "athens-edge", "latency_budget_ms": 20, } ) assert constraints.model_extra == { "preferred_zone_ref": "athens-edge", "latency_budget_ms": 20, } def test_deploy_v1_parses_minimal_valid_payload() -> None: payload = _envelope( service_specification_id=str(uuid4()), Loading Loading @@ -190,9 +233,9 @@ class TestPinShape: with pytest.raises(ValidationError, match="domain_id requires zone_id"): SrmNetworkCapabilityActivateV1.model_validate(payload) def test_update_ignores_pins_and_does_not_apply_the_rule(self) -> None: """§B.6: zone_id/domain_id are ignored when identifying an existing realization, so a bare domain_id must not be rejected on update.""" def test_update_rejects_activate_only_pin_fields(self) -> None: """§B.6 update identifies an existing realization; zone/domain pins belong only to activate and are rejected as unknown command fields.""" payload = _envelope( domain_id=str(uuid4()), network_capability={ Loading @@ -202,9 +245,8 @@ class TestPinShape: }, ) command = SrmNetworkCapabilityUpdateV1.model_validate(payload) assert command.network_capability.external_ref == "sess-123" with pytest.raises(ValidationError, match="domain_id"): SrmNetworkCapabilityUpdateV1.model_validate(payload) def test_scale_payload_requires_replicas() -> None: Loading