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:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-25 23:47:07 +01:00
commit 99123762df
72 files changed
+2844 -345

No files matched your search

@@ -0,0 +1,46 @@
"""Unit tests for ``utils/roles/applications/dashboard.py``."""
from __future__ import annotations
import unittest
from ansible.errors import AnsibleError
from utils.roles.applications.dashboard import card_target_key
class TestCardTargetKey(unittest.TestCase):
def test_a_binding_without_the_key_targets_the_primary_domain(self):
self.assertEqual(
"", card_target_key("web-app-foo", {"enabled": True, "shared": True})
)
def test_a_missing_binding_targets_the_primary_domain(self):
self.assertEqual("", card_target_key("web-app-foo", None))
def test_the_named_key_is_returned(self):
self.assertEqual(
"element",
card_target_key("web-app-matrix", {"card": "element"}),
)
def test_surrounding_whitespace_is_dropped(self):
self.assertEqual(
"webmail",
card_target_key("web-app-stalwart", {"card": " webmail "}),
)
def test_an_alias_canonical_key_is_not_a_card_target(self):
self.assertEqual("", card_target_key("web-svc-cdn", {"canonical": "cdn"}))
def test_an_empty_key_fails_loudly(self):
with self.assertRaisesRegex(AnsibleError, "web-app-foo"):
card_target_key("web-app-foo", {"card": " "})
def test_a_non_string_key_fails_loudly(self):
with self.assertRaises(AnsibleError):
card_target_key("web-app-foo", {"card": ["element"]})
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,73 @@
from __future__ import annotations
import contextlib
import io
import json
import tempfile
import unittest
from pathlib import Path
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.testing import patch_module_args
from utils.directory_batch import ensure_all
def _entry(path, mode="0755"):
return {"path": str(path), "mode": mode, "owner": None, "group": None}
class EnsureAllTests(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name)
def tearDown(self):
self._tmp.cleanup()
def _ensure(self, entries, *, check_mode=False):
with patch_module_args({"_ansible_check_mode": check_mode}):
module = AnsibleModule(argument_spec={}, supports_check_mode=True)
return ensure_all(module, entries)
def test_returns_the_changed_paths_in_order_with_one_diff_each(self):
first = self.root / "a"
second = self.root / "b"
changed_paths, diffs = self._ensure([_entry(first), _entry(second)])
self.assertEqual(changed_paths, [str(first), str(second)])
self.assertEqual(
[diff["after"]["path"] for diff in diffs], [str(first), str(second)]
)
def test_unchanged_paths_are_left_out(self):
existing = self.root / "kept"
existing.mkdir()
existing.chmod(0o755)
self.assertEqual(self._ensure([_entry(existing)]), ([], []))
def test_empty_entries_change_nothing(self):
self.assertEqual(self._ensure([]), ([], []))
def test_check_mode_reports_without_creating(self):
target = self.root / "new"
changed_paths, _ = self._ensure([_entry(target)], check_mode=True)
self.assertEqual(changed_paths, [str(target)])
self.assertFalse(target.exists())
def test_first_error_fails_with_the_paths_changed_before_it(self):
created = self.root / "created"
blocker = self.root / "blocker"
never = self.root / "never"
blocker.write_text("", encoding="utf-8")
buffer = io.StringIO()
with contextlib.redirect_stdout(buffer), self.assertRaises(SystemExit):
self._ensure([_entry(created), _entry(blocker), _entry(never)])
result = json.loads(buffer.getvalue())
self.assertTrue(result["failed"])
self.assertEqual(result["path"], str(blocker))
self.assertEqual(result["changed_paths"], [str(created)])
self.assertFalse(never.exists())
if __name__ == "__main__":
unittest.main()