Files
computer-playbook/tests/unit/python/roles/web-app-matrix/test_telegram_gate.py
T
kevinveenbirkenbachandClaude Opus 5 d81af8184d fix(web-app-matrix): gate the telegram bridge in both flavors, not only in compose
mautrix-telegram cannot register without an API id and hash, and the task that dropped it when they were missing ran only in the compose flavor. The ansible flavor renders its upstream flags from MATRIX_BRIDGE_ADDONS, which carried telegram regardless, so MDAD was told to enable a bridge it could not authenticate.

The gate moves onto that variable: MATRIX_BRIDGE_ADDONS_DECLARED keeps the unfiltered selection and MATRIX_BRIDGE_ADDONS drops telegram unless MATRIX_TELEGRAM_USABLE finds both credentials non-blank. The compose bridge list built addons itself rather than reading that variable, so removing the task alone would have taken the filter off the list that drives the docker loops, the homeserver registration and the compose services; it now derives from the gated addons, and a unit test fails if it stops doing so.

MATRIX_BRIDGES deliberately stays a compose set_fact. As a role variable it would also be defined in the ansible flavor, where the playwright env would start emitting MAUTRIX_<BRIDGE>_ADDON_ENABLED and run addon specs that flavor has always skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 20:50:03 +02:00

103 lines
3.5 KiB
Python

"""The telegram bridge gate of web-app-matrix.
mautrix-telegram needs an API id and hash to register, so a deployment
without them must not be handed the bridge. The gate used to be a compose
task filtering the bridge list, which left the ansible flavor configuring a
bridge it could not authenticate; it now sits on ``MATRIX_BRIDGE_ADDONS``,
which both flavors read. The compose list has to stay derived from that
variable, or deleting the task takes the filter with it.
"""
from __future__ import annotations
import unittest
from pathlib import Path
from typing import ClassVar
from jinja2 import Environment, StrictUndefined
from utils import PROJECT_ROOT
from utils.cache.yaml import load_yaml, load_yaml_any
from utils.roles.mapping import ROLE_FILE_VARS_MAIN
ROLE = "web-app-matrix"
ROLE_DIR = Path(PROJECT_ROOT) / "roles" / ROLE
VARS = ROLE_DIR / ROLE_FILE_VARS_MAIN
COMPOSE_MAIN = ROLE_DIR / "tasks" / "flavor" / "compose" / "main.yml"
DECLARED = [
{"value": {"enabled": True, "config": {"bridge_name": "telegram"}}},
{"value": {"enabled": True, "config": {"bridge_name": "signal"}}},
]
def _ansible_bool(value) -> bool:
if isinstance(value, str):
return value.strip().lower() in {"y", "yes", "true", "on", "1"}
return bool(value)
class TestTelegramGate(unittest.TestCase):
variables: ClassVar[dict] = load_yaml(str(VARS))
def _render(self, key: str, **context) -> str:
"""Render one role variable.
Args:
key: the variable name in ``vars/main.yml``.
context: values the expression reads, plus ``credentials`` for
what ``lookup('config', ...)`` answers.
"""
credentials = context.pop("credentials", "")
env = Environment(undefined=StrictUndefined, autoescape=False) # noqa: S701 - renders an ansible var expression, not markup
env.filters["bool"] = _ansible_bool
env.globals["lookup"] = lambda *_a, **_kw: credentials
return env.from_string(self.variables[key]).render(
application_id=ROLE, **context
)
def test_both_credentials_make_telegram_usable(self) -> None:
rendered = self._render("MATRIX_TELEGRAM_USABLE", credentials="set")
self.assertTrue(_ansible_bool(rendered))
def test_a_blank_credential_makes_telegram_unusable(self) -> None:
for blank in ("", " "):
with self.subTest(blank=repr(blank)):
rendered = self._render("MATRIX_TELEGRAM_USABLE", credentials=blank)
self.assertFalse(_ansible_bool(rendered))
def test_an_unusable_telegram_is_dropped_from_the_addons(self) -> None:
rendered = self._render(
"MATRIX_BRIDGE_ADDONS",
MATRIX_BRIDGE_ADDONS_DECLARED=DECLARED,
MATRIX_TELEGRAM_USABLE=False,
)
self.assertNotIn("telegram", rendered)
self.assertIn("signal", rendered)
def test_a_usable_telegram_stays_in_the_addons(self) -> None:
rendered = self._render(
"MATRIX_BRIDGE_ADDONS",
MATRIX_BRIDGE_ADDONS_DECLARED=DECLARED,
MATRIX_TELEGRAM_USABLE=True,
)
self.assertIn("telegram", rendered)
self.assertIn("signal", rendered)
def test_the_compose_list_is_derived_from_the_gated_addons(self) -> None:
build = load_yaml_any(str(COMPOSE_MAIN))[0]["set_fact"]["MATRIX_BRIDGES"]
self.assertIn(
"MATRIX_BRIDGE_ADDONS",
build,
"a compose list read straight from addons bypasses the gate",
)
if __name__ == "__main__":
unittest.main()