"""Tests for the BlueBubbles iMessage gateway adapter.""" import asyncio import json import pytest from gateway.config import Platform, PlatformConfig def _make_adapter(monkeypatch, **extra): monkeypatch.setenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234") monkeypatch.setenv("BLUEBUBBLES_PASSWORD", "secret") from gateway.platforms.bluebubbles import BlueBubblesAdapter cfg = PlatformConfig( enabled=True, extra={ "server_url": "http://localhost:1234", "password": "secret", **extra, }, ) return BlueBubblesAdapter(cfg) class TestBlueBubblesConfigLoading: def test_apply_env_overrides_bluebubbles(self, monkeypatch): monkeypatch.setenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234") monkeypatch.setenv("BLUEBUBBLES_PASSWORD", "secret") monkeypatch.setenv("BLUEBUBBLES_WEBHOOK_PORT", "9999") monkeypatch.setenv("BLUEBUBBLES_REQUIRE_MENTION", "true") monkeypatch.setenv("BLUEBUBBLES_MENTION_PATTERNS", r'["(?i)^amos\\b"]') from gateway.config import GatewayConfig, _apply_env_overrides config = GatewayConfig() _apply_env_overrides(config) assert Platform.BLUEBUBBLES in config.platforms bc = config.platforms[Platform.BLUEBUBBLES] assert bc.enabled is True assert bc.extra["server_url"] == "http://localhost:1234" assert bc.extra["password"] == "secret" assert bc.extra["webhook_port"] == 9999 assert bc.extra["require_mention"] is True assert bc.extra["mention_patterns"] == ["(?i)^amos\\b"] def test_home_channel_set_from_env(self, monkeypatch): monkeypatch.setenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234") monkeypatch.setenv("BLUEBUBBLES_PASSWORD", "secret") monkeypatch.setenv("BLUEBUBBLES_HOME_CHANNEL", "user@example.com") from gateway.config import GatewayConfig, _apply_env_overrides config = GatewayConfig() _apply_env_overrides(config) hc = config.platforms[Platform.BLUEBUBBLES].home_channel assert hc is not None assert hc.chat_id == "user@example.com" def test_not_connected_without_password(self, monkeypatch): monkeypatch.setenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234") monkeypatch.delenv("BLUEBUBBLES_PASSWORD", raising=False) from gateway.config import GatewayConfig, _apply_env_overrides config = GatewayConfig() _apply_env_overrides(config) assert Platform.BLUEBUBBLES not in config.get_connected_platforms() class TestBlueBubblesHelpers: def test_check_requirements(self, monkeypatch): monkeypatch.setenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234") monkeypatch.setenv("BLUEBUBBLES_PASSWORD", "secret") from gateway.platforms.bluebubbles import check_bluebubbles_requirements assert check_bluebubbles_requirements() is True def test_supports_message_editing_is_false(self, monkeypatch): adapter = _make_adapter(monkeypatch) assert adapter.SUPPORTS_MESSAGE_EDITING is False def test_truncate_message_omits_pagination_suffixes(self, monkeypatch): adapter = _make_adapter(monkeypatch) chunks = adapter.truncate_message("abcdefghij", max_length=6) assert len(chunks) > 1 assert "".join(chunks) == "abcdefghij" assert all("(" not in chunk for chunk in chunks) @pytest.mark.asyncio async def test_send_splits_paragraphs_into_multiple_bubbles(self, monkeypatch): adapter = _make_adapter(monkeypatch) sent = [] async def fake_resolve_chat_guid(chat_id): return "iMessage;-;user@example.com" async def fake_api_post(path, payload): sent.append(payload["message"]) return {"data": {"guid": f"msg-{len(sent)}"}} monkeypatch.setattr(adapter, "_resolve_chat_guid", fake_resolve_chat_guid) monkeypatch.setattr(adapter, "_api_post", fake_api_post) result = await adapter.send("user@example.com", "first thought\n\nsecond thought") assert result.success is True assert sent == ["first thought", "second thought"] def test_format_message_strips_markdown(self, monkeypatch): adapter = _make_adapter(monkeypatch) assert adapter.format_message("**Hello** `world`") == "Hello world" def test_format_message_preserves_underscores_in_identifiers(self, monkeypatch): adapter = _make_adapter(monkeypatch) text = "Use /api_v2 with FEATURE_FLAG_NAME and config_file.json" assert adapter.format_message(text) == text def test_strip_markdown_headers(self, monkeypatch): adapter = _make_adapter(monkeypatch) assert adapter.format_message("## Heading\ntext") == "Heading\ntext" def test_strip_markdown_links(self, monkeypatch): adapter = _make_adapter(monkeypatch) assert adapter.format_message("[click here](http://example.com)") == "click here" def test_init_normalizes_webhook_path(self, monkeypatch): adapter = _make_adapter(monkeypatch, webhook_path="bluebubbles-webhook") assert adapter.webhook_path == "/bluebubbles-webhook" def test_init_preserves_leading_slash(self, monkeypatch): adapter = _make_adapter(monkeypatch, webhook_path="/my-hook") assert adapter.webhook_path == "/my-hook" def test_server_url_normalized(self, monkeypatch): adapter = _make_adapter(monkeypatch, server_url="http://localhost:1234/") assert adapter.server_url == "http://localhost:1234" def test_server_url_adds_scheme(self, monkeypatch): adapter = _make_adapter(monkeypatch, server_url="localhost:1234") assert adapter.server_url == "http://localhost:1234" def test_default_mention_patterns_match_hermes_variants(self, monkeypatch): adapter = _make_adapter(monkeypatch, require_mention=True) assert adapter.require_mention is True assert adapter._message_matches_mention_patterns("Hermes, summarize this") assert adapter._message_matches_mention_patterns("@Hermes agent help") assert not adapter._message_matches_mention_patterns("casual family chatter") assert not adapter._message_matches_mention_patterns("antihermes should not match") def test_custom_mention_patterns_override_defaults(self, monkeypatch): adapter = _make_adapter( monkeypatch, require_mention=True, mention_patterns=[r"(?