From 64746b4bd3ea3ebb1386d4e1cce3a2b1917c8981 Mon Sep 17 00:00:00 2001 From: Jay Stothard Date: Sun, 12 Jul 2026 15:57:21 +0000 Subject: [PATCH] fix(gateway): validate multiplex adapter config by platform --- gateway/platforms/signal.py | 12 +++++++++-- gateway/run.py | 9 +++++++- plugins/platforms/homeassistant/adapter.py | 8 +++---- plugins/platforms/mattermost/adapter.py | 25 ++++++++++++++-------- tests/gateway/test_homeassistant.py | 16 +++++++++++++- tests/gateway/test_mattermost.py | 23 ++++++++++++++++++-- tests/gateway/test_signal.py | 24 ++++++++++++++++++++- 7 files changed, 97 insertions(+), 20 deletions(-) diff --git a/gateway/platforms/signal.py b/gateway/platforms/signal.py index a3f6916707a8..c72a0844d2eb 100644 --- a/gateway/platforms/signal.py +++ b/gateway/platforms/signal.py @@ -236,8 +236,16 @@ def _looks_like_e164_number(value: str) -> bool: def check_signal_requirements() -> bool: - """Check if Signal is configured (has URL and account).""" - return bool(os.getenv("SIGNAL_HTTP_URL") and os.getenv("SIGNAL_ACCOUNT")) + """Check if Signal runtime dependencies are available.""" + return True + + +def validate_signal_config(config: PlatformConfig) -> bool: + """Check if Signal has enough config to connect.""" + extra = getattr(config, "extra", {}) or {} + http_url = (extra.get("http_url", "") or os.getenv("SIGNAL_HTTP_URL", "")).strip() + account = (extra.get("account", "") or os.getenv("SIGNAL_ACCOUNT", "")).strip() + return bool(http_url and account) # --------------------------------------------------------------------------- diff --git a/gateway/run.py b/gateway/run.py index bf6a7d7b9245..2066f4dbbb9b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -9018,8 +9018,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew return WhatsAppCloudAdapter(config) elif platform == Platform.SIGNAL: - from gateway.platforms.signal import SignalAdapter, check_signal_requirements + from gateway.platforms.signal import ( + SignalAdapter, + check_signal_requirements, + validate_signal_config, + ) if not check_signal_requirements(): + logger.warning("Signal: runtime requirements not met") + return None + if not validate_signal_config(config): logger.warning("Signal: SIGNAL_HTTP_URL or SIGNAL_ACCOUNT not configured") return None return SignalAdapter(config) diff --git a/plugins/platforms/homeassistant/adapter.py b/plugins/platforms/homeassistant/adapter.py index 2168e3747c36..8e042133eee8 100644 --- a/plugins/platforms/homeassistant/adapter.py +++ b/plugins/platforms/homeassistant/adapter.py @@ -41,13 +41,13 @@ logger = logging.getLogger(__name__) def check_ha_requirements() -> bool: """Check if Home Assistant runtime dependencies are available.""" - return bool(AIOHTTP_AVAILABLE) + return AIOHTTP_AVAILABLE def validate_ha_config(config: PlatformConfig) -> bool: - """Accept either a scoped config token or an env-provided HASS_TOKEN.""" - token = getattr(config, "token", None) or os.getenv("HASS_TOKEN", "") - return bool(str(token or "").strip()) + """Return True when Home Assistant has enough credential config to connect.""" + token = (getattr(config, "token", None) or os.getenv("HASS_TOKEN", "")).strip() + return bool(token) class HomeAssistantAdapter(BasePlatformAdapter): diff --git a/plugins/platforms/mattermost/adapter.py b/plugins/platforms/mattermost/adapter.py index c5427af46f99..a6c10d5baac0 100644 --- a/plugins/platforms/mattermost/adapter.py +++ b/plugins/platforms/mattermost/adapter.py @@ -51,15 +51,7 @@ _RECONNECT_JITTER = 0.2 def check_mattermost_requirements() -> bool: - """Return True if the Mattermost adapter can be used.""" - token = os.getenv("MATTERMOST_TOKEN", "") - url = os.getenv("MATTERMOST_URL", "") - if not token: - logger.debug("Mattermost: MATTERMOST_TOKEN not set") - return False - if not url: - logger.warning("Mattermost: MATTERMOST_URL not set") - return False + """Return True if the Mattermost adapter runtime dependency is available.""" try: import aiohttp # noqa: F401 return True @@ -68,6 +60,20 @@ def check_mattermost_requirements() -> bool: return False +def validate_mattermost_config(config: PlatformConfig) -> bool: + """Return True when Mattermost has enough config to connect.""" + extra = getattr(config, "extra", {}) or {} + token = (getattr(config, "token", None) or os.getenv("MATTERMOST_TOKEN", "")).strip() + url = (extra.get("url", "") or os.getenv("MATTERMOST_URL", "")).strip() + if not token: + logger.debug("Mattermost: MATTERMOST_TOKEN not set") + return False + if not url: + logger.warning("Mattermost: MATTERMOST_URL not set") + return False + return True + + class MattermostAdapter(BasePlatformAdapter): """Gateway adapter for Mattermost (self-hosted or cloud).""" @@ -1248,6 +1254,7 @@ def register(ctx) -> None: label="Mattermost", adapter_factory=_build_adapter, check_fn=check_mattermost_requirements, + validate_config=validate_mattermost_config, is_connected=_is_connected, required_env=["MATTERMOST_URL", "MATTERMOST_TOKEN"], install_hint="pip install aiohttp", diff --git a/tests/gateway/test_homeassistant.py b/tests/gateway/test_homeassistant.py index 4c5d0cf09644..be91a3e33ac5 100644 --- a/tests/gateway/test_homeassistant.py +++ b/tests/gateway/test_homeassistant.py @@ -27,15 +27,29 @@ from plugins.platforms.homeassistant.adapter import ( class TestCheckRequirements: - def test_returns_true_when_aiohttp_present(self, monkeypatch): + def test_returns_true_without_token_when_aiohttp_available(self, monkeypatch): monkeypatch.delenv("HASS_TOKEN", raising=False) assert check_ha_requirements() is True + def test_returns_true_with_token(self, monkeypatch): + monkeypatch.setenv("HASS_TOKEN", "test-token") + assert check_ha_requirements() is True + @patch("plugins.platforms.homeassistant.adapter.AIOHTTP_AVAILABLE", False) def test_returns_false_without_aiohttp(self, monkeypatch): monkeypatch.setenv("HASS_TOKEN", "test-token") assert check_ha_requirements() is False + def test_validate_config_accepts_platform_token(self, monkeypatch): + monkeypatch.delenv("HASS_TOKEN", raising=False) + config = PlatformConfig(enabled=True, token="config-token") + assert validate_ha_config(config) is True + + def test_validate_config_rejects_missing_token(self, monkeypatch): + monkeypatch.delenv("HASS_TOKEN", raising=False) + config = PlatformConfig(enabled=True, token="") + assert validate_ha_config(config) is False + class TestValidateConfig: def test_returns_false_without_token_in_config_or_env(self, monkeypatch): diff --git a/tests/gateway/test_mattermost.py b/tests/gateway/test_mattermost.py index 808180ba144c..22fbea5fb59e 100644 --- a/tests/gateway/test_mattermost.py +++ b/tests/gateway/test_mattermost.py @@ -916,13 +916,32 @@ class TestMattermostRequirements: monkeypatch.delenv("MATTERMOST_TOKEN", raising=False) monkeypatch.delenv("MATTERMOST_URL", raising=False) from plugins.platforms.mattermost.adapter import check_mattermost_requirements - assert check_mattermost_requirements() is False + assert check_mattermost_requirements() is True def test_check_requirements_without_url(self, monkeypatch): monkeypatch.setenv("MATTERMOST_TOKEN", "test-token") monkeypatch.delenv("MATTERMOST_URL", raising=False) from plugins.platforms.mattermost.adapter import check_mattermost_requirements - assert check_mattermost_requirements() is False + assert check_mattermost_requirements() is True + + def test_validate_config_accepts_platform_values(self, monkeypatch): + monkeypatch.delenv("MATTERMOST_TOKEN", raising=False) + monkeypatch.delenv("MATTERMOST_URL", raising=False) + from plugins.platforms.mattermost.adapter import validate_mattermost_config + + config = PlatformConfig( + enabled=True, + token="cfg-token", + extra={"url": "https://mm.example.com"}, + ) + assert validate_mattermost_config(config) is True + + def test_validate_config_rejects_missing_url(self, monkeypatch): + monkeypatch.delenv("MATTERMOST_URL", raising=False) + from plugins.platforms.mattermost.adapter import validate_mattermost_config + + config = PlatformConfig(enabled=True, token="cfg-token", extra={}) + assert validate_mattermost_config(config) is False # --------------------------------------------------------------------------- diff --git a/tests/gateway/test_signal.py b/tests/gateway/test_signal.py index 1be595050369..6677cd4b4581 100644 --- a/tests/gateway/test_signal.py +++ b/tests/gateway/test_signal.py @@ -302,7 +302,29 @@ class TestSignalHelpers: from gateway.platforms.signal import check_signal_requirements monkeypatch.delenv("SIGNAL_HTTP_URL", raising=False) monkeypatch.delenv("SIGNAL_ACCOUNT", raising=False) - assert check_signal_requirements() is False + assert check_signal_requirements() is True + + def test_validate_signal_config_accepts_platform_values(self, monkeypatch): + monkeypatch.delenv("SIGNAL_HTTP_URL", raising=False) + monkeypatch.delenv("SIGNAL_ACCOUNT", raising=False) + from gateway.platforms.signal import validate_signal_config + + config = PlatformConfig( + enabled=True, + extra={ + "http_url": "http://localhost:8080", + "account": "+155****4567", + }, + ) + assert validate_signal_config(config) is True + + def test_validate_signal_config_rejects_missing_account(self, monkeypatch): + monkeypatch.delenv("SIGNAL_HTTP_URL", raising=False) + monkeypatch.delenv("SIGNAL_ACCOUNT", raising=False) + from gateway.platforms.signal import validate_signal_config + + config = PlatformConfig(enabled=True, extra={"http_url": "http://localhost:8080"}) + assert validate_signal_config(config) is False # ---------------------------------------------------------------------------