Files
mono/shared/tests/test_dtos.py
giles 00efbc2a35 Add unit test coverage for shared pure-logic modules (240 tests)
Track 1.1 of master plan: expand from sexp-only tests to cover
DTOs, HTTP signatures, HMAC auth, URL utilities, Jinja filters,
calendar helpers, config freeze, activity bus registry, parse
utilities, sexp helpers, error classes, and jinja bridge render API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:34:37 +00:00

216 lines
7.5 KiB
Python

"""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="<p>Hello</p>",
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