mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Merge branch 'feature/updater' into feature/store-i18n
16 conflicts, resolved per hunk: - sys-stk-front-proxy: tasks/main.yml keeps the network-sibling loop; the single stack_host_contents write of http.conf plus vhost and the front_proxy_vhost_changed gate move into tasks/vhost.yml, with the http.conf decided per sibling domain so an onion vhost gets no HTTPS redirect. - sys-front-tls: 01_http.yml is gone as the updater intends; the cert-renew include in main.yml gates on the vhost domain's TLS. - sys-svc-compose: the updater's resolve_host_cid without the remote echo and the prepared pending flag in up.yml, written with IS_SWARM_MODE and the repeated-when block grouping. - web-app-nextcloud: the updater's post-upgrade gate and pending-only plugin configuration, which already carry the pinned-plugin handling. - web-app-xwiki: one XWIKI_CORE_VERSION for the readiness probe and style.css.j2; OIDC/LDAP follow xwiki_extensions_ready at bootstrap, the S3 blob store stays off there as TestBlobstoreRenderOrder demands. - roles_with_service: onion swap and dashboard card key combined, the canonical domain is the canonical URL's host in both branches. - sys-ctl-hlth-csp: --clearnet-timeout and --concurrency together; run_checker forwards its options to build_docker_cmd, keeping the script under the 500-line limit. - web-app-matrix: Element branding and web_client_location side by side. - web-app-stalwart/README.md: the generated Cosmos section is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
99123762df
72 files changed
+2844
-345
No files matched your search
@@ -0,0 +1,324 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from pathlib import Path
|
||||
|
||||
from baudolo.databases import DatabasesCsvError, validate_database
|
||||
from baudolo.seed.__main__ import check_and_add_entry
|
||||
|
||||
from plugins.filter.baudolo_seed import (
|
||||
FilterModule,
|
||||
_database,
|
||||
baudolo_seed_apply,
|
||||
baudolo_seed_current,
|
||||
baudolo_seeded,
|
||||
)
|
||||
|
||||
HEADER = "instance;database;username;password\n"
|
||||
|
||||
|
||||
def _b64(text: str) -> str:
|
||||
return base64.b64encode(text.encode("utf-8")).decode("ascii")
|
||||
|
||||
|
||||
def _seeded(text: str) -> dict:
|
||||
return baudolo_seeded(_b64(text))
|
||||
|
||||
|
||||
STABLE = [
|
||||
("central-postgres", "nextcloud", "nextcloud", "pw-1"),
|
||||
("central-mariadb", "wordpress", "wordpress", "pa;ss"),
|
||||
("central-postgres", "", "postgres", 'quo"te'),
|
||||
("central-mariadb", "matomo", "matomo", "multi\nline"),
|
||||
("central-postgres", " keycloak ", "keycloak", " spaced "),
|
||||
("central-postgres", "gitea", "gitea", ""),
|
||||
("central-postgres", "listmonk", "listmonk", "ünïcode"),
|
||||
]
|
||||
ENTRIES = [
|
||||
*STABLE[:5],
|
||||
("central-postgres", "nextcloud", "nextcloud", "pw-2"),
|
||||
("central-mariadb", "wordpress", "wordpress", "pa;ss"),
|
||||
*STABLE[5:],
|
||||
]
|
||||
|
||||
|
||||
def _argv(entry) -> tuple[str, str, str, str]:
|
||||
instance, name, username, password = entry
|
||||
return instance, name or "*", username, password
|
||||
|
||||
|
||||
def _bytes(path: Path) -> bytes:
|
||||
if not path.exists():
|
||||
return b""
|
||||
return path.read_bytes() # nocheck: cache-read baudolo-seed rewrites the tempfile
|
||||
|
||||
|
||||
def _b64_of(path: Path) -> str:
|
||||
return base64.b64encode(_bytes(path)).decode("ascii")
|
||||
|
||||
|
||||
def _seed(path: Path, argv) -> None:
|
||||
with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
|
||||
check_and_add_entry(str(path), *argv)
|
||||
|
||||
|
||||
class TestBaudoloSeedEquivalence(unittest.TestCase):
|
||||
def _per_entry(self, path: Path, entries) -> int:
|
||||
for entry in entries:
|
||||
_seed(path, _argv(entry))
|
||||
return len(entries)
|
||||
|
||||
def _skipping(self, path: Path, entries) -> int:
|
||||
state = baudolo_seeded(_b64_of(path))
|
||||
calls = 0
|
||||
for entry in entries:
|
||||
argv = _argv(entry)
|
||||
current = baudolo_seed_current(state, *argv)
|
||||
state = baudolo_seed_apply(state, *argv)
|
||||
if not current:
|
||||
_seed(path, argv)
|
||||
calls += 1
|
||||
return calls
|
||||
|
||||
def _run_both(self, start: bytes | None, entries):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
old = Path(tmp) / "old.csv"
|
||||
new = Path(tmp) / "new.csv"
|
||||
if start is not None:
|
||||
old.write_bytes(start)
|
||||
new.write_bytes(start)
|
||||
old_calls = self._per_entry(old, entries)
|
||||
new_calls = self._skipping(new, entries)
|
||||
return _bytes(old), _bytes(new), old_calls, new_calls
|
||||
|
||||
def test_fresh_file_matches_and_skips_only_repeats(self):
|
||||
old, new, old_calls, new_calls = self._run_both(None, ENTRIES)
|
||||
self.assertEqual(old, new)
|
||||
self.assertEqual(old_calls, len(ENTRIES))
|
||||
self.assertEqual(new_calls, len(ENTRIES) - 1)
|
||||
|
||||
def test_empty_file_from_touch_matches(self):
|
||||
old, new, _, _ = self._run_both(b"", ENTRIES)
|
||||
self.assertEqual(old, new)
|
||||
|
||||
def _converged(self, entries) -> bytes:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
first = Path(tmp) / "first.csv"
|
||||
self._per_entry(first, entries)
|
||||
return _bytes(first)
|
||||
|
||||
def test_converged_file_needs_no_call_and_keeps_its_bytes(self):
|
||||
converged = self._converged(STABLE)
|
||||
old, new, _, new_calls = self._run_both(converged, STABLE)
|
||||
self.assertEqual(old, new)
|
||||
self.assertEqual(new, converged)
|
||||
self.assertEqual(new_calls, 0)
|
||||
|
||||
def test_entries_that_overwrite_each_other_are_seeded_every_time(self):
|
||||
converged = self._converged(ENTRIES)
|
||||
old, new, _, new_calls = self._run_both(converged, ENTRIES)
|
||||
self.assertEqual(old, new)
|
||||
self.assertEqual(new_calls, 2)
|
||||
|
||||
def test_rotated_password_is_seeded_again(self):
|
||||
converged = self._converged(STABLE)
|
||||
rotated = [*STABLE[:-1], ("central-postgres", "listmonk", "listmonk", "rot")]
|
||||
old, new, _, new_calls = self._run_both(converged, rotated)
|
||||
self.assertEqual(old, new)
|
||||
self.assertEqual(new_calls, 1)
|
||||
|
||||
def test_new_consumer_is_appended_after_the_converged_rows(self):
|
||||
converged = self._converged(STABLE)
|
||||
grown = [*STABLE, ("central-mariadb", "pixelfed", "pixelfed", "x")]
|
||||
old, new, _, new_calls = self._run_both(converged, grown)
|
||||
self.assertEqual(old, new)
|
||||
self.assertEqual(new_calls, 1)
|
||||
|
||||
def test_invalid_database_name_still_reaches_baudolo(self):
|
||||
entry = ("central-postgres", "bad name", "u", "p")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "databases.csv"
|
||||
self._per_entry(path, STABLE)
|
||||
with self.assertRaises(DatabasesCsvError):
|
||||
self._skipping(path, [entry])
|
||||
|
||||
def test_hand_edited_file_falls_back_to_one_call_per_entry(self):
|
||||
hand = (HEADER + "central-postgres;nextcloud;nextcloud;pw-2\r\n").encode()
|
||||
old, new, old_calls, new_calls = self._run_both(hand, ENTRIES)
|
||||
self.assertEqual(old, new)
|
||||
self.assertEqual(new_calls, old_calls)
|
||||
|
||||
def test_short_row_falls_back_to_one_call_per_entry(self):
|
||||
short = (HEADER + "central-postgres;nextcloud\n").encode()
|
||||
old, new, old_calls, new_calls = self._run_both(short, ENTRIES)
|
||||
self.assertEqual(old, new)
|
||||
self.assertEqual(new_calls, old_calls)
|
||||
|
||||
def test_negative_control_a_state_older_than_the_file_breaks_equivalence(self):
|
||||
converged = self._converged(STABLE)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
stale = Path(tmp) / "stale.csv"
|
||||
stale.write_bytes(b"")
|
||||
state = baudolo_seeded(base64.b64encode(converged).decode("ascii"))
|
||||
for entry in STABLE:
|
||||
if not baudolo_seed_current(state, *_argv(entry)):
|
||||
_seed(stale, _argv(entry))
|
||||
self.assertNotEqual(_bytes(stale), converged)
|
||||
|
||||
|
||||
class TestBaudoloSeeded(unittest.TestCase):
|
||||
def test_absent_or_empty_file_is_canonical_without_rows(self):
|
||||
self.assertEqual(baudolo_seeded(""), {"canonical": True, "rows": []})
|
||||
|
||||
def test_header_only_is_canonical_without_rows(self):
|
||||
self.assertEqual(_seeded(HEADER), {"canonical": True, "rows": []})
|
||||
|
||||
def test_rows_are_read_in_file_order(self):
|
||||
text = HEADER + "a;db1;u1;p1\nb;*;u2;p2\n"
|
||||
self.assertEqual(
|
||||
_seeded(text),
|
||||
{
|
||||
"canonical": True,
|
||||
"rows": [["a", "db1", "u1", "p1"], ["b", "*", "u2", "p2"]],
|
||||
},
|
||||
)
|
||||
|
||||
def test_quoted_fields_stay_canonical(self):
|
||||
text = HEADER + 'a;db;u;"pa;ss"\nb;db;u;"q""t"\n'
|
||||
state = _seeded(text)
|
||||
self.assertTrue(state["canonical"])
|
||||
self.assertEqual(state["rows"][0][3], "pa;ss")
|
||||
self.assertEqual(state["rows"][1][3], 'q"t')
|
||||
|
||||
def test_crlf_is_not_canonical(self):
|
||||
self.assertFalse(_seeded(HEADER.replace("\n", "\r\n"))["canonical"])
|
||||
|
||||
def test_missing_trailing_newline_is_not_canonical(self):
|
||||
self.assertFalse(_seeded(HEADER + "a;db;u;p")["canonical"])
|
||||
|
||||
def test_blank_line_is_not_canonical(self):
|
||||
self.assertFalse(_seeded(HEADER + "\na;db;u;p\n")["canonical"])
|
||||
|
||||
def test_needless_quotes_are_not_canonical(self):
|
||||
self.assertFalse(_seeded(HEADER + 'a;db;u;"p"\n')["canonical"])
|
||||
|
||||
def test_foreign_header_is_not_canonical(self):
|
||||
self.assertFalse(_seeded("instance;database;user;password\n")["canonical"])
|
||||
|
||||
def test_undecodable_content_is_not_canonical(self):
|
||||
self.assertFalse(baudolo_seeded("not base64!")["canonical"])
|
||||
self.assertFalse(
|
||||
baudolo_seeded(base64.b64encode(b"\xff\xfe").decode())["canonical"]
|
||||
)
|
||||
|
||||
|
||||
class TestBaudoloSeedCurrent(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.state = _seeded(HEADER + "a;db;u;p\na;*;su;sp\nb;db;u;p\nb;db;u;q\n")
|
||||
|
||||
def test_same_credentials_are_current(self):
|
||||
self.assertTrue(baudolo_seed_current(self.state, "a", "db", "u", "p"))
|
||||
|
||||
def test_cluster_row_is_current(self):
|
||||
self.assertTrue(baudolo_seed_current(self.state, "a", "*", "su", "sp"))
|
||||
|
||||
def test_padded_database_name_matches_the_stripped_row(self):
|
||||
self.assertTrue(baudolo_seed_current(self.state, "a", " db ", "u", "p"))
|
||||
|
||||
def test_negative_control_other_password_is_not_current(self):
|
||||
self.assertFalse(baudolo_seed_current(self.state, "a", "db", "u", "p2"))
|
||||
|
||||
def test_negative_control_other_username_is_not_current(self):
|
||||
self.assertFalse(baudolo_seed_current(self.state, "a", "db", "u2", "p"))
|
||||
|
||||
def test_missing_row_is_not_current(self):
|
||||
self.assertFalse(baudolo_seed_current(self.state, "c", "db", "u", "p"))
|
||||
|
||||
def test_duplicate_rows_with_other_credentials_are_not_current(self):
|
||||
self.assertFalse(baudolo_seed_current(self.state, "b", "db", "u", "p"))
|
||||
|
||||
def test_invalid_database_name_is_never_current(self):
|
||||
state = _seeded(HEADER + "a;bad name;u;p\n")
|
||||
self.assertFalse(baudolo_seed_current(state, "a", "bad name", "u", "p"))
|
||||
|
||||
def test_non_canonical_state_is_never_current(self):
|
||||
state = _seeded(HEADER + 'a;db;u;"p"\n')
|
||||
self.assertFalse(baudolo_seed_current(state, "a", "db", "u", "p"))
|
||||
|
||||
|
||||
class TestBaudoloSeedApply(unittest.TestCase):
|
||||
def test_new_definition_is_appended(self):
|
||||
state = baudolo_seed_apply(_seeded(HEADER + "a;db;u;p\n"), "b", "*", "u", "p")
|
||||
self.assertEqual(state["rows"], [["a", "db", "u", "p"], ["b", "*", "u", "p"]])
|
||||
|
||||
def test_every_matching_row_takes_the_credentials(self):
|
||||
state = baudolo_seed_apply(
|
||||
_seeded(HEADER + "a;db;u;p\nx;db;u;p\na;db;v;q\n"), "a", "db", "n", "m"
|
||||
)
|
||||
self.assertEqual(
|
||||
state["rows"],
|
||||
[["a", "db", "n", "m"], ["x", "db", "u", "p"], ["a", "db", "n", "m"]],
|
||||
)
|
||||
|
||||
def test_database_name_is_stored_stripped(self):
|
||||
state = baudolo_seed_apply(baudolo_seeded(""), "a", " db ", "u", "p")
|
||||
self.assertEqual(state["rows"], [["a", "db", "u", "p"]])
|
||||
|
||||
def test_input_state_is_not_mutated(self):
|
||||
state = _seeded(HEADER + "a;db;u;p\n")
|
||||
baudolo_seed_apply(state, "a", "db", "u", "q")
|
||||
self.assertEqual(state["rows"], [["a", "db", "u", "p"]])
|
||||
|
||||
def test_non_canonical_state_stays_non_canonical(self):
|
||||
state = baudolo_seed_apply(
|
||||
_seeded(HEADER + 'a;db;u;"p"\n'), "a", "db", "u", "p"
|
||||
)
|
||||
self.assertEqual(state, {"canonical": False, "rows": []})
|
||||
|
||||
def test_invalid_database_name_makes_the_state_non_canonical(self):
|
||||
state = baudolo_seed_apply(baudolo_seeded(""), "a", "nan", "u", "p")
|
||||
self.assertFalse(state["canonical"])
|
||||
|
||||
|
||||
class TestDatabaseNameParity(unittest.TestCase):
|
||||
def test_validation_matches_baudolo(self):
|
||||
for value in (
|
||||
"db",
|
||||
" db ",
|
||||
"*",
|
||||
" * ",
|
||||
"",
|
||||
" ",
|
||||
"nan",
|
||||
"NaN",
|
||||
"a-b_c",
|
||||
"-a",
|
||||
"a b",
|
||||
"ü",
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
try:
|
||||
expected = validate_database(value, instance="i")
|
||||
except DatabasesCsvError:
|
||||
expected = None
|
||||
self.assertEqual(_database(value), expected)
|
||||
|
||||
|
||||
class TestFilterModule(unittest.TestCase):
|
||||
def test_filters_are_registered(self):
|
||||
self.assertEqual(
|
||||
FilterModule().filters(),
|
||||
{
|
||||
"baudolo_seeded": baudolo_seeded,
|
||||
"baudolo_seed_current": baudolo_seed_current,
|
||||
"baudolo_seed_apply": baudolo_seed_apply,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -125,6 +125,26 @@ class DummyTlsResolveLookup:
|
||||
return [base_url]
|
||||
|
||||
|
||||
class DummyCanonicalUrlLookup:
|
||||
"""
|
||||
Deterministic canonical_url stub.
|
||||
Emulates: lookup('canonical_url', app_id, key) -> 'http(s)://<domains[app_id][key]>'
|
||||
"""
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
variables = variables or {}
|
||||
app_id, key = terms
|
||||
app_domains = variables.get("domains", {}).get(app_id)
|
||||
if not isinstance(app_domains, dict) or key not in app_domains:
|
||||
raise AnsibleError(
|
||||
f"dummy canonical_url: '{app_id}' has no canonical '{key}'"
|
||||
)
|
||||
scheme = (
|
||||
"https" if _ansible_bool(variables.get("TLS_ENABLED", True)) else "http"
|
||||
)
|
||||
return [f"{scheme}://{app_domains[key]}"]
|
||||
|
||||
|
||||
class TestDockerCardsLookup(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.test_roles_dir = tempfile.mkdtemp(prefix="test_roles_")
|
||||
@@ -174,6 +194,8 @@ logo:
|
||||
return _VarsLookup("domains")
|
||||
if name == "tls":
|
||||
return DummyTlsResolveLookup(templar)
|
||||
if name == "canonical_url":
|
||||
return DummyCanonicalUrlLookup()
|
||||
raise AnsibleError(f"Unexpected lookup requested: {name}")
|
||||
|
||||
docker_cards_module.lookup_loader.get = _patched_get
|
||||
@@ -304,6 +326,39 @@ logo:
|
||||
|
||||
self.assertEqual(result[0][0]["url"], "http://myportfolio.com")
|
||||
|
||||
def _card_target_variables(self, dashboard):
|
||||
fake_variables = self._base_fake_variables()
|
||||
fake_variables["domains"] = {
|
||||
"portfolio": {
|
||||
"www": "myportfolio.com",
|
||||
"client": "client.myportfolio.com",
|
||||
}
|
||||
}
|
||||
fake_variables["applications"]["portfolio"]["services"]["dashboard"] = dashboard
|
||||
return fake_variables
|
||||
|
||||
def test_card_url_targets_the_card_key(self):
|
||||
fake_variables = self._card_target_variables(
|
||||
{"enabled": True, "card": "client"}
|
||||
)
|
||||
|
||||
result = self._run_lookup(LookupModule(), fake_variables)
|
||||
|
||||
self.assertEqual(result[0][0]["url"], "https://client.myportfolio.com")
|
||||
|
||||
def test_card_url_without_card_key_targets_the_primary_domain(self):
|
||||
fake_variables = self._card_target_variables({"enabled": True})
|
||||
|
||||
result = self._run_lookup(LookupModule(), fake_variables)
|
||||
|
||||
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
|
||||
|
||||
def test_unknown_card_key_fails_loudly(self):
|
||||
fake_variables = self._card_target_variables({"enabled": True, "card": "nope"})
|
||||
|
||||
with self.assertRaisesRegex(AnsibleError, "nope"):
|
||||
self._run_lookup(LookupModule(), fake_variables)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -72,13 +72,15 @@ class RolesWithServiceLookupTests(unittest.TestCase):
|
||||
handover: frozenset = frozenset(),
|
||||
**kwargs,
|
||||
):
|
||||
"""Run the lookup with the ``applications``, ``tls`` and
|
||||
``network_handover`` sub-lookups patched to stay hermetic. The stubbed
|
||||
tls answers ``[role_id, "url.base"]`` with the role's own configured
|
||||
domain, which is what a clearnet deployment resolves; pass ``url_host``
|
||||
(every role) or ``url_hosts`` (per role) to model a deployment whose URL
|
||||
host differs, such as an onion one. A role in ``handover`` hands every
|
||||
sibling outside its canonical network over."""
|
||||
"""Run the lookup with the ``applications``, ``tls``,
|
||||
``canonical_url`` and ``network_handover`` sub-lookups patched to stay
|
||||
hermetic; any other sub-lookup raises ``KeyError``. The stubbed tls
|
||||
answers ``[role_id, "url.base"]`` and the stubbed canonical_url
|
||||
``[role_id, key]`` with the role's own configured domain, which is what
|
||||
a clearnet deployment resolves; pass ``url_host`` (every role) or
|
||||
``url_hosts`` (per role) to model a deployment whose URL host differs,
|
||||
such as an onion one. A role in ``handover`` hands every sibling outside
|
||||
its canonical network over."""
|
||||
lookup = self._make_lookup(vars_ or {})
|
||||
resolve = self.mod._resolve_canonical_domain
|
||||
|
||||
@@ -100,16 +102,31 @@ class RolesWithServiceLookupTests(unittest.TestCase):
|
||||
role in handover and network_of(domain) != network_of(canonical)
|
||||
]
|
||||
|
||||
named = self.mod._resolve_named_canonical_domain
|
||||
|
||||
class _StubCanonicalUrl:
|
||||
def run(self, terms_, variables=None, **kwargs):
|
||||
role, key = str(terms_[0]), str(terms_[1])
|
||||
host = (
|
||||
(url_hosts or {}).get(role)
|
||||
or url_host
|
||||
or named(role, applications.get(role, {}), key)
|
||||
)
|
||||
return [f"https://{host}"]
|
||||
|
||||
class _StubApplications:
|
||||
def run(self, terms_, variables=None, **kwargs):
|
||||
return [applications]
|
||||
|
||||
stubs = {
|
||||
"applications": _StubApplications(),
|
||||
"tls": _StubTls(),
|
||||
"canonical_url": _StubCanonicalUrl(),
|
||||
"network_handover": _StubHandover(),
|
||||
}
|
||||
|
||||
def _dispatch(name, **_kwargs):
|
||||
if name == "applications":
|
||||
return _StubApplications()
|
||||
if name == "network_handover":
|
||||
return _StubHandover()
|
||||
return _StubTls()
|
||||
return stubs[name]
|
||||
|
||||
with patch.object(self.mod.lookup_loader, "get", side_effect=_dispatch):
|
||||
return lookup.run(terms, variables=vars_ or {}, **kwargs)
|
||||
@@ -683,6 +700,60 @@ class RolesWithServiceLookupTests(unittest.TestCase):
|
||||
with self.assertRaises(AnsibleError):
|
||||
self._network_ids({}, network="onion")
|
||||
|
||||
def _card_target_role(self, service: str, **block_extra) -> dict:
|
||||
block = {"enabled": True, "shared": True}
|
||||
block.update(block_extra)
|
||||
return {
|
||||
"web-app-chat": {
|
||||
"services": {service: block},
|
||||
"domains": {
|
||||
"canonical": {
|
||||
"synapse": "matrix.example.com",
|
||||
"element": "element.example.com",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def test_dashboard_card_key_targets_that_domain(self):
|
||||
applications = self._card_target_role("dashboard", card="element")
|
||||
entry = self._run(["dashboard"], applications)[0][0]
|
||||
self.assertEqual(
|
||||
(entry["canonical_domain"], entry["canonical_url"]),
|
||||
("element.example.com", "https://element.example.com"),
|
||||
)
|
||||
|
||||
def test_dashboard_card_domain_follows_the_onion_url(self):
|
||||
applications = self._card_target_role("dashboard", card="element")
|
||||
entry = self._run(
|
||||
["dashboard"], applications, url_hosts={"web-app-chat": f"element.{ONION}"}
|
||||
)[0][0]
|
||||
self.assertEqual(
|
||||
(entry["canonical_domain"], entry["canonical_url"]),
|
||||
(f"element.{ONION}", f"https://element.{ONION}"),
|
||||
)
|
||||
|
||||
def test_dashboard_without_card_key_targets_the_primary_domain(self):
|
||||
applications = self._card_target_role("dashboard")
|
||||
entry = self._run(["dashboard"], applications)[0][0]
|
||||
self.assertEqual(
|
||||
(entry["canonical_domain"], entry["canonical_url"]),
|
||||
("matrix.example.com", "https://matrix.example.com"),
|
||||
)
|
||||
|
||||
def test_dashboard_unknown_card_key_fails_loudly(self):
|
||||
applications = self._card_target_role("dashboard", card="nope")
|
||||
with self.assertRaisesRegex(AnsibleError, "nope"):
|
||||
self._run(["dashboard"], applications)
|
||||
|
||||
def test_other_services_ignore_a_card_key(self):
|
||||
applications = self._card_target_role("prometheus", card="element")
|
||||
entry = self._run(["prometheus"], applications)[0][0]
|
||||
self.assertEqual(
|
||||
(entry["canonical_domain"], entry["canonical_url"]),
|
||||
("matrix.example.com", "https://matrix.example.com"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user