"""Tests for all Pydantic models — encode/decode roundtrips and field contracts.""" import json import pytest from pydantic import ValidationError from engine.types.operation import Operation from engine.types.snapshot import Snapshot from server.types.sync_request import SyncRequest from server.types.sync_response import SyncResponse # --------------------------------------------------------------------------- # Operation # --------------------------------------------------------------------------- class TestOperation: def _full_op(self) -> Operation: return Operation( id="op-1", type="insert", device_id="dev-1", timestamp="1000000.0", entity_id="entity-42", entity_type="object", payload={"key": "value", "count": 3}, ) def test_encode_returns_bytes(self) -> None: assert isinstance(self._full_op().encode(), bytes) def test_encode_decode_roundtrip(self) -> None: op = self._full_op() assert Operation.decode(op.encode()) == op def test_all_fields_optional_allows_empty_instance(self) -> None: op = Operation() assert op.id is None assert op.type is None def test_decode_partial_json(self) -> None: """Only required fields in bytes → remaining fields default to None.""" raw = b'{"id": "x"}' op = Operation.decode(raw) assert op.id == "x" assert op.type is None def test_encode_is_utf8_json(self) -> None: op = self._full_op() parsed = json.loads(op.encode().decode("utf-8")) assert parsed["id"] == "op-1" assert parsed["payload"] == {"key": "value", "count": 3} # --------------------------------------------------------------------------- # Snapshot # --------------------------------------------------------------------------- class TestSnapshot: def _full_snap(self) -> Snapshot: return Snapshot(id="snap-1", cursor=10, data={"users": []}, timestamp=1000) def test_encode_returns_bytes(self) -> None: assert isinstance(self._full_snap().encode(), bytes) def test_encode_decode_roundtrip(self) -> None: snap = self._full_snap() assert Snapshot.decode(snap.encode()) == snap def test_all_fields_optional(self) -> None: snap = Snapshot() assert snap.id is None assert snap.cursor is None assert snap.data is None assert snap.timestamp is None # --------------------------------------------------------------------------- # SyncRequest # --------------------------------------------------------------------------- class TestSyncRequest: def _make(self, cursor: int = 0, ops: list[Operation] | None = None) -> SyncRequest: return SyncRequest( device_id="client-1", cursor=cursor, operations=ops or [], ) def test_encode_returns_bytes(self) -> None: assert isinstance(self._make().encode(), bytes) def test_encode_decode_roundtrip_empty_ops(self) -> None: req = self._make(cursor=5) assert SyncRequest.decode(req.encode()) == req def test_encode_decode_roundtrip_with_ops(self) -> None: op = Operation( id="1", type="insert", device_id="client-1", timestamp="1.0", entity_id="e1", entity_type="obj", payload={}, ) req = self._make(cursor=0, ops=[op]) decoded = SyncRequest.decode(req.encode()) assert decoded.device_id == "client-1" assert len(decoded.operations) == 1 assert decoded.operations[0].id == "1" def test_required_fields_enforced(self) -> None: with pytest.raises(ValidationError): SyncRequest(cursor=0, operations=[]) # type: ignore[call-arg] def test_cursor_required(self) -> None: with pytest.raises(ValidationError): SyncRequest(device_id="x", operations=[]) # type: ignore[call-arg] # --------------------------------------------------------------------------- # SyncResponse # --------------------------------------------------------------------------- class TestSyncResponse: def _make( self, cursor: int = 0, ops: list[Operation] | None = None, snapshot: Snapshot | None = None, ) -> SyncResponse: return SyncResponse(cursor=cursor, operations=ops or [], snapshot=snapshot) def test_encode_returns_bytes(self) -> None: assert isinstance(self._make().encode(), bytes) def test_encode_decode_roundtrip_no_snapshot(self) -> None: res = self._make(cursor=3) assert SyncResponse.decode(res.encode()) == res def test_encode_decode_roundtrip_with_snapshot(self) -> None: snap = Snapshot(id="s1", cursor=3, data={}, timestamp=999) res = self._make(cursor=3, snapshot=snap) decoded = SyncResponse.decode(res.encode()) assert decoded.snapshot is not None assert decoded.snapshot.id == "s1" def test_snapshot_defaults_to_none(self) -> None: res = self._make() assert res.snapshot is None def test_operations_carries_nested_ops(self) -> None: op = Operation( id="2", type="delete", device_id="dev-1", timestamp="2.0", entity_id="e2", entity_type="obj", payload={}, ) res = self._make(cursor=1, ops=[op]) decoded = SyncResponse.decode(res.encode()) assert decoded.operations[0].type == "delete"