""" Unit tests for WhatsApp event normalization to KonstructMessage. Tests CHAN-03: Messages are normalized to the unified KonstructMessage format. Covers: - Text message normalizes correctly - Image message normalizes with MediaAttachment - Document message normalizes with MediaAttachment - Correct field mapping: sender, channel, metadata - ChannelType includes 'whatsapp' - thread_id set to sender wa_id (WhatsApp has no threads) - tenant_id is None after normalization """ from __future__ import annotations from datetime import timezone import pytest from shared.models.message import ChannelType, MediaAttachment, MediaType def make_text_webhook_payload( phone_number_id: str = "12345678901", wa_id: str = "5511987654321", message_id: str = "wamid.abc123", text: str = "Hello, I need help with my order", timestamp: str = "1711234567", ) -> dict: """Minimal valid Meta Cloud API v20.0 text message webhook payload.""" return { "object": "whatsapp_business_account", "entry": [ { "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", "changes": [ { "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "15550000000", "phone_number_id": phone_number_id, }, "contacts": [ { "profile": {"name": "John Customer"}, "wa_id": wa_id, } ], "messages": [ { "from": wa_id, "id": message_id, "timestamp": timestamp, "text": {"body": text}, "type": "text", } ], }, "field": "messages", } ], } ], } def make_image_webhook_payload( phone_number_id: str = "12345678901", wa_id: str = "5511987654321", message_id: str = "wamid.img456", media_id: str = "media_id_xyz789", mime_type: str = "image/jpeg", ) -> dict: """Minimal valid Meta Cloud API v20.0 image message webhook payload.""" return { "object": "whatsapp_business_account", "entry": [ { "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", "changes": [ { "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "15550000000", "phone_number_id": phone_number_id, }, "contacts": [ { "profile": {"name": "Jane Customer"}, "wa_id": wa_id, } ], "messages": [ { "from": wa_id, "id": message_id, "timestamp": "1711234567", "image": { "id": media_id, "mime_type": mime_type, "sha256": "abc123hash", }, "type": "image", } ], }, "field": "messages", } ], } ], } def make_document_webhook_payload( phone_number_id: str = "12345678901", wa_id: str = "5511987654321", message_id: str = "wamid.doc789", media_id: str = "media_id_doc001", filename: str = "invoice.pdf", ) -> dict: """Minimal valid Meta Cloud API v20.0 document message webhook payload.""" return { "object": "whatsapp_business_account", "entry": [ { "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", "changes": [ { "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "15550000000", "phone_number_id": phone_number_id, }, "contacts": [ { "profile": {"name": "Bob Customer"}, "wa_id": wa_id, } ], "messages": [ { "from": wa_id, "id": message_id, "timestamp": "1711234567", "document": { "id": media_id, "mime_type": "application/pdf", "filename": filename, }, "type": "document", } ], }, "field": "messages", } ], } ], } class TestChannelTypeWhatsApp: """Tests for ChannelType enum including WhatsApp.""" def test_whatsapp_in_channel_type(self) -> None: """ChannelType enum must include 'whatsapp' value.""" assert ChannelType.WHATSAPP == "whatsapp" assert "whatsapp" in [c.value for c in ChannelType] def test_whatsapp_channel_type_str(self) -> None: """ChannelType.WHATSAPP must equal string 'whatsapp'.""" assert str(ChannelType.WHATSAPP) == "whatsapp" class TestMediaAttachmentModel: """Tests for the MediaAttachment model.""" def test_media_attachment_image_fields(self) -> None: """MediaAttachment must accept image type with required fields.""" attachment = MediaAttachment( media_type=MediaType.IMAGE, mime_type="image/jpeg", ) assert attachment.media_type == MediaType.IMAGE assert attachment.mime_type == "image/jpeg" assert attachment.url is None assert attachment.storage_key is None def test_media_attachment_document_with_filename(self) -> None: """MediaAttachment must accept document type with filename.""" attachment = MediaAttachment( media_type=MediaType.DOCUMENT, mime_type="application/pdf", filename="report.pdf", size_bytes=102400, ) assert attachment.media_type == MediaType.DOCUMENT assert attachment.filename == "report.pdf" assert attachment.size_bytes == 102400 def test_media_attachment_all_fields_optional_except_type(self) -> None: """MediaAttachment requires only media_type; all other fields are optional.""" attachment = MediaAttachment(media_type=MediaType.AUDIO) assert attachment.media_type == MediaType.AUDIO assert attachment.url is None assert attachment.storage_key is None assert attachment.mime_type is None assert attachment.filename is None assert attachment.size_bytes is None class TestNormalizeWhatsAppText: """Tests for normalizing WhatsApp text messages.""" def test_channel_type_is_whatsapp(self) -> None: """Normalized message must have channel=whatsapp.""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload() msg = normalize_whatsapp_event(payload) assert msg.channel == ChannelType.WHATSAPP def test_sender_user_id_is_wa_id(self) -> None: """sender.user_id must be the WhatsApp ID (wa_id) of the sender.""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload(wa_id="5511987654321") msg = normalize_whatsapp_event(payload) assert msg.sender.user_id == "5511987654321" def test_content_text_extracted(self) -> None: """content.text must contain the message body text.""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload(text="Can you help me track my order?") msg = normalize_whatsapp_event(payload) assert msg.content.text == "Can you help me track my order?" def test_channel_metadata_phone_number_id(self) -> None: """channel_metadata must contain phone_number_id.""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload(phone_number_id="99988877766") msg = normalize_whatsapp_event(payload) assert msg.channel_metadata["phone_number_id"] == "99988877766" def test_channel_metadata_message_id(self) -> None: """channel_metadata must contain the WhatsApp message ID (wamid).""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload(message_id="wamid.unique123") msg = normalize_whatsapp_event(payload) assert msg.channel_metadata["message_id"] == "wamid.unique123" def test_thread_id_is_sender_wa_id(self) -> None: """thread_id must be the sender's wa_id (WhatsApp has no threads).""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload(wa_id="5511999998888") msg = normalize_whatsapp_event(payload) assert msg.thread_id == "5511999998888" def test_tenant_id_none_after_normalization(self) -> None: """tenant_id must be None immediately after normalization.""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload() msg = normalize_whatsapp_event(payload) assert msg.tenant_id is None def test_timestamp_is_utc(self) -> None: """timestamp must be a timezone-aware datetime in UTC.""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload(timestamp="1711234567") msg = normalize_whatsapp_event(payload) assert msg.timestamp.tzinfo is not None assert msg.timestamp.tzinfo == timezone.utc def test_no_media_attachments_for_text_message(self) -> None: """Text message must not produce any media attachments.""" from gateway.normalize import normalize_whatsapp_event payload = make_text_webhook_payload() msg = normalize_whatsapp_event(payload) assert msg.content.media == [] class TestNormalizeWhatsAppImage: """Tests for normalizing WhatsApp image messages.""" def test_image_message_has_media_attachment(self) -> None: """Image message must produce a MediaAttachment in content.media.""" from gateway.normalize import normalize_whatsapp_event payload = make_image_webhook_payload() msg = normalize_whatsapp_event(payload) assert len(msg.content.media) == 1 def test_image_attachment_type_is_image(self) -> None: """MediaAttachment media_type must be IMAGE for image messages.""" from gateway.normalize import normalize_whatsapp_event payload = make_image_webhook_payload() msg = normalize_whatsapp_event(payload) assert msg.content.media[0].media_type == MediaType.IMAGE def test_image_attachment_mime_type(self) -> None: """MediaAttachment mime_type must be preserved from webhook payload.""" from gateway.normalize import normalize_whatsapp_event payload = make_image_webhook_payload(mime_type="image/png") msg = normalize_whatsapp_event(payload) assert msg.content.media[0].mime_type == "image/png" def test_image_attachment_url_contains_media_id(self) -> None: """MediaAttachment url must contain the media_id as a placeholder for later download.""" from gateway.normalize import normalize_whatsapp_event payload = make_image_webhook_payload(media_id="media_id_xyz789") msg = normalize_whatsapp_event(payload) assert msg.content.media[0].url is not None assert "media_id_xyz789" in msg.content.media[0].url def test_image_message_text_is_empty_string(self) -> None: """Image message with no caption must have empty string as text content.""" from gateway.normalize import normalize_whatsapp_event payload = make_image_webhook_payload() msg = normalize_whatsapp_event(payload) assert msg.content.text == "" class TestNormalizeWhatsAppDocument: """Tests for normalizing WhatsApp document messages.""" def test_document_message_has_media_attachment(self) -> None: """Document message must produce a MediaAttachment in content.media.""" from gateway.normalize import normalize_whatsapp_event payload = make_document_webhook_payload() msg = normalize_whatsapp_event(payload) assert len(msg.content.media) == 1 def test_document_attachment_type_is_document(self) -> None: """MediaAttachment media_type must be DOCUMENT for document messages.""" from gateway.normalize import normalize_whatsapp_event payload = make_document_webhook_payload() msg = normalize_whatsapp_event(payload) assert msg.content.media[0].media_type == MediaType.DOCUMENT def test_document_attachment_filename(self) -> None: """MediaAttachment filename must be preserved from webhook payload.""" from gateway.normalize import normalize_whatsapp_event payload = make_document_webhook_payload(filename="contract.pdf") msg = normalize_whatsapp_event(payload) assert msg.content.media[0].filename == "contract.pdf"