ForcePilot/backend/test/unit/channels/test_whatsapp_reactions_markdown.py

415 lines
15 KiB
Python
Raw Normal View History

from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from yuxi.channels.adapters.whatsapp.reactions.ack_reaction import AckReactionManager
from yuxi.channels.adapters.whatsapp.reactions.reaction_level import ReactionLevel, ReactionLevelController
from yuxi.channels.adapters.whatsapp.markdown import markdown_to_whatsapp, text_sanitizer
from yuxi.channels.adapters.whatsapp.structured_context import (
ContextSource,
ContextType,
StructuredContextEntry,
UntrustedStructuredContext,
)
class TestReactionLevel:
def test_enum_values(self):
assert ReactionLevel.OFF == "off"
assert ReactionLevel.MINIMAL == "minimal"
assert ReactionLevel.FULL == "full"
class TestReactionLevelController:
def test_default_constructor(self):
ctrl = ReactionLevelController({})
assert ctrl.level == ReactionLevel.MINIMAL
def test_off_level(self):
ctrl = ReactionLevelController({"reactionLevel": "off"})
assert ctrl.level == ReactionLevel.OFF
assert ctrl.can_send_reaction() is False
assert ctrl.can_send_reaction(is_ack=True) is False
def test_minimal_level(self):
ctrl = ReactionLevelController({"reactionLevel": "minimal"})
assert ctrl.level == ReactionLevel.MINIMAL
assert ctrl.can_send_reaction() is False
assert ctrl.can_send_reaction(is_ack=True) is True
def test_full_level(self):
ctrl = ReactionLevelController({"reactionLevel": "full"})
assert ctrl.level == ReactionLevel.FULL
assert ctrl.can_send_reaction() is True
assert ctrl.can_send_reaction(is_ack=True) is True
def test_invalid_level_defaults(self):
ctrl = ReactionLevelController({"reactionLevel": "invalid"})
assert ctrl.level == ReactionLevel.MINIMAL
def test_agent_reactions_disabled(self):
ctrl = ReactionLevelController({"reactionLevel": "full", "agentReactionsEnabled": False})
assert ctrl.agent_reactions_enabled is False
assert ctrl.can_agent_react() is False
def test_agent_reactions_off_level(self):
ctrl = ReactionLevelController({"reactionLevel": "off", "agentReactionsEnabled": True})
assert ctrl.can_agent_react() is False
def test_agent_reactions_enabled(self):
ctrl = ReactionLevelController({"reactionLevel": "full", "agentReactionsEnabled": True})
assert ctrl.can_agent_react() is True
def test_agent_reaction_guidance(self):
ctrl = ReactionLevelController({"agentReactionGuidance": "Use emoji"})
assert ctrl.get_agent_guidance() == "Use emoji"
def test_agent_reaction_guidance_empty(self):
ctrl = ReactionLevelController({})
assert ctrl.agent_reaction_guidance == ""
def test_agent_reaction_guidance_none(self):
ctrl = ReactionLevelController({})
assert ctrl.get_agent_guidance() is None
def test_properties_readonly(self):
ctrl = ReactionLevelController({"reactionLevel": "full"})
assert ctrl.level == ReactionLevel.FULL
assert ctrl.agent_reactions_enabled is True
class TestAckReactionManager:
def test_default_constructor(self):
mgr = AckReactionManager({})
assert mgr.emoji == "\u2705"
assert mgr.should_ack_direct() is True
assert mgr.should_ack_group() is True
def test_custom_emoji(self):
mgr = AckReactionManager({"ackReaction": {"emoji": "👍"}})
assert mgr.emoji == "👍"
def test_direct_disabled(self):
mgr = AckReactionManager({"ackReaction": {"direct": False}})
assert mgr.should_ack_direct() is False
def test_group_off(self):
mgr = AckReactionManager({"ackReaction": {"group": "off"}})
assert mgr.should_ack_group() is False
def test_group_mentions_only(self):
mgr = AckReactionManager({"ackReaction": {"group": "mentions"}})
assert mgr.should_ack_group() is True
assert mgr.should_ack_group_mentions_only() is True
def test_can_ack_direct(self):
mgr = AckReactionManager({"ackReaction": {"cooldown": 0}})
assert mgr.can_ack("chat1", is_group=False) is True
def test_can_ack_group_full(self):
mgr = AckReactionManager({"ackReaction": {"group": "full", "cooldown": 0}})
assert mgr.can_ack("chat1", is_group=True) is True
def test_can_ack_group_mentions_only_mentioned(self):
mgr = AckReactionManager({"ackReaction": {"group": "mentions", "cooldown": 0}})
assert mgr.can_ack("chat1", is_group=True, is_mentioned=True) is True
def test_can_ack_group_mentions_only_not_mentioned(self):
mgr = AckReactionManager({"ackReaction": {"group": "mentions", "cooldown": 0}})
assert mgr.can_ack("chat1", is_group=True, is_mentioned=False) is False
def test_can_ack_group_off(self):
mgr = AckReactionManager({"ackReaction": {"group": "off"}})
assert mgr.can_ack("chat1", is_group=True) is False
def test_can_ack_direct_disabled(self):
mgr = AckReactionManager({"ackReaction": {"direct": False, "cooldown": 0}})
assert mgr.can_ack("chat1", is_group=False) is False
def test_cooldown_prevents_ack(self):
mgr = AckReactionManager({"ackReaction": {"cooldown": 60}})
assert mgr.can_ack("chat1", is_group=False) is True
mgr.record_ack("chat1")
assert mgr.can_ack("chat1", is_group=False) is False
def test_different_chats_independent_cooldown(self):
mgr = AckReactionManager({"ackReaction": {"cooldown": 60}})
assert mgr.can_ack("chat1", is_group=False) is True
mgr.record_ack("chat1")
assert mgr.can_ack("chat2", is_group=False) is True
def test_record_ack(self):
mgr = AckReactionManager({})
mgr.record_ack("chat1")
class TestTextSanitizer:
def test_empty_string(self):
assert text_sanitizer("") == ""
def test_none_like(self):
assert text_sanitizer("") == ""
def test_nbsp_replacement(self):
assert text_sanitizer("hello world") == "hello world"
def test_allows_b_tag(self):
assert text_sanitizer("<b>bold</b>") == "<b>bold</b>"
def test_allows_i_tag(self):
assert text_sanitizer("<i>italic</i>") == "<i>italic</i>"
def test_allows_strong_tag(self):
assert text_sanitizer("<strong>bold</strong>") == "<strong>bold</strong>"
def test_allows_em_tag(self):
assert text_sanitizer("<em>em</em>") == "<em>em</em>"
def test_allows_u_tag(self):
assert text_sanitizer("<u>underline</u>") == "<u>underline</u>"
def test_allows_s_tag(self):
assert text_sanitizer("<s>strikethrough</s>") == "<s>strikethrough</s>"
def test_allows_code_tag(self):
assert text_sanitizer("<code>code</code>") == "<code>code</code>"
def test_allows_pre_tag(self):
assert text_sanitizer("<pre>pre</pre>") == "<pre>pre</pre>"
def test_allows_a_tag(self):
assert text_sanitizer('<a href="url">link</a>') == '<a href="url">link</a>'
def test_strips_script_tag(self):
result = text_sanitizer("<script>alert(1)</script>")
assert "<script>" not in result
assert "</script>" not in result
def test_strips_unknown_tags(self):
assert text_sanitizer("<div>text</div>") == "text"
assert text_sanitizer("<span>text</span>") == "text"
def test_strips_mixed_allowed_disallowed(self):
result = text_sanitizer("<b>safe</b><script>bad</script>")
assert "<b>safe</b>" in result
assert "<script>" not in result
def test_strips_text(self):
assert text_sanitizer(" hello ") == "hello"
def test_plain_text_unchanged(self):
assert text_sanitizer("Hello World") == "Hello World"
class TestMarkdownToWhatsapp:
def test_empty_string(self):
assert markdown_to_whatsapp("") == ""
def test_bold_conversion(self):
assert markdown_to_whatsapp("**bold**") == "*bold*"
def test_double_underscore_bold(self):
assert markdown_to_whatsapp("__bold__") == "*bold*"
def test_italic_conversion(self):
result = markdown_to_whatsapp("*italic*")
assert "italic" in result
def test_bold_italic_combo(self):
result = markdown_to_whatsapp("***bold italic***")
assert "bold italic" in result
def test_strikethrough(self):
result = markdown_to_whatsapp("~~strike~~")
assert "strike" in result
assert "~~" not in result
def test_inline_code(self):
result = markdown_to_whatsapp("`code`")
assert "```" in result
assert "code" in result
def test_code_block(self):
result = markdown_to_whatsapp("```python\nprint('hello')\n```")
assert "python" in result
assert "print" in result
def test_heading_conversion(self):
result = markdown_to_whatsapp("# Heading 1")
assert "Heading 1" in result
assert "# " not in result
def test_heading2_conversion(self):
result = markdown_to_whatsapp("## Heading 2")
assert "Heading 2" in result
assert "## " not in result
def test_heading3_conversion(self):
result = markdown_to_whatsapp("### Heading 3")
assert "Heading 3" in result
assert "### " not in result
def test_link_with_different_text(self):
result = markdown_to_whatsapp("[Click here](https://example.com)")
assert "Click here" in result
assert "example.com" in result
def test_link_same_text_url(self):
result = markdown_to_whatsapp("[https://example.com](https://example.com)")
assert result == "https://example.com"
def test_unordered_list(self):
result = markdown_to_whatsapp("- item1\n- item2")
assert "• item1" in result
assert "• item2" in result
def test_ordered_list(self):
result = markdown_to_whatsapp("1. item1\n2. item2")
assert "• item1" in result
assert "• item2" in result
def test_br_to_newline(self):
result = markdown_to_whatsapp("line1<br>line2")
assert "line1\nline2" in result
def test_excessive_newlines_collapsed(self):
result = markdown_to_whatsapp("a\n\n\n\nb")
assert result.count("\n\n") <= 1
def test_complex_markdown(self):
result = markdown_to_whatsapp("**bold** and *italic* with `code`")
assert "bold" in result
assert "italic" in result
assert "code" in result
def test_table_conversion(self):
md = "| Name | Age |\n|------|-----|\n| John | 30 |"
result = markdown_to_whatsapp(md)
assert "John" in result
def test_empty_link_text(self):
result = markdown_to_whatsapp("[](https://example.com)")
assert "example.com" in result
class TestStructuredContextEntry:
def test_create_entry(self):
entry = StructuredContextEntry(
label="test",
source=ContextSource.BUTTON,
type=ContextType.STRUCTURED_DATA,
data={"key": "value"},
)
assert entry.label == "test"
assert entry.source == ContextSource.BUTTON
assert entry.type == ContextType.STRUCTURED_DATA
assert entry.data["key"] == "value"
def test_default_values(self):
entry = StructuredContextEntry(label="default_test")
assert entry.source == ContextSource.UNKNOWN
assert entry.type == ContextType.STRUCTURED_DATA
assert entry.data == {}
class TestUntrustedStructuredContext:
def test_empty_context(self):
ctx = UntrustedStructuredContext()
assert ctx.is_empty() is True
assert len(ctx.entries) == 0
def test_add_entry(self):
ctx = UntrustedStructuredContext()
entry = StructuredContextEntry(label="test")
ctx.add_entry(entry)
assert ctx.is_empty() is False
assert len(ctx.entries) == 1
def test_to_metadata(self):
ctx = UntrustedStructuredContext()
entry = StructuredContextEntry(
label="location",
source=ContextSource.LOCATION,
data={"latitude": 39.9, "longitude": 116.4},
)
ctx.add_entry(entry)
meta = ctx.to_metadata()
assert "structured_location" in meta
assert meta["structured_location"]["source"] == "location"
def test_get_by_label(self):
ctx = UntrustedStructuredContext()
entry = StructuredContextEntry(label="contact")
ctx.add_entry(entry)
assert ctx.get_by_label("contact") is not None
assert ctx.get_by_label("nonexistent") is None
def test_get_by_source(self):
ctx = UntrustedStructuredContext()
ctx.add_entry(StructuredContextEntry(label="btn1", source=ContextSource.BUTTON))
ctx.add_entry(StructuredContextEntry(label="btn2", source=ContextSource.BUTTON))
ctx.add_entry(StructuredContextEntry(label="loc1", source=ContextSource.LOCATION))
buttons = ctx.get_by_source(ContextSource.BUTTON)
assert len(buttons) == 2
locations = ctx.get_by_source(ContextSource.LOCATION)
assert len(locations) == 1
def test_from_contact(self):
ctx = UntrustedStructuredContext.from_contact("John", "vcard_data", [{"name": "Jane"}])
assert len(ctx.entries) == 1
assert ctx.entries[0].label == "contact"
def test_from_location(self):
ctx = UntrustedStructuredContext.from_location(39.9, 116.4, "Beijing", "China")
assert len(ctx.entries) == 1
assert ctx.entries[0].label == "location"
assert ctx.entries[0].data["latitude"] == 39.9
def test_from_reaction(self):
ctx = UntrustedStructuredContext.from_reaction("👍", "msg-001", "test@s.whatsapp.net")
assert len(ctx.entries) == 1
assert ctx.entries[0].label == "reaction"
def test_from_poll(self):
ctx = UntrustedStructuredContext.from_poll("Vote", ["A", "B"], ["A"])
assert len(ctx.entries) == 1
assert ctx.entries[0].label == "poll"
def test_from_button(self):
ctx = UntrustedStructuredContext.from_button("btn-1", "Approve")
assert len(ctx.entries) == 1
assert ctx.entries[0].label == "button"
def test_from_forward(self):
ctx = UntrustedStructuredContext.from_forward(3, "newsletter@jid")
assert len(ctx.entries) == 1
assert ctx.entries[0].label == "forward"
assert ctx.entries[0].data["is_forwarded"] is True
def test_from_mentions(self):
ctx = UntrustedStructuredContext.from_mentions(["jid1", "jid2"])
assert len(ctx.entries) == 1
assert ctx.entries[0].label == "mention"
assert len(ctx.entries[0].data["mentioned_jids"]) == 2
class TestContextSource:
def test_enum_values(self):
assert ContextSource.CONTACT == "contact"
assert ContextSource.LOCATION == "location"
assert ContextSource.REACTION == "reaction"
assert ContextSource.POLL == "poll"
assert ContextSource.BUTTON == "button"
assert ContextSource.FORWARD == "forward"
assert ContextSource.MENTION == "mention"
assert ContextSource.MEDIA == "media"
assert ContextSource.UNKNOWN == "unknown"
class TestContextType:
def test_enum_values(self):
assert ContextType.STRUCTURED_DATA == "structured_data"
assert ContextType.USER_GENERATED == "user_generated"
assert ContextType.SYSTEM == "system"