"""Tests for DTO serialization helpers and dataclass contracts.""" from __future__ import annotations import pytest from datetime import datetime from decimal import Decimal from shared.contracts.dtos import ( PostDTO, CalendarDTO, CalendarEntryDTO, TicketDTO, MarketPlaceDTO, ProductDTO, CartItemDTO, CartSummaryDTO, ActorProfileDTO, APActivityDTO, RemotePostDTO, dto_to_dict, dto_from_dict, _serialize_value, _unwrap_optional, ) # --------------------------------------------------------------------------- # _serialize_value # --------------------------------------------------------------------------- class TestSerializeValue: def test_datetime(self): dt = datetime(2025, 6, 15, 12, 30, 0) assert _serialize_value(dt) == "2025-06-15T12:30:00" def test_decimal(self): assert _serialize_value(Decimal("19.99")) == "19.99" def test_set_to_list(self): result = _serialize_value({1, 2, 3}) assert isinstance(result, list) assert set(result) == {1, 2, 3} def test_string_passthrough(self): assert _serialize_value("hello") == "hello" def test_int_passthrough(self): assert _serialize_value(42) == 42 def test_none_passthrough(self): assert _serialize_value(None) is None def test_nested_list(self): dt = datetime(2025, 1, 1) result = _serialize_value([dt, Decimal("5")]) assert result == ["2025-01-01T00:00:00", "5"] def test_nested_dataclass(self): post = PostDTO(id=1, slug="test", title="Test", status="published", visibility="public") result = _serialize_value(post) assert isinstance(result, dict) assert result["slug"] == "test" # --------------------------------------------------------------------------- # _unwrap_optional # --------------------------------------------------------------------------- class TestUnwrapOptional: def test_optional_str(self): from typing import Optional result = _unwrap_optional(Optional[str]) assert result is str def test_optional_int(self): from typing import Optional result = _unwrap_optional(Optional[int]) assert result is int def test_plain_type(self): assert _unwrap_optional(str) is str def test_union_with_none(self): from typing import Union result = _unwrap_optional(Union[datetime, None]) assert result is datetime # --------------------------------------------------------------------------- # dto_to_dict # --------------------------------------------------------------------------- class TestDtoToDict: def test_simple_post(self): post = PostDTO(id=1, slug="my-post", title="My Post", status="published", visibility="public") d = dto_to_dict(post) assert d["id"] == 1 assert d["slug"] == "my-post" assert d["feature_image"] is None def test_with_datetime(self): dt = datetime(2025, 6, 15, 10, 0, 0) post = PostDTO(id=1, slug="s", title="T", status="published", visibility="public", published_at=dt) d = dto_to_dict(post) assert d["published_at"] == "2025-06-15T10:00:00" def test_with_decimal(self): product = ProductDTO(id=1, slug="widget", rrp=Decimal("29.99"), regular_price=Decimal("24.99")) d = dto_to_dict(product) assert d["rrp"] == "29.99" assert d["regular_price"] == "24.99" def test_cart_summary_with_items(self): item = CartItemDTO(id=1, product_id=10, quantity=2, unit_price=Decimal("5.00")) summary = CartSummaryDTO(count=1, total=Decimal("10.00"), items=[item]) d = dto_to_dict(summary) assert d["count"] == 1 assert d["total"] == "10.00" assert len(d["items"]) == 1 assert d["items"][0]["product_id"] == 10 def test_remote_post_with_nested_lists(self): rp = RemotePostDTO( id=1, remote_actor_id=5, object_id="https://example.com/1", content="
Hello
", attachments=[{"type": "Image", "url": "https://img.example.com/1.jpg"}], tags=[{"type": "Hashtag", "name": "#test"}], ) d = dto_to_dict(rp) assert d["attachments"] == [{"type": "Image", "url": "https://img.example.com/1.jpg"}] # --------------------------------------------------------------------------- # dto_from_dict # --------------------------------------------------------------------------- class TestDtoFromDict: def test_none_data(self): assert dto_from_dict(PostDTO, None) is None def test_empty_dict(self): assert dto_from_dict(PostDTO, {}) is None def test_simple_post(self): d = {"id": 1, "slug": "test", "title": "Test", "status": "published", "visibility": "public"} post = dto_from_dict(PostDTO, d) assert post.slug == "test" assert post.feature_image is None def test_datetime_coercion(self): d = { "id": 1, "slug": "s", "title": "T", "status": "published", "visibility": "public", "published_at": "2025-06-15T10:00:00", } post = dto_from_dict(PostDTO, d) assert isinstance(post.published_at, datetime) assert post.published_at.year == 2025 def test_decimal_coercion(self): d = {"id": 1, "slug": "w", "rrp": "29.99", "regular_price": 24.99} product = dto_from_dict(ProductDTO, d) assert isinstance(product.rrp, Decimal) assert product.rrp == Decimal("29.99") assert isinstance(product.regular_price, Decimal) def test_round_trip(self): dt = datetime(2025, 3, 1, 9, 30, 0) original = PostDTO(id=1, slug="rt", title="Round Trip", status="draft", visibility="members", published_at=dt) d = dto_to_dict(original) restored = dto_from_dict(PostDTO, d) assert restored.id == original.id assert restored.slug == original.slug assert restored.published_at == original.published_at def test_extra_keys_ignored(self): d = {"id": 1, "slug": "s", "title": "T", "status": "published", "visibility": "public", "extra_field": "ignored"} post = dto_from_dict(PostDTO, d) assert post.slug == "s" def test_calendar_entry_decimals(self): d = { "id": 1, "calendar_id": 2, "name": "Event", "start_at": "2025-07-01T14:00:00", "state": "confirmed", "cost": "15.00", "ticket_price": "10.50", } entry = dto_from_dict(CalendarEntryDTO, d) assert isinstance(entry.cost, Decimal) assert entry.cost == Decimal("15.00") assert isinstance(entry.ticket_price, Decimal) # --------------------------------------------------------------------------- # Frozen DTOs # --------------------------------------------------------------------------- class TestFrozenDTOs: def test_post_is_frozen(self): post = PostDTO(id=1, slug="s", title="T", status="published", visibility="public") with pytest.raises(AttributeError): post.title = "changed" def test_product_is_frozen(self): product = ProductDTO(id=1, slug="s") with pytest.raises(AttributeError): product.slug = "changed" def test_calendar_dto_defaults(self): cal = CalendarDTO(id=1, container_type="page", container_id=5, name="My Cal", slug="my-cal") assert cal.description is None def test_cart_summary_defaults(self): summary = CartSummaryDTO() assert summary.count == 0 assert summary.total == Decimal("0") assert summary.items == [] assert summary.ticket_count == 0