Commit 38872baa authored by Paris Stentoumis's avatar Paris Stentoumis
Browse files

fix: databus messages schemas are now more strict. no extra fields are allowed. more tests

parent ea042771
Loading
Loading
Loading
Loading
+14 −10
Original line number Diff line number Diff line
@@ -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,
@@ -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
@@ -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
@@ -61,7 +65,7 @@ class SrmServiceDeployV1(CommandEnvelopeV1):
    deploy: DeployPayloadV1


class ScalePayloadV1(BaseModel):
class ScalePayloadV1(CommandSchema):
    replicas: int = Field(ge=0)


@@ -71,7 +75,7 @@ class SrmServiceScaleV1(CommandEnvelopeV1):
    scale: ScalePayloadV1


class TerminatePayloadV1(BaseModel):
class TerminatePayloadV1(CommandSchema):
    grace_period_seconds: int = Field(default=0, ge=0)


@@ -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
@@ -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
+48 −6
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ from srm.api.databus.schemas import (
    NetworkCapabilityDeactivatePayloadV1,
    NetworkCapabilityPayloadV1,
    NetworkCapabilityUpdatePayloadV1,
    PlacementConstraintsV1,
    ScalePayloadV1,
    SrmNetworkCapabilityActivateV1,
    SrmNetworkCapabilityDeactivateV1,
@@ -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()),
@@ -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={
@@ -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: