""" Unit tests for KonstructMessage normalization from Slack event payloads. Tests CHAN-01: Channel Gateway normalizes messages from all channels into unified KonstructMessage format. These tests exercise normalization logic without requiring a live database. """ from __future__ import annotations from datetime import datetime, timezone import pytest from shared.models.message import ChannelType, KonstructMessage, MessageContent, SenderInfo def make_slack_event() -> dict: """Minimal valid Slack message event payload.""" return { "type": "message", "channel": "C12345ABC", "user": "U98765XYZ", "text": "Hey @bot can you help me?", "ts": "1711234567.123456", "thread_ts": "1711234567.123456", "team": "T11223344", "blocks": [], "event_ts": "1711234567.123456", } def normalize_slack_event(payload: dict) -> KonstructMessage: """ Minimal normalizer for test purposes. The real normalizer lives in packages/gateway/channels/slack.py. This function mirrors the expected output for unit testing the model. """ ts = float(payload["ts"]) timestamp = datetime.fromtimestamp(ts, tz=timezone.utc) return KonstructMessage( channel=ChannelType.SLACK, channel_metadata={ "workspace_id": payload["team"], "channel_id": payload["channel"], "event_ts": payload["event_ts"], }, sender=SenderInfo( user_id=payload["user"], display_name=payload["user"], # Display name resolved later by Slack API ), content=MessageContent( text=payload["text"], mentions=[u for u in payload["text"].split() if u.startswith("@")], ), timestamp=timestamp, thread_id=payload.get("thread_ts"), reply_to=None if payload.get("thread_ts") == payload.get("ts") else payload.get("thread_ts"), ) class TestKonstructMessageNormalization: """Tests for Slack event normalization to KonstructMessage.""" def test_channel_type_is_slack(self) -> None: """ChannelType must be set to 'slack' for Slack events.""" msg = normalize_slack_event(make_slack_event()) assert msg.channel == ChannelType.SLACK assert msg.channel == "slack" def test_sender_info_extracted(self) -> None: """SenderInfo user_id must match Slack user field.""" payload = make_slack_event() msg = normalize_slack_event(payload) assert msg.sender.user_id == "U98765XYZ" assert msg.sender.is_bot is False def test_content_text_preserved(self) -> None: """MessageContent.text must contain original Slack message text.""" payload = make_slack_event() msg = normalize_slack_event(payload) assert msg.content.text == "Hey @bot can you help me?" def test_thread_id_from_thread_ts(self) -> None: """thread_id must be populated from Slack's thread_ts field.""" payload = make_slack_event() payload["thread_ts"] = "1711234500.000001" # Different from ts — it's a reply msg = normalize_slack_event(payload) assert msg.thread_id == "1711234500.000001" def test_thread_id_none_when_no_thread(self) -> None: """thread_id must be None when the message is not in a thread.""" payload = make_slack_event() del payload["thread_ts"] msg = normalize_slack_event(payload) assert msg.thread_id is None def test_channel_metadata_contains_workspace_id(self) -> None: """channel_metadata must contain workspace_id (Slack team ID).""" payload = make_slack_event() msg = normalize_slack_event(payload) assert "workspace_id" in msg.channel_metadata assert msg.channel_metadata["workspace_id"] == "T11223344" def test_channel_metadata_contains_channel_id(self) -> None: """channel_metadata must contain the Slack channel ID.""" payload = make_slack_event() msg = normalize_slack_event(payload) assert msg.channel_metadata["channel_id"] == "C12345ABC" def test_tenant_id_is_none_before_resolution(self) -> None: """tenant_id must be None immediately after normalization (Router populates it).""" msg = normalize_slack_event(make_slack_event()) assert msg.tenant_id is None def test_message_has_uuid_id(self) -> None: """KonstructMessage must have a UUID id assigned at construction.""" import uuid msg = normalize_slack_event(make_slack_event()) # Should not raise parsed = uuid.UUID(msg.id) assert str(parsed) == msg.id def test_timestamp_is_utc_datetime(self) -> None: """timestamp must be a timezone-aware datetime in UTC.""" msg = normalize_slack_event(make_slack_event()) assert msg.timestamp.tzinfo is not None assert msg.timestamp.tzinfo == timezone.utc def test_pydantic_validation_rejects_invalid_channel(self) -> None: """KonstructMessage must reject unknown ChannelType values.""" with pytest.raises(Exception): # pydantic.ValidationError KonstructMessage( channel="fax_machine", # type: ignore[arg-type] channel_metadata={}, sender=SenderInfo(user_id="u1", display_name="User"), content=MessageContent(text="hello"), timestamp=datetime.now(timezone.utc), )