"""Tests for the platform adapter registry and dynamic Platform enum.""" import os import pytest from unittest.mock import MagicMock, patch from dataclasses import dataclass from gateway.platform_registry import PlatformRegistry, PlatformEntry, platform_registry from gateway.config import Platform, PlatformConfig, GatewayConfig # ── Platform enum dynamic members ───────────────────────────────────────── class TestPlatformEnumDynamic: """Test that Platform enum accepts unknown values for plugin platforms.""" def test_builtin_members_still_work(self): assert Platform.TELEGRAM.value == "telegram" assert Platform("telegram") is Platform.TELEGRAM def test_dynamic_member_created(self): p = Platform("irc") assert p.value == "irc" assert p.name == "IRC" def test_dynamic_member_identity_stable(self): """Same value returns same object (cached).""" a = Platform("irc") b = Platform("irc") assert a is b def test_dynamic_member_case_normalised(self): """Mixed case normalised to lowercase.""" a = Platform("IRC") b = Platform("irc") assert a is b assert a.value == "irc" def test_dynamic_member_with_hyphens(self): p = Platform("my-platform") assert p.value == "my-platform" assert p.name == "MY_PLATFORM" def test_dynamic_member_rejects_non_string(self): with pytest.raises(ValueError): Platform(123) def test_dynamic_member_rejects_empty(self): with pytest.raises(ValueError): Platform("") def test_dynamic_member_rejects_whitespace_only(self): with pytest.raises(ValueError): Platform(" ") # ── PlatformRegistry ────────────────────────────────────────────────────── class TestPlatformRegistry: """Test the PlatformRegistry itself.""" def _make_entry(self, name="test", check_ok=True, validate_ok=True, factory_ok=True): adapter_mock = MagicMock() return PlatformEntry( name=name, label=name.title(), adapter_factory=lambda cfg, _m=adapter_mock: _m if factory_ok else (_ for _ in ()).throw(RuntimeError("factory error")), check_fn=lambda: check_ok, validate_config=lambda cfg: validate_ok, required_env=[], source="plugin", ), adapter_mock def test_register_and_get(self): reg = PlatformRegistry() entry, _ = self._make_entry("alpha") reg.register(entry) assert reg.get("alpha") is entry assert reg.is_registered("alpha") def test_get_unknown_returns_none(self): reg = PlatformRegistry() assert reg.get("nonexistent") is None def test_unregister(self): reg = PlatformRegistry() entry, _ = self._make_entry("beta") reg.register(entry) assert reg.unregister("beta") is True assert reg.get("beta") is None assert reg.unregister("beta") is False # already gone def test_create_adapter_success(self): reg = PlatformRegistry() entry, mock_adapter = self._make_entry("gamma") reg.register(entry) result = reg.create_adapter("gamma", MagicMock()) assert result is mock_adapter def test_create_adapter_unknown_name(self): reg = PlatformRegistry() assert reg.create_adapter("unknown", MagicMock()) is None def test_create_adapter_check_fails(self): reg = PlatformRegistry() entry, _ = self._make_entry("delta", check_ok=False) reg.register(entry) assert reg.create_adapter("delta", MagicMock()) is None def test_create_adapter_validate_fails(self): reg = PlatformRegistry() entry, _ = self._make_entry("epsilon", validate_ok=False) reg.register(entry) assert reg.create_adapter("epsilon", MagicMock()) is None def test_create_adapter_factory_exception(self): reg = PlatformRegistry() entry = PlatformEntry( name="broken", label="Broken", adapter_factory=lambda cfg: (_ for _ in ()).throw(RuntimeError("boom")), check_fn=lambda: True, validate_config=None, source="plugin", ) reg.register(entry) # factory raises → create_adapter returns None instead of propagating assert reg.create_adapter("broken", MagicMock()) is None def test_create_adapter_no_validate(self): """When validate_config is None, skip validation.""" reg = PlatformRegistry() mock_adapter = MagicMock() entry = PlatformEntry( name="novalidate", label="NoValidate", adapter_factory=lambda cfg: mock_adapter, check_fn=lambda: True, validate_config=None, source="plugin", ) reg.register(entry) assert reg.create_adapter("novalidate", MagicMock()) is mock_adapter def test_all_entries(self): reg = PlatformRegistry() e1, _ = self._make_entry("one") e2, _ = self._make_entry("two") reg.register(e1) reg.register(e2) names = {e.name for e in reg.all_entries()} assert names == {"one", "two"} def test_plugin_entries(self): reg = PlatformRegistry() plugin_entry, _ = self._make_entry("plugged") builtin_entry = PlatformEntry( name="core", label="Core", adapter_factory=lambda cfg: MagicMock(), check_fn=lambda: True, source="builtin", ) reg.register(plugin_entry) reg.register(builtin_entry) plugin_names = {e.name for e in reg.plugin_entries()} assert plugin_names == {"plugged"} def test_re_register_replaces(self): reg = PlatformRegistry() entry1, mock1 = self._make_entry("dup") entry2 = PlatformEntry( name="dup", label="Dup v2", adapter_factory=lambda cfg: "v2", check_fn=lambda: True, source="plugin", ) reg.register(entry1) reg.register(entry2) assert reg.get("dup").label == "Dup v2" # ── GatewayConfig integration ──────────────────────────────────────────── class TestGatewayConfigPluginPlatform: """Test that GatewayConfig parses and validates plugin platforms.""" def test_from_dict_accepts_plugin_platform(self): data = { "platforms": { "telegram": {"enabled": True, "token": "test-token"}, "irc": {"enabled": True, "extra": {"server": "irc.libera.chat"}}, } } cfg = GatewayConfig.from_dict(data) platform_values = {p.value for p in cfg.platforms} assert "telegram" in platform_values assert "irc" in platform_values def test_get_connected_platforms_includes_registered_plugin(self): """Plugin platform with registry entry passes get_connected_platforms.""" # Register a fake plugin platform from gateway.platform_registry import platform_registry as _reg test_entry = PlatformEntry( name="testplat", label="TestPlat", adapter_factory=lambda cfg: MagicMock(), check_fn=lambda: True, validate_config=lambda cfg: bool(cfg.extra.get("token")), source="plugin", ) _reg.register(test_entry) try: data = { "platforms": { "testplat": {"enabled": True, "extra": {"token": "abc"}}, } } cfg = GatewayConfig.from_dict(data) connected = cfg.get_connected_platforms() connected_values = {p.value for p in connected} assert "testplat" in connected_values finally: _reg.unregister("testplat") def test_get_connected_platforms_excludes_unregistered_plugin(self): """Plugin platform without registry entry is excluded.""" data = { "platforms": { "unknown_plugin": {"enabled": True, "extra": {"token": "abc"}}, } } cfg = GatewayConfig.from_dict(data) connected = cfg.get_connected_platforms() connected_values = {p.value for p in connected} assert "unknown_plugin" not in connected_values def test_get_connected_platforms_excludes_invalid_config(self): """Plugin platform with failing validate_config is excluded.""" from gateway.platform_registry import platform_registry as _reg test_entry = PlatformEntry( name="badconfig", label="BadConfig", adapter_factory=lambda cfg: MagicMock(), check_fn=lambda: True, validate_config=lambda cfg: False, # always fails source="plugin", ) _reg.register(test_entry) try: data = { "platforms": { "badconfig": {"enabled": True, "extra": {}}, } } cfg = GatewayConfig.from_dict(data) connected = cfg.get_connected_platforms() connected_values = {p.value for p in connected} assert "badconfig" not in connected_values finally: _reg.unregister("badconfig")