"""Юнит-тесты envelope.py — контракт зеркалит app/api/schemas.py сервера."""
import logging
from datetime import UTC, datetime
import pytest
from gnexus_synapse.envelope import build_envelope, warn_bad_user_id
from gnexus_synapse.exceptions import SynapseValidationError
def test_minimal_envelope_exact_keys():
env = build_envelope("bugtrail", "test", "failed")
assert env == {"source": "bugtrail", "subject": "test", "action": "failed"}
def test_none_fields_never_sent():
# сервер extra="forbid" — None-поле это 422; клиент не должен их слать
env = build_envelope("s", "x", "y", payload=None, dedup_key=None, ttl_seconds=None,
scheduled_at=None)
assert set(env) == {"source", "subject", "action"}
env2 = build_envelope("s", "x", "y", payload={})
assert "payload" not in env2
def test_full_envelope_values():
ts = datetime(2026, 10, 3, 12, 0, tzinfo=UTC)
env = build_envelope("s", "x", "y", priority="high", payload={"user_id": "42"},
dedup_key="d-1", ttl_seconds=60, scheduled_at=ts)
assert env == {"source": "s", "subject": "x", "action": "y", "priority": "high",
"payload": {"user_id": "42"}, "dedup_key": "d-1", "ttl_seconds": 60,
"scheduled_at": "2026-10-03T12:00:00+00:00"}
# default priority не шлётся — равен серверному дефолту
env3 = build_envelope("s", "x", "y", priority="normal")
assert "priority" not in env3
def test_pattern_names():
# валидные
for name in ("a", "bugtrail", "test.failed", "task-created", "v2", "a1_b.c-d"):
build_envelope(name, name, name)
# невалидные: uppercase, пробел, ведущий/хвостовой разделитель, пусто, длина 65
for bad in ("Bugtrail", "a b", "-lead", "trail-", "_lead", "trail_", "", "a" * 65, "спецсимволы"):
with pytest.raises(SynapseValidationError):
build_envelope(bad, "x", "y")
with pytest.raises(SynapseValidationError):
build_envelope("s", bad, "y")
with pytest.raises(SynapseValidationError):
build_envelope("s", "x", bad)
def test_bad_priority_ttl_dedup():
with pytest.raises(SynapseValidationError, match="priority"):
build_envelope("s", "x", "y", priority="urgent")
for bad_ttl in (0, -5, 604801, 1.5, "3600", True):
with pytest.raises(SynapseValidationError, match="ttl_seconds"):
build_envelope("s", "x", "y", ttl_seconds=bad_ttl)
assert build_envelope("s", "x", "y", ttl_seconds=604800)["ttl_seconds"] == 604800
with pytest.raises(SynapseValidationError, match="dedup_key"):
build_envelope("s", "x", "y", dedup_key="d" * 256)
assert "dedup_key" not in build_envelope("s", "x", "y", dedup_key="")
with pytest.raises(SynapseValidationError, match="payload"):
build_envelope("s", "x", "y", payload=["не", "словарь"]) # type: ignore[arg-type]
def test_naive_scheduled_at_treated_as_utc():
env = build_envelope("s", "x", "y", scheduled_at=datetime(2026, 10, 3, 12, 0))
assert env["scheduled_at"] == "2026-10-03T12:00:00+00:00"
def test_user_id_warning(caplog):
log = logging.getLogger("test.syn")
ok = {"user_id": "42", "x": 1}
warn_bad_user_id(ok, log)
warn_bad_user_id({"user_id": ""}, log)
warn_bad_user_id({"user_id": None}, log)
warn_bad_user_id({"user_id": {}} , log)
warn_bad_user_id({"user_id": True}, log)
warn_bad_user_id({}, log)
# 5 warning'ов (пустая строка, None, dict, bool, отсутствие)
assert len([r for r in caplog.records if r.levelno == logging.WARNING]) == 5
warn_bad_user_id({"user_id": 42}, log) # число ок