diff --git a/bridge/bridge.py b/bridge/bridge.py index 1813f6b..4565b4e 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -64,6 +64,8 @@ def parse_trigger(body): if s == f"{TRIGGER} --quick": return True, True return False, False + + ALLOWLIST = {u.strip() for u in os.environ.get("AUTH_ALLOWLIST", "").split(",") if u.strip()} @@ -167,8 +169,12 @@ def post_commit_status(owner, repo, sha, state, target_url, description=""): f"{GITEA_API}/repos/{owner}/{repo}/statuses/{sha}", GITEA_TOKEN, method="POST", - data={"state": state, "target_url": target_url, - "description": description, "context": "cc-ci/testme"}, + data={ + "state": state, + "target_url": target_url, + "description": description, + "context": "cc-ci/testme", + }, ) @@ -217,7 +223,9 @@ def result_comment_body(recipe, sha, num, run_url, status): if artifact_available(badge_url): body += f"\n\n[![level]({badge_url})]({run_url})" return f"{body}\n\n{links}" - return f"{header} → {run_url}\n\n_(summary card unavailable — see the run for details.)_ {links}" + return ( + f"{header} → {run_url}\n\n_(summary card unavailable — see the run for details.)_ {links}" + ) def watch_and_reflect(owner, name, number, num, recipe, sha, comment_id, run_url): diff --git a/dashboard/dashboard.py b/dashboard/dashboard.py index 831f254..48de3d3 100644 --- a/dashboard/dashboard.py +++ b/dashboard/dashboard.py @@ -66,8 +66,13 @@ _COLORS = { # Level → colour ramp, kept in sync with runner/harness/card.py LEVEL_COLOR (the dashboard is a # standalone stdlib service that doesn't import the runner harness, so the small map is duplicated). _LEVEL_COLOR = { - 0: "#e5534b", 1: "#e0823d", 2: "#e0823d", 3: "#d9b343", - 4: "#a0b93f", 5: "#57ab5a", 6: "#3fb950", + 0: "#e5534b", + 1: "#e0823d", + 2: "#e0823d", + 3: "#d9b343", + 4: "#a0b93f", + 5: "#57ab5a", + 6: "#3fb950", } @@ -269,7 +274,11 @@ def _card(r): f'' f'no screenshot{_level_pill(r["level"])}' ) - cap = f'
{html.escape(r["level_cap_reason"])}
' if r["level_cap_reason"] else "" + cap = ( + f'
{html.escape(r["level_cap_reason"])}
' + if r["level_cap_reason"] + else "" + ) return ( f'
{shot}
' f'
{html.escape(r["recipe"])}
' @@ -307,7 +316,11 @@ def render_history(recipe, rows): trs = [] for r in rows: color = _COLORS.get(r["status"], "#8b949e") - lvl = "—" if r["level"] is None else f'L{int(r["level"])}' + lvl = ( + "—" + if r["level"] is None + else f'L{int(r["level"])}' + ) shot = f'card' if r["has_screenshot"] else "—" trs.append( f'#{r["number"]}' @@ -317,7 +330,7 @@ def render_history(recipe, rows): ) body = "\n".join(trs) or 'no runs for this recipe yet' inner = ( - f'

{_FLOWER} {html.escape(recipe)} — run history

' + f"

{_FLOWER} {html.escape(recipe)} — run history

" '

← all recipes · every !testme run, newest first.

' "" "" diff --git a/flake.nix b/flake.nix index a1d9f58..48ffb7a 100644 --- a/flake.nix +++ b/flake.nix @@ -31,34 +31,36 @@ ]; in { - # Canonical live host target: the Hetzner cc-ci server. - # Use `.#cc-ci` for the current production host. - nixosConfigurations.cc-ci = nixpkgs.lib.nixosSystem { - inherit system; - modules = [ - sops-nix.nixosModules.sops - ./nix/hosts/cc-ci-hetzner/configuration.nix - ]; - }; + nixosConfigurations = { + # Canonical live host target: the Hetzner cc-ci server. + # Use `.#cc-ci` for the current production host. + cc-ci = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + sops-nix.nixosModules.sops + ./nix/hosts/cc-ci-hetzner/configuration.nix + ]; + }; - # Legacy Incus VM host definition retained only for historical comparison and fallback. - # Do NOT use this target on the live Hetzner server. - nixosConfigurations.cc-ci-incus = nixpkgs.lib.nixosSystem { - inherit system; - modules = [ - sops-nix.nixosModules.sops - ./nix/hosts/cc-ci/configuration.nix - ]; - }; + # Legacy Incus VM host definition retained only for historical comparison and fallback. + # Do NOT use this target on the live Hetzner server. + cc-ci-incus = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + sops-nix.nixosModules.sops + ./nix/hosts/cc-ci/configuration.nix + ]; + }; - # Explicit alias for the live Hetzner host. Kept alongside `cc-ci` so the intended host target - # remains obvious in recovery/migration workflows. - nixosConfigurations.cc-ci-hetzner = nixpkgs.lib.nixosSystem { - inherit system; - modules = [ - sops-nix.nixosModules.sops - ./nix/hosts/cc-ci-hetzner/configuration.nix - ]; + # Explicit alias for the live Hetzner host. Kept alongside `cc-ci` so the intended host + # target remains obvious in recovery/migration workflows. + cc-ci-hetzner = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + sops-nix.nixosModules.sops + ./nix/hosts/cc-ci-hetzner/configuration.nix + ]; + }; }; devShells.${system} = { diff --git a/nix/hosts/cc-ci-hetzner/configuration.nix b/nix/hosts/cc-ci-hetzner/configuration.nix index 388f293..243b86a 100644 --- a/nix/hosts/cc-ci-hetzner/configuration.nix +++ b/nix/hosts/cc-ci-hetzner/configuration.nix @@ -7,7 +7,7 @@ # git clone --recursive https://git.autonomic.zone/recipe-maintainers/cc-ci.git /etc/cc-ci # install -m600 /var/lib/sops-nix/key.txt # nixos-rebuild switch --flake /etc/cc-ci#cc-ci-hetzner -{ pkgs, lib, ... }: +{ pkgs, ... }: { imports = [ ./hardware.nix diff --git a/nix/hosts/cc-ci-hetzner/hardware.nix b/nix/hosts/cc-ci-hetzner/hardware.nix index d19d0ac..dec7e73 100644 --- a/nix/hosts/cc-ci-hetzner/hardware.nix +++ b/nix/hosts/cc-ci-hetzner/hardware.nix @@ -11,13 +11,17 @@ { imports = [ (modulesPath + "/profiles/qemu-guest.nix") ]; - boot.loader = { - efi.efiSysMountPoint = "/boot/efi"; - grub = { - efiSupport = true; - efiInstallAsRemovable = true; - device = "nodev"; + boot = { + loader = { + efi.efiSysMountPoint = "/boot/efi"; + grub = { + efiSupport = true; + efiInstallAsRemovable = true; + device = "nodev"; + }; }; + initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" "vmw_pvscsi" ]; + initrd.kernelModules = [ "nvme" ]; }; fileSystems."/boot/efi" = { @@ -25,9 +29,6 @@ fsType = "vfat"; }; - boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" "vmw_pvscsi" ]; - boot.initrd.kernelModules = [ "nvme" ]; - fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; diff --git a/nix/modules/nightly-sweep.nix b/nix/modules/nightly-sweep.nix index 42018d3..c31b70a 100644 --- a/nix/modules/nightly-sweep.nix +++ b/nix/modules/nightly-sweep.nix @@ -29,7 +29,7 @@ in serviceConfig = { Type = "oneshot"; # A full sweep across several recipes (each a cold deploy/test/teardown) is long; bound it. - TimeoutStartSec = "21600"; # 6h ceiling + TimeoutStartSec = "21600"; # 6h ceiling ExecStart = "${sweep}/bin/cc-ci-nightly-sweep"; }; }; @@ -39,7 +39,7 @@ in wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "*-*-* 03:00:00"; - Persistent = true; # catch up a missed nightly after downtime + Persistent = true; # catch up a missed nightly after downtime RandomizedDelaySec = "600"; }; }; diff --git a/runner/harness/abra.py b/runner/harness/abra.py index a17d24f..1c92066 100644 --- a/runner/harness/abra.py +++ b/runner/harness/abra.py @@ -168,7 +168,9 @@ def secret_generate(domain: str, timeout: int = 300) -> None: ) -def deploy(domain: str, chaos: bool = True, timeout: int = 900, no_converge_checks: bool = False) -> None: +def deploy( + domain: str, chaos: bool = True, timeout: int = 900, no_converge_checks: bool = False +) -> None: args = ["app", "deploy", domain, "-o", "-n"] if chaos: args.append("-C") @@ -203,7 +205,10 @@ def backup_create(domain: str, timeout: int = 900) -> str: # remote and fails "authentication required: Unauthorized". Returns the captured output, whose # restic JSON summary line carries the produced "snapshot_id" (the backup artifact, DG3) — note # `abra app backup snapshots` needs a TTY and is awkward to script, so we read the create output. - out = _run_pty(["app", "backup", "create", domain, "-n", "-C", "-o"], timeout=timeout).stdout or "" + out = ( + _run_pty(["app", "backup", "create", domain, "-n", "-C", "-o"], timeout=timeout).stdout + or "" + ) # Echo the backup output (incl. backupbot's pre-hook run / any "Failed to run command" or # "Container ... not running" ERROR) into the run log. Backup is otherwise opaque: a pre-hook that # fails to register/run leaves the DB dump out of the snapshot, surfacing only as a downstream diff --git a/runner/harness/browser.py b/runner/harness/browser.py index 8333a2b..786a7fa 100644 --- a/runner/harness/browser.py +++ b/runner/harness/browser.py @@ -13,8 +13,15 @@ from __future__ import annotations import time -def goto_with_retry(page, url, *, deadline_seconds: int = 120, accept_statuses=(200, 304), - goto_timeout_ms: int = 30_000, wait_until: str = "domcontentloaded"): +def goto_with_retry( + page, + url, + *, + deadline_seconds: int = 120, + accept_statuses=(200, 304), + goto_timeout_ms: int = 30_000, + wait_until: str = "domcontentloaded", +): """Poll `page.goto(url)` until status is in `accept_statuses` OR the deadline expires. Returns the final Playwright response. Raises AssertionError if the deadline expires without diff --git a/runner/harness/canonical.py b/runner/harness/canonical.py index 64e21b2..a80d299 100644 --- a/runner/harness/canonical.py +++ b/runner/harness/canonical.py @@ -55,7 +55,9 @@ def enrolled_recipes() -> list[str]: out = [] try: for name in sorted(os.listdir(tests_dir)): - if os.path.isfile(os.path.join(tests_dir, name, "recipe_meta.py")) and is_enrolled(name): + if os.path.isfile(os.path.join(tests_dir, name, "recipe_meta.py")) and is_enrolled( + name + ): out.append(name) except OSError: pass @@ -122,11 +124,15 @@ def deploy_canonical(recipe: str, timeout: int = 900) -> None: abra.recipe_checkout(recipe, version) r = subprocess.run( ["abra", "app", "deploy", domain, version, "-o", "-n", "-f"], - capture_output=True, text=True, timeout=timeout, + capture_output=True, + text=True, + timeout=timeout, ) if r.returncode != 0: - raise RuntimeError(f"deploy canonical {domain} {version} failed: " - f"{(r.stderr + ' ' + r.stdout).strip()[:300]}") + raise RuntimeError( + f"deploy canonical {domain} {version} failed: " + f"{(r.stderr + ' ' + r.stdout).strip()[:300]}" + ) _set_status(recipe, "warm") diff --git a/runner/harness/card.py b/runner/harness/card.py index 6f44d2a..28be566 100644 --- a/runner/harness/card.py +++ b/runner/harness/card.py @@ -148,7 +148,9 @@ RUNG_LABEL = { "backup_restore": "backup/restore", "functional": "functional", } -SKIP_GREEN = "#57ab5a" # muted green — an intentional skip reads like a pass (but labelled, never inflating) +SKIP_GREEN = ( + "#57ab5a" # muted green — an intentional skip reads like a pass (but labelled, never inflating) +) def _skip_rows(skips: dict) -> str: @@ -159,14 +161,16 @@ def _skip_rows(skips: dict) -> str: for rung, reason in (skips.get("intentional") or {}).items(): rows.append( f'' + f"{html.escape(RUNG_LABEL.get(rung, rung))}" f'' ) - rows.append(f'') + rows.append( + f'' + ) for rung in skips.get("unintentional") or []: rows.append( f'' + f"{html.escape(RUNG_LABEL.get(rung, rung))}" f'' ) rows.append( diff --git a/runner/harness/deps.py b/runner/harness/deps.py index ba131cc..bd679a1 100644 --- a/runner/harness/deps.py +++ b/runner/harness/deps.py @@ -28,7 +28,7 @@ from __future__ import annotations import contextlib import json import os -from typing import Iterable +from collections.abc import Iterable from . import lifecycle, naming @@ -36,9 +36,7 @@ from . import lifecycle, naming def declared_deps(recipe: str) -> list[str]: """Read `DEPS` from `tests//recipe_meta.py` — a list of recipe names this recipe needs deployed alongside it. Returns [] if none.""" - path = os.path.join( - os.path.dirname(__file__), "..", "..", "tests", recipe, "recipe_meta.py" - ) + path = os.path.join(os.path.dirname(__file__), "..", "..", "tests", recipe, "recipe_meta.py") if not os.path.exists(path): return [] ns: dict = {} diff --git a/runner/harness/generic.py b/runner/harness/generic.py index 216b1ae..f42b896 100644 --- a/runner/harness/generic.py +++ b/runner/harness/generic.py @@ -222,7 +222,11 @@ def assert_restore_healthy(domain: str, meta: dict) -> None: def perform_upgrade( - domain: str, recipe: str, head_ref: str | None, deploy_timeout: int = 900, meta: dict | None = None + domain: str, + recipe: str, + head_ref: str | None, + deploy_timeout: int = 900, + meta: dict | None = None, ) -> dict[str, str | None]: """Perform the UPGRADE op once, in place, to the PR-HEAD code under test (HC1): re-checkout the PR head (the prev-tag base deploy reset the recipe working tree), then `abra app deploy --chaos` @@ -267,7 +271,9 @@ def perform_upgrade( deploy_timeout=int(meta.get("DEPLOY_TIMEOUT", deploy_timeout)), http_timeout=int(meta.get("HTTP_TIMEOUT", 300)), ) - lifecycle.wait_ready_probes(meta, domain, timeout=int(meta.get("DEPLOY_TIMEOUT", deploy_timeout))) + lifecycle.wait_ready_probes( + meta, domain, timeout=int(meta.get("DEPLOY_TIMEOUT", deploy_timeout)) + ) after = lifecycle.deployed_identity(domain) # Evidence (HC1): the chaos-version label = the deployed recipe commit; it should match the # PR-head we checked out — proving the upgrade deployed the code under test, not a published tag. diff --git a/runner/harness/http.py b/runner/harness/http.py index 002d9b6..1f1e802 100644 --- a/runner/harness/http.py +++ b/runner/harness/http.py @@ -73,7 +73,7 @@ def http_post( `data` is JSON-encoded if content_type='application/json', form-encoded if 'application/x-www-form-urlencoded' (the OIDC token endpoint form), or sent raw bytes if data is already bytes.""" - if isinstance(data, (bytes, bytearray)): + if isinstance(data, bytes | bytearray): body: bytes | None = bytes(data) elif content_type == "application/json" and data is not None: body = json.dumps(data).encode() @@ -107,7 +107,7 @@ def http_request( ) -> tuple[int, object | None]: """Arbitrary-method HTTP (PUT/DELETE/PATCH) for parity tests that mutate. Same shape as http_post (returns (status, json_or_None)).""" - if isinstance(data, (bytes, bytearray)): + if isinstance(data, bytes | bytearray): body: bytes | None = bytes(data) elif content_type == "application/json" and data is not None: body = json.dumps(data).encode() @@ -142,7 +142,7 @@ def post_with_headers( """Like http_post but ALSO returns the response headers as a dict — for APIs that hand back an auth token in a response header rather than the body (e.g. mattermost login → `Token` header). Returns (status, parsed_json_or_None, response_headers). status=0 + {} on transport failure.""" - if isinstance(data, (bytes, bytearray)): + if isinstance(data, bytes | bytearray): body: bytes | None = bytes(data) elif content_type == "application/json" and data is not None: body = json.dumps(data).encode() @@ -252,13 +252,16 @@ def retry_http_post( ) -> tuple[int, object | None]: """POST with retry until expect_fn(status, json) is truthy. Defaults to any 2xx.""" if expect_fn is None: + def expect_fn(s, _j): # noqa: ARG001 return 200 <= s < 300 result: list[tuple[int, object | None]] = [(0, None)] def _check(): - s, j = http_post(url, data=data, headers=headers, content_type=content_type, timeout=timeout) + s, j = http_post( + url, data=data, headers=headers, content_type=content_type, timeout=timeout + ) result[0] = (s, j) return expect_fn(s, j) diff --git a/runner/harness/warmsnap.py b/runner/harness/warmsnap.py index 36170c6..4c97863 100644 --- a/runner/harness/warmsnap.py +++ b/runner/harness/warmsnap.py @@ -113,7 +113,9 @@ def _assert_undeployed(domain: str) -> None: ) -def snapshot(recipe: str, domain: str, commit: str | None = None, version: str | None = None) -> dict: +def snapshot( + recipe: str, domain: str, commit: str | None = None, version: str | None = None +) -> dict: """Take a last-known-good snapshot of every data volume of 's stack. The app MUST be undeployed. Atomically replaces the prior last-good. Returns the written meta dict.""" _assert_undeployed(domain) @@ -169,7 +171,9 @@ def restore(recipe: str, domain: str) -> dict: for vol in meta.get("volumes", []): tar_path = os.path.join(volumes_dir(recipe), f"{vol}.tar") if vol not in current: - raise SnapshotError(f"snapshot volume {vol} absent from current stack {sorted(current)}") + raise SnapshotError( + f"snapshot volume {vol} absent from current stack {sorted(current)}" + ) mp = _volume_mountpoint(vol) # Clear the volume contents (incl. dotfiles) without removing the mountpoint itself. r = _run(["sh", "-c", f'rm -rf -- "{mp}"/* "{mp}"/.[!.]* "{mp}"/..?* 2>/dev/null; true']) diff --git a/runner/nightly_sweep.py b/runner/nightly_sweep.py index cf233c5..121d0ca 100644 --- a/runner/nightly_sweep.py +++ b/runner/nightly_sweep.py @@ -60,14 +60,17 @@ def sweep() -> int: for r in recipes: print(f"\n===== nightly: full-cold {r} (latest) =====", flush=True) env = dict(os.environ, RECIPE=r) - env.pop("REF", None) # latest, not a PR head + env.pop("REF", None) # latest, not a PR head env.pop("CCCI_QUICK", None) env.pop("MODE", None) rc = subprocess.run( [sys.executable, os.path.join(_here(), "run_recipe_ci.py")], env=env ).returncode results[r] = rc - print(f"nightly: {r} rc={rc} ({'green→canonical refreshed' if rc == 0 else 'red'})", flush=True) + print( + f"nightly: {r} rc={rc} ({'green→canonical refreshed' if rc == 0 else 'red'})", + flush=True, + ) # WC8 disk hygiene: drop warm data for de-enrolled canonicals; log the disk budget. pruned = canonical.prune_stale() if pruned: diff --git a/runner/warm_reconcile.py b/runner/warm_reconcile.py index f3e8496..d9e58b7 100644 --- a/runner/warm_reconcile.py +++ b/runner/warm_reconcile.py @@ -43,11 +43,16 @@ def _traefik_setup(recipe: str, domain: str, version: str) -> None: ssl_cert/ssl_key swarm secrets; NO ACME). Uses the proven abra.env_set (newline-safe, unlike the bash set_env that bit keycloak).""" cert_dir = "/var/lib/ci-certs/live" - if not (os.path.isfile(f"{cert_dir}/fullchain.pem") and os.path.isfile(f"{cert_dir}/privkey.pem")): + if not ( + os.path.isfile(f"{cert_dir}/fullchain.pem") and os.path.isfile(f"{cert_dir}/privkey.pem") + ): raise RuntimeError(f"FATAL: wildcard cert missing at {cert_dir} (sops decrypt broken?)") if not os.path.isfile(env_file(domain)): - _run(["abra", "app", "new", recipe, "-s", "default", "-D", domain, version, "-o", "-n"], - timeout=120, check=True) + _run( + ["abra", "app", "new", recipe, "-s", "default", "-D", domain, version, "-o", "-n"], + timeout=120, + check=True, + ) abra.env_set(domain, "DOMAIN", domain) abra.env_set(domain, "LETS_ENCRYPT_ENV", "") abra.env_set(domain, "WILDCARDS_ENABLED", "1") @@ -61,11 +66,39 @@ def _traefik_setup(recipe: str, domain: str, version: str) -> None: return any(s.endswith(f"_{name}_v1") for s in have) if not _has("ssl_cert"): - _run(["abra", "app", "secret", "insert", domain, "ssl_cert", "v1", - f"{cert_dir}/fullchain.pem", "-f", "-n"], timeout=120, check=True) + _run( + [ + "abra", + "app", + "secret", + "insert", + domain, + "ssl_cert", + "v1", + f"{cert_dir}/fullchain.pem", + "-f", + "-n", + ], + timeout=120, + check=True, + ) if not _has("ssl_key"): - _run(["abra", "app", "secret", "insert", domain, "ssl_key", "v1", - f"{cert_dir}/privkey.pem", "-f", "-n"], timeout=120, check=True) + _run( + [ + "abra", + "app", + "secret", + "insert", + domain, + "ssl_key", + "v1", + f"{cert_dir}/privkey.pem", + "-f", + "-n", + ], + timeout=120, + check=True, + ) SPECS: dict[str, dict] = { @@ -218,8 +251,17 @@ def health_code(spec: dict) -> int: domain = spec.get("health_domain", spec["domain"]) r = _run( [ - "curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "10", - "--resolve", f"{domain}:443:127.0.0.1", f"https://{domain}{spec['health_path']}", + "curl", + "-sk", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "--max-time", + "10", + "--resolve", + f"{domain}:443:127.0.0.1", + f"https://{domain}{spec['health_path']}", ], timeout=20, ) @@ -230,7 +272,6 @@ def health_code(spec: dict) -> int: def wait_healthy(spec: dict, timeout: int | None = None) -> bool: - domain = spec["domain"] deadline = time.time() + (timeout or spec["health_timeout"]) while time.time() < deadline: if health_code(spec) in tuple(spec["health_ok"]): @@ -325,15 +366,18 @@ def ensure_server() -> None: def ensure_app_config(recipe: str, domain: str, version: str) -> None: if not os.path.isfile(env_file(domain)): - _run(["abra", "app", "new", recipe, "-s", "default", "-D", domain, version, "-o", "-n"], - timeout=120, check=True) + _run( + ["abra", "app", "new", recipe, "-s", "default", "-D", domain, version, "-o", "-n"], + timeout=120, + check=True, + ) abra.env_set(domain, "DOMAIN", domain) abra.env_set(domain, "LETS_ENCRYPT_ENV", "") def ensure_secrets(domain: str) -> None: stack = lifecycle._stack_name(domain) # noqa: SLF001 - have = {n for n in lifecycle._docker_names("secret", stack)} # noqa: SLF001 + have = set(lifecycle._docker_names("secret", stack)) # noqa: SLF001 if not any(n.endswith("_admin_password_v1") for n in have): abra.secret_generate(domain) @@ -393,8 +437,9 @@ def reconcile(app: str) -> str: write_alert(app, "held-major", current=current, latest=latest, release_notes=notes[:4000]) return f"held-major:{current}->{latest}" if notes_flag_manual_migration(notes): - write_alert(app, "held-manual-migration", current=current, latest=latest, - release_notes=notes[:4000]) + write_alert( + app, "held-manual-migration", current=current, latest=latest, release_notes=notes[:4000] + ) return f"held-manual-migration:{current}->{latest}" # WC1.1 health-gated upgrade with rollback. @@ -428,8 +473,14 @@ def reconcile(app: str) -> str: warmsnap.restore(recipe, domain) deploy_version(recipe, domain, last_good, dt) recovered = wait_healthy(spec) - write_alert(app, "rollback", last_good=last_good, attempted=latest, recovered=recovered, - release_notes=notes[:2000]) + write_alert( + app, + "rollback", + last_good=last_good, + attempted=latest, + recovered=recovered, + release_notes=notes[:2000], + ) if not recovered: raise RuntimeError(f"{app} rollback to {last_good} did not become healthy") return f"rolled-back:{latest}->{last_good}" diff --git a/tests/bluesky-pds/_p4.py b/tests/bluesky-pds/_p4.py index 58063e1..29b2c74 100644 --- a/tests/bluesky-pds/_p4.py +++ b/tests/bluesky-pds/_p4.py @@ -15,7 +15,8 @@ import shlex import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) -from harness import http as harness_http, lifecycle # noqa: E402 +from harness import http as harness_http # noqa: E402 +from harness import lifecycle PDS_HOST_LOCAL = "http://localhost:3000" _PW = "ccci-P4-marker-pw-2026" diff --git a/tests/bluesky-pds/functional/test_account_and_post.py b/tests/bluesky-pds/functional/test_account_and_post.py index 5d0cb9a..ea8604f 100644 --- a/tests/bluesky-pds/functional/test_account_and_post.py +++ b/tests/bluesky-pds/functional/test_account_and_post.py @@ -27,6 +27,7 @@ CRUD). A wedged PDS subsystem fails AT its layer. from __future__ import annotations +import contextlib import os import re import secrets @@ -35,7 +36,8 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) -from harness import http as harness_http, lifecycle # noqa: E402 +from harness import http as harness_http # noqa: E402 +from harness import lifecycle PDS_HOST_LOCAL = "http://localhost:3000" @@ -58,14 +60,18 @@ def _goat_admin(domain: str, args: str) -> str: return _in_container(domain, cmd) -def _xrpc_post(domain: str, nsid: str, data: dict, token: str | None = None) -> tuple[int, dict | None]: +def _xrpc_post( + domain: str, nsid: str, data: dict, token: str | None = None +) -> tuple[int, dict | None]: headers = {} if token: headers["Authorization"] = f"Bearer {token}" return harness_http.http_post(f"https://{domain}/xrpc/{nsid}", data=data, headers=headers) -def _xrpc_get(domain: str, nsid: str, query: str, token: str | None = None) -> tuple[int, dict | None]: +def _xrpc_get( + domain: str, nsid: str, query: str, token: str | None = None +) -> tuple[int, dict | None]: headers = {} if token: headers["Authorization"] = f"Bearer {token}" @@ -82,9 +88,9 @@ def test_account_lifecycle_and_post_roundtrip(live_app): # Step 1: PDS describe via goat — recipe self-identifies as did:web: out = _in_container(domain, f"goat pds describe {PDS_HOST_LOCAL} 2>&1") - assert f"did:web:{domain}" in out, ( - f"goat pds describe did not contain expected DID 'did:web:{domain}'. Output:\n{out[:500]!r}" - ) + assert ( + f"did:web:{domain}" in out + ), f"goat pds describe did not contain expected DID 'did:web:{domain}'. Output:\n{out[:500]!r}" # Step 2: Create account (UUID-suffixed handle = no run-to-run collision) out = _goat_admin( @@ -127,9 +133,9 @@ def test_account_lifecycle_and_post_roundtrip(live_app): assert s == 200, f"createRecord HTTP {s}: {body!r}" record_uri = (body or {}).get("uri", "") # URI format: at:///app.bsky.feed.post/ - assert record_uri.startswith(f"at://{new_did}/app.bsky.feed.post/"), ( - f"unexpected record uri: {record_uri!r}" - ) + assert record_uri.startswith( + f"at://{new_did}/app.bsky.feed.post/" + ), f"unexpected record uri: {record_uri!r}" rkey = record_uri.rsplit("/", 1)[-1] assert rkey, f"no rkey in uri: {record_uri!r}" @@ -142,15 +148,13 @@ def test_account_lifecycle_and_post_roundtrip(live_app): ) assert s == 200, f"getRecord HTTP {s}: {body!r}" record_value = (body or {}).get("value", {}) - assert record_value.get("text") == marker, ( - f"post text did not round-trip: created={marker!r}, fetched={record_value.get('text')!r}" - ) + assert ( + record_value.get("text") == marker + ), f"post text did not round-trip: created={marker!r}, fetched={record_value.get('text')!r}" assert record_value.get("$type") == "app.bsky.feed.post" finally: # Step 6: Best-effort cleanup. (The per-run domain teardown will discard the volume # too, but we exercise the delete-account path because it's part of §4.3.) if cleanup_did: - try: + with contextlib.suppress(Exception): _goat_admin(domain, f"account delete {cleanup_did}") - except Exception: # noqa: BLE001 - pass diff --git a/tests/bluesky-pds/functional/test_describe_server.py b/tests/bluesky-pds/functional/test_describe_server.py index c993ffe..fd49524 100644 --- a/tests/bluesky-pds/functional/test_describe_server.py +++ b/tests/bluesky-pds/functional/test_describe_server.py @@ -26,6 +26,6 @@ def test_describe_server_returns_atproto_envelope(live_app): # At least one of these atproto-spec fields must be present expected_any = ("availableUserDomains", "inviteCodeRequired", "links", "did") present = [k for k in expected_any if k in body] - assert present, ( - f"describe-server missing all of {expected_any}; got keys: {sorted(body.keys())[:20]}" - ) + assert ( + present + ), f"describe-server missing all of {expected_any}; got keys: {sorted(body.keys())[:20]}" diff --git a/tests/bluesky-pds/functional/test_health_check.py b/tests/bluesky-pds/functional/test_health_check.py index 0fb19a3..847ab3e 100644 --- a/tests/bluesky-pds/functional/test_health_check.py +++ b/tests/bluesky-pds/functional/test_health_check.py @@ -17,6 +17,6 @@ def test_pds_health_returns_version(live_app): url = f"https://{live_app}/xrpc/_health" status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=60, interval=3) assert status == 200, f"GET {url} HTTP {status} (expected 200)" - assert isinstance(body, dict) and isinstance(body.get("version"), str) and body["version"], ( - f"GET {url} response is not the expected health envelope: {body!r}" - ) + assert ( + isinstance(body, dict) and isinstance(body.get("version"), str) and body["version"] + ), f"GET {url} response is not the expected health envelope: {body!r}" diff --git a/tests/bluesky-pds/functional/test_session_auth.py b/tests/bluesky-pds/functional/test_session_auth.py index da4c474..f26b417 100644 --- a/tests/bluesky-pds/functional/test_session_auth.py +++ b/tests/bluesky-pds/functional/test_session_auth.py @@ -30,6 +30,6 @@ def test_get_session_requires_auth(live_app): f"body: {body!r}" ) # The XRPC error envelope is JSON with an `error` field per the atproto spec. - assert isinstance(body, dict) and body.get("error"), ( - f"expected XRPC JSON error envelope; got: {body!r}" - ) + assert isinstance(body, dict) and body.get( + "error" + ), f"expected XRPC JSON error envelope; got: {body!r}" diff --git a/tests/bluesky-pds/install_steps.sh b/tests/bluesky-pds/install_steps.sh index f5ab73e..4a67e00 100755 --- a/tests/bluesky-pds/install_steps.sh +++ b/tests/bluesky-pds/install_steps.sh @@ -22,12 +22,12 @@ echo " bluesky-pds install_steps: generating secp256k1 PLC rotation key..." # same shape the PDS expects (32-byte hex). Equivalent for atproto PDS bootstrap. KEY_HEX=$(cc-ci-run -c 'import secrets; print(secrets.token_bytes(32).hex())') if [ -z "${KEY_HEX}" ] || [ "${#KEY_HEX}" != "64" ]; then - echo " install_steps: failed to generate PLC rotation key (KEY_HEX length=${#KEY_HEX})" >&2 - exit 1 + echo " install_steps: failed to generate PLC rotation key (KEY_HEX length=${#KEY_HEX})" >&2 + exit 1 fi # Insert via abra under TTY-wrap (`abra app secret insert` requires a TTY on this version). # We DON'T log the key value — abra also doesn't print it. script -qec "abra app secret insert ${CCCI_APP_DOMAIN} pds_plc_rotation_key v1 ${KEY_HEX} --no-input" /dev/null \ - >/dev/null 2>&1 + >/dev/null 2>&1 echo " bluesky-pds install_steps: PLC rotation key inserted (v1)." diff --git a/tests/bluesky-pds/test_restore.py b/tests/bluesky-pds/test_restore.py index 7bfa04f..6a7e798 100644 --- a/tests/bluesky-pds/test_restore.py +++ b/tests/bluesky-pds/test_restore.py @@ -11,6 +11,6 @@ import _p4 # noqa: E402 def test_restore_returns_state(live_app): - assert _p4.account_exists(live_app), ( - "restore did not bring back the seeded marker account (PDS data did not survive restore)" - ) + assert _p4.account_exists( + live_app + ), "restore did not bring back the seeded marker account (PDS data did not survive restore)" diff --git a/tests/conftest.py b/tests/conftest.py index 9575356..e9be5cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,8 @@ import sys import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "runner")) -from harness import deps as deps_mod, lifecycle, naming # noqa: E402 +from harness import deps as deps_mod # noqa: E402 +from harness import lifecycle, naming def _short(s: str, n: int = 8) -> str: diff --git a/tests/cryptpad/playwright/test_pad_content_roundtrip.py b/tests/cryptpad/playwright/test_pad_content_roundtrip.py index 7218c6b..0b9d1cf 100644 --- a/tests/cryptpad/playwright/test_pad_content_roundtrip.py +++ b/tests/cryptpad/playwright/test_pad_content_roundtrip.py @@ -26,6 +26,7 @@ Transient `net::ERR_NETWORK_CHANGED` is handled by the shared `goto_with_retry` from __future__ import annotations +import contextlib import os import sys import uuid @@ -39,7 +40,11 @@ def _open_pad(ctx, url): bar once CryptPad has created/loaded the fragment-keyed pad (`#/2/pad/edit//`).""" page = ctx.new_page() harness_browser.goto_with_retry( - page, url, accept_statuses=(200,), goto_timeout_ms=60_000, wait_until="load", + page, + url, + accept_statuses=(200,), + goto_timeout_ms=60_000, + wait_until="load", deadline_seconds=150, ) pad_url = url @@ -53,13 +58,15 @@ def _open_pad(ctx, url): pad_url = page.url break if i == 40: - try: + with contextlib.suppress(Exception): # best-effort unstick harness_browser.goto_with_retry( - page, url, accept_statuses=(200,), goto_timeout_ms=60_000, - wait_until="load", deadline_seconds=120, + page, + url, + accept_statuses=(200,), + goto_timeout_ms=60_000, + wait_until="load", + deadline_seconds=120, ) - except Exception: # noqa: BLE001 — best-effort unstick - pass return page, pad_url @@ -74,18 +81,22 @@ def _ckeditor_frame(page, deadline_polls=90, reload_at=22, reload_url=None): if "ckeditor-inner" in f.url: return f if i == reload_at and reload_url is not None: - try: + with contextlib.suppress(Exception): # reload is a best-effort unstick harness_browser.goto_with_retry( - page, reload_url, accept_statuses=(200,), goto_timeout_ms=60_000, - wait_until="load", deadline_seconds=120, + page, + reload_url, + accept_statuses=(200,), + goto_timeout_ms=60_000, + wait_until="load", + deadline_seconds=120, ) - except Exception: # noqa: BLE001 — reload is a best-effort unstick - pass page.wait_for_timeout(2000) return None -def _poll_any_frame_for_text(page, needle, deadline_polls=120, reload_at=(20, 45, 75, 100), reload_url=None): +def _poll_any_frame_for_text( + page, needle, deadline_polls=120, reload_at=(20, 45, 75, 100), reload_url=None +): """Robust read-back (F2-13): poll EVERY frame's body text for `needle`, returning True as soon as it appears. The fresh cold-cache read-back context's deeply-nested CKEditor frame is slow/flaky to *attach* by URL (the prior `_ckeditor_frame` wait timed out on the Adversary's cold run), but the @@ -101,13 +112,15 @@ def _poll_any_frame_for_text(page, needle, deadline_polls=120, reload_at=(20, 45 except Exception: # noqa: BLE001 — frame not ready / detached; keep polling pass if reload_url and i in reload_at: - try: + with contextlib.suppress(Exception): # best-effort unstick harness_browser.goto_with_retry( - page, reload_url, accept_statuses=(200,), goto_timeout_ms=60_000, - wait_until="load", deadline_seconds=120, + page, + reload_url, + accept_statuses=(200,), + goto_timeout_ms=60_000, + wait_until="load", + deadline_seconds=120, ) - except Exception: # noqa: BLE001 — best-effort unstick - pass page.wait_for_timeout(2000) return False @@ -137,9 +150,9 @@ def test_cryptpad_pad_content_survives_fresh_session(live_app): # --- session 1: create the pad + write the marker --- ctx1 = browser.new_context(ignore_https_errors=True) page, pad_url = _open_pad(ctx1, f"https://{live_app}/pad/") - assert "#/2/pad/edit/" in pad_url, ( - f"CryptPad did not create a fragment-keyed pad URL; got {pad_url!r}" - ) + assert ( + "#/2/pad/edit/" in pad_url + ), f"CryptPad did not create a fragment-keyed pad URL; got {pad_url!r}" ck = _ckeditor_frame(page, reload_url=pad_url) assert ck is not None, "CKEditor content frame never attached (pad editor not ready)" _dismiss_store_modal(page) @@ -148,9 +161,9 @@ def test_cryptpad_pad_content_survives_fresh_session(live_app): page.wait_for_timeout(1000) body.type(marker, delay=40) page.wait_for_timeout(12000) # let CryptPad encrypt + sync the update to the server - assert marker in ck.locator("body").inner_text(), ( - "marker not present in the editor after typing — type did not land" - ) + assert ( + marker in ck.locator("body").inner_text() + ), "marker not present in the editor after typing — type did not land" ctx1.close() # --- session 2: FRESH context (no shared storage/localStorage) reads the pad back by URL. diff --git a/tests/cryptpad/playwright/test_pad_create.py b/tests/cryptpad/playwright/test_pad_create.py index ecabba1..6dd57d0 100644 --- a/tests/cryptpad/playwright/test_pad_create.py +++ b/tests/cryptpad/playwright/test_pad_create.py @@ -51,9 +51,9 @@ def test_cryptpad_spa_renders_with_no_console_errors(live_app): title = (page.title() or "").lower() body = page.content() blower = body.lower() - assert "cryptpad" in title or "cryptpad" in blower, ( - f"CryptPad SPA does not carry brand. title={title!r}, body excerpt: {body[:200]!r}" - ) + assert ( + "cryptpad" in title or "cryptpad" in blower + ), f"CryptPad SPA does not carry brand. title={title!r}, body excerpt: {body[:200]!r}" # Canonical CryptPad asset references in the rendered DOM canonical = ("/customize/", "/components/", "main.js", "/api/broadcast") diff --git a/tests/cryptpad/test_install.py b/tests/cryptpad/test_install.py index 9038d60..ceeff73 100644 --- a/tests/cryptpad/test_install.py +++ b/tests/cryptpad/test_install.py @@ -8,7 +8,8 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) -from harness import browser as harness_browser, generic, lifecycle # noqa: E402 +from harness import browser as harness_browser # noqa: E402 +from harness import generic, lifecycle def test_serving_and_content(live_app, meta): diff --git a/tests/custom-html-bkp-bad/test_backup.py b/tests/custom-html-bkp-bad/test_backup.py index 373fae9..976354c 100644 --- a/tests/custom-html-bkp-bad/test_backup.py +++ b/tests/custom-html-bkp-bad/test_backup.py @@ -20,7 +20,9 @@ def test_backup_captures_state(live_app): Since custom-html-bkp-bad has no ops.py::pre_backup to seed the marker, this file does NOT exist at backup time — exec_in_app returns empty or raises → assertion fails → backup tier RED. This models a recipe that declares backup capability but omits the data-seeding hook.""" - result = lifecycle.exec_in_app(live_app, ["sh", "-c", f"cat {MARKER_PATH} 2>/dev/null || echo MISSING"]).strip() + result = lifecycle.exec_in_app( + live_app, ["sh", "-c", f"cat {MARKER_PATH} 2>/dev/null || echo MISSING"] + ).strip() assert result == "original", ( f"backup did not capture the expected marker at {MARKER_PATH}: got {result!r}. " "Expected 'original' (seeded by pre_backup). If the marker is 'MISSING', the pre_backup " diff --git a/tests/custom-html-tiny/functional/test_serves_content.py b/tests/custom-html-tiny/functional/test_serves_content.py index cb30917..7eb17e6 100644 --- a/tests/custom-html-tiny/functional/test_serves_content.py +++ b/tests/custom-html-tiny/functional/test_serves_content.py @@ -79,9 +79,9 @@ def test_static_file_roundtrip_and_404(live_app): # A random non-existent path must 404 — proves real static-file semantics, distinguishing a # working server from a 200-everything stub or a mis-routed Traefik fallback. miss_status, _ = _get(f"https://{live_app}/ccci-missing-{uuid.uuid4().hex}.txt") - assert miss_status == 404, ( - f"missing path returned {miss_status} (expected 404 — generic 200-returner / mis-route?)" - ) + assert ( + miss_status == 404 + ), f"missing path returned {miss_status} (expected 404 — generic 200-returner / mis-route?)" finally: with contextlib.suppress(OSError): os.remove(path) diff --git a/tests/custom-html/functional/test_content_roundtrip.py b/tests/custom-html/functional/test_content_roundtrip.py index c3a8b5f..7879314 100644 --- a/tests/custom-html/functional/test_content_roundtrip.py +++ b/tests/custom-html/functional/test_content_roundtrip.py @@ -15,7 +15,8 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) -from harness import http as harness_http, lifecycle # noqa: E402 +from harness import http as harness_http # noqa: E402 +from harness import lifecycle def test_content_roundtrip(live_app): diff --git a/tests/custom-html/functional/test_content_type_header.py b/tests/custom-html/functional/test_content_type_header.py index 64c2fd9..dba467b 100644 --- a/tests/custom-html/functional/test_content_type_header.py +++ b/tests/custom-html/functional/test_content_type_header.py @@ -53,9 +53,9 @@ def test_content_type_html_and_txt(live_app): ct_txt = h_txt.get("content-type", "") # nginx default: "text/html" for .html and "text/plain" for .txt (may include "; charset=utf-8") - assert ct_html.startswith("text/html"), ( - f"{html_name} Content-Type={ct_html!r}, expected text/html (nginx MIME config broken?)" - ) - assert ct_txt.startswith("text/plain"), ( - f"{txt_name} Content-Type={ct_txt!r}, expected text/plain (nginx MIME config broken?)" - ) + assert ct_html.startswith( + "text/html" + ), f"{html_name} Content-Type={ct_html!r}, expected text/html (nginx MIME config broken?)" + assert ct_txt.startswith( + "text/plain" + ), f"{txt_name} Content-Type={ct_txt!r}, expected text/plain (nginx MIME config broken?)" diff --git a/tests/custom-html/test_install.py b/tests/custom-html/test_install.py index 4a5c409..353eeb5 100644 --- a/tests/custom-html/test_install.py +++ b/tests/custom-html/test_install.py @@ -9,7 +9,8 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) -from harness import browser as harness_browser, generic # noqa: E402 +from harness import browser as harness_browser # noqa: E402 +from harness import generic def test_serving_and_content(live_app, meta): diff --git a/tests/discourse/functional/_discourse.py b/tests/discourse/functional/_discourse.py index 203e4ee..d548bd0 100644 --- a/tests/discourse/functional/_discourse.py +++ b/tests/discourse/functional/_discourse.py @@ -53,7 +53,7 @@ def mint_admin(domain: str) -> tuple[str, str]: cmd = ( "cd /opt/bitnami/discourse && " "RUBY=$(command -v ruby || echo /opt/bitnami/ruby/bin/ruby) && " - f"RAILS_ENV=production \"$RUBY\" bin/rails runner \"{_BOOTSTRAP_RB}\"" + f'RAILS_ENV=production "$RUBY" bin/rails runner "{_BOOTSTRAP_RB}"' ) out = lifecycle.exec_in_app(domain, ["bash", "-c", cmd], service="app", timeout=240) key = user = None @@ -63,9 +63,9 @@ def mint_admin(domain: str) -> tuple[str, str]: key = line.split("=", 1)[1].strip() elif line.startswith("CCCI_API_USER="): user = line.split("=", 1)[1].strip() - assert key and user, ( - f"could not bootstrap discourse admin/API key; rails output tail:\n{out[-1000:]}" - ) + assert ( + key and user + ), f"could not bootstrap discourse admin/API key; rails output tail:\n{out[-1000:]}" return key, user diff --git a/tests/discourse/functional/test_create_topic.py b/tests/discourse/functional/test_create_topic.py index fbadcef..4b078a5 100644 --- a/tests/discourse/functional/test_create_topic.py +++ b/tests/discourse/functional/test_create_topic.py @@ -48,21 +48,23 @@ def test_create_topic_roundtrip(live_app): headers=hdrs, timeout=60, ) - assert status in (200, 201) and isinstance(body, dict), ( - f"create topic failed: HTTP {status}, body={body!r}" - ) + assert status in (200, 201) and isinstance( + body, dict + ), f"create topic failed: HTTP {status}, body={body!r}" topic_id = body.get("topic_id") assert topic_id, f"create topic returned no topic_id: {body!r}" # 4) Read the topic back and assert title + first-post body round-trip. status, got = harness_http.http_get(f"{base}/t/{topic_id}.json", headers=hdrs, timeout=30) - assert status == 200 and isinstance(got, dict), f"read topic failed: HTTP {status}, body={got!r}" - assert got.get("title") == title, ( - f"topic title did not round-trip: sent {title!r}, got {got.get('title')!r}" - ) + assert status == 200 and isinstance( + got, dict + ), f"read topic failed: HTTP {status}, body={got!r}" + assert ( + got.get("title") == title + ), f"topic title did not round-trip: sent {title!r}, got {got.get('title')!r}" posts = (got.get("post_stream") or {}).get("posts") or [] assert posts, f"topic has no posts on read-back: {got!r}" first_cooked = posts[0].get("cooked", "") - assert marker in first_cooked, ( - f"topic body did not round-trip: marker {marker!r} not in first post {first_cooked!r}" - ) + assert ( + marker in first_cooked + ), f"topic body did not round-trip: marker {marker!r} not in first post {first_cooked!r}" diff --git a/tests/discourse/functional/test_site_basic.py b/tests/discourse/functional/test_site_basic.py index fd8793d..77414c2 100644 --- a/tests/discourse/functional/test_site_basic.py +++ b/tests/discourse/functional/test_site_basic.py @@ -20,12 +20,12 @@ def test_site_json_has_discourse_config(live_app): status, body = harness_http.retry_http_get( f"https://{live_app}/site.json", expect_status=200, max_wait=120, interval=5 ) - assert status == 200 and isinstance(body, dict), ( - f"GET /site.json failed: HTTP {status}, body type={type(body).__name__}" - ) + assert status == 200 and isinstance( + body, dict + ), f"GET /site.json failed: HTTP {status}, body type={type(body).__name__}" # /site.json carries Discourse-specific structure — `categories` (a list) and `groups` are always # present in a booted Discourse. A non-Discourse 200 (placeholder page) would not parse to this. assert "categories" in body, f"/site.json missing 'categories' key: keys={list(body)[:20]}" - assert isinstance(body["categories"], list), ( - f"/site.json 'categories' not a list: {type(body['categories']).__name__}" - ) + assert isinstance( + body["categories"], list + ), f"/site.json 'categories' not a list: {type(body['categories']).__name__}" diff --git a/tests/discourse/ops.py b/tests/discourse/ops.py index 73aa05d..5f619a7 100644 --- a/tests/discourse/ops.py +++ b/tests/discourse/ops.py @@ -15,8 +15,7 @@ from harness import lifecycle # noqa: E402 def _psql(domain, sql): cmd = ( - 'PGPASSWORD=$(cat /run/secrets/db_password) ' - f'psql -U discourse -d discourse -tAc "{sql}"' + "PGPASSWORD=$(cat /run/secrets/db_password) " f'psql -U discourse -d discourse -tAc "{sql}"' ) return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() @@ -42,6 +41,7 @@ def pre_backup(domain, meta): def pre_restore(domain, meta): # diverge from the backup so a successful restore is observable _psql(domain, "DROP TABLE IF EXISTS ci_marker;") - assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in ("", "NULL"), ( - "drop did not take" - ) + assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in ( + "", + "NULL", + ), "drop did not take" diff --git a/tests/discourse/recipe_meta.py b/tests/discourse/recipe_meta.py index 2af8ff6..9092875 100644 --- a/tests/discourse/recipe_meta.py +++ b/tests/discourse/recipe_meta.py @@ -6,7 +6,9 @@ # app is actually serving (the canonical "is discourse up" signal — NOT "/", which may redirect to setup). HEALTH_PATH = "/srv/status" HEALTH_OK = (200,) -DEPLOY_TIMEOUT = 3600 # slow Rails cold boot (15-25min) on the 7-GiB single node; bumped 2400→3600 for +DEPLOY_TIMEOUT = ( + 3600 # slow Rails cold boot (15-25min) on the 7-GiB single node; bumped 2400→3600 for +) # headroom after full4's base deploy timed out at 2400s (RAM/CPU-constrained boot + image re-pull). HTTP_TIMEOUT = 1200 @@ -59,7 +61,11 @@ def BACKUP_VERIFY(domain): try: out = lifecycle.exec_in_app( domain, - ["sh", "-c", "gzip -t /var/lib/postgresql/data/backup.sql && wc -c < /var/lib/postgresql/data/backup.sql"], + [ + "sh", + "-c", + "gzip -t /var/lib/postgresql/data/backup.sql && wc -c < /var/lib/postgresql/data/backup.sql", + ], service="db", timeout=60, ).strip() diff --git a/tests/discourse/test_backup.py b/tests/discourse/test_backup.py index 1955078..003dad3 100644 --- a/tests/discourse/test_backup.py +++ b/tests/discourse/test_backup.py @@ -14,13 +14,12 @@ from harness import lifecycle # noqa: E402 def _psql(domain, sql): cmd = ( - 'PGPASSWORD=$(cat /run/secrets/db_password) ' - f'psql -U discourse -d discourse -tAc "{sql}"' + "PGPASSWORD=$(cat /run/secrets/db_password) " f'psql -U discourse -d discourse -tAc "{sql}"' ) return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() def test_backup_captures_state(live_app): - assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", ( - "the seeded discourse postgres state was not present at backup time" - ) + assert ( + _psql(live_app, "SELECT v FROM ci_marker;") == "original" + ), "the seeded discourse postgres state was not present at backup time" diff --git a/tests/discourse/test_restore.py b/tests/discourse/test_restore.py index 7137028..f93b9e7 100644 --- a/tests/discourse/test_restore.py +++ b/tests/discourse/test_restore.py @@ -14,13 +14,12 @@ from harness import lifecycle # noqa: E402 def _psql(domain, sql): cmd = ( - 'PGPASSWORD=$(cat /run/secrets/db_password) ' - f'psql -U discourse -d discourse -tAc "{sql}"' + "PGPASSWORD=$(cat /run/secrets/db_password) " f'psql -U discourse -d discourse -tAc "{sql}"' ) return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() def test_restore_returns_state(live_app): - assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", ( - "restore did not return the pre-mutation discourse postgres state (data-integrity failure)" - ) + assert ( + _psql(live_app, "SELECT v FROM ci_marker;") == "original" + ), "restore did not return the pre-mutation discourse postgres state (data-integrity failure)" diff --git a/tests/ghost/functional/_ghost.py b/tests/ghost/functional/_ghost.py index 22a2e30..5147099 100644 --- a/tests/ghost/functional/_ghost.py +++ b/tests/ghost/functional/_ghost.py @@ -93,9 +93,10 @@ class GhostAdmin: status, body = self.req( "POST", "/session/", {"username": ADMIN_EMAIL, "password": ADMIN_PW} ) - assert status in (200, 201), ( - f"ghost admin session login failed: HTTP {status}, body={body!r}" - ) + assert status in ( + 200, + 201, + ), f"ghost admin session login failed: HTTP {status}, body={body!r}" def create_post(self, title: str, html: str) -> dict: status, body = self.req( diff --git a/tests/ghost/functional/test_admin_redirect.py b/tests/ghost/functional/test_admin_redirect.py index 2331a10..5403546 100644 --- a/tests/ghost/functional/test_admin_redirect.py +++ b/tests/ghost/functional/test_admin_redirect.py @@ -53,13 +53,15 @@ def test_ghost_admin_route_is_wired(live_app): return None status_body = harness_http.assert_converges( - _ready, f"GET {url} returns Ghost admin (200) or setup redirect (302)", - max_wait=60, interval=3, + _ready, + f"GET {url} returns Ghost admin (200) or setup redirect (302)", + max_wait=60, + interval=3, ) status, body = status_body assert status in (200, 302), f"unexpected status: {status}" if status == 200: # The admin SPA references /ghost-assets/ or contains "ghost" in title/body - assert "ghost" in body.lower(), ( - f"GET {url} 200 but body has no Ghost markers: {body[:200]!r}" - ) + assert ( + "ghost" in body.lower() + ), f"GET {url} 200 but body has no Ghost markers: {body[:200]!r}" diff --git a/tests/ghost/functional/test_content_api.py b/tests/ghost/functional/test_content_api.py index 32feaab..3c8282a 100644 --- a/tests/ghost/functional/test_content_api.py +++ b/tests/ghost/functional/test_content_api.py @@ -35,10 +35,10 @@ def test_content_api_settings_endpoint(live_app): assert body is not None, f"GET {url} returned non-JSON body" # On success: {"settings": {...}}. On error: {"errors": [...]}. Either shape is valid. if status == 200: - assert isinstance(body, dict) and "settings" in body, ( - f"200 response missing 'settings' envelope: {body!r}" - ) + assert ( + isinstance(body, dict) and "settings" in body + ), f"200 response missing 'settings' envelope: {body!r}" else: - assert isinstance(body, dict) and ("errors" in body or "message" in body or body), ( - f"error response not a proper Ghost error envelope: {body!r}" - ) + assert isinstance(body, dict) and ( + "errors" in body or "message" in body or body + ), f"error response not a proper Ghost error envelope: {body!r}" diff --git a/tests/ghost/functional/test_post_roundtrip.py b/tests/ghost/functional/test_post_roundtrip.py index ccb1916..f61f84d 100644 --- a/tests/ghost/functional/test_post_roundtrip.py +++ b/tests/ghost/functional/test_post_roundtrip.py @@ -43,17 +43,17 @@ def test_create_post_roundtrip(live_app): title = f"ccci-marker-{uniq}" marker = f"ccci-body-marker-{uniq}-roundtrip" created = admin.create_post(title, f"

{marker}

") - assert created.get("title") == title, ( - f"created post title mismatch: sent {title!r}, got {created.get('title')!r}" - ) + assert ( + created.get("title") == title + ), f"created post title mismatch: sent {title!r}, got {created.get('title')!r}" # 4) Read it back by id and assert the post survived the round-trip (title always returned; # html returned because we requested ?formats=html). got = admin.get_post(created["id"]) - assert got.get("title") == title, ( - f"post title did not round-trip: sent {title!r}, got {got.get('title')!r}" - ) + assert ( + got.get("title") == title + ), f"post title did not round-trip: sent {title!r}, got {got.get('title')!r}" html = got.get("html") or "" - assert marker in html, ( - f"post body did not round-trip: marker {marker!r} not in read-back html {html!r}" - ) + assert ( + marker in html + ), f"post body did not round-trip: marker {marker!r} not in read-back html {html!r}" diff --git a/tests/ghost/ops.py b/tests/ghost/ops.py index da1d782..24448ba 100644 --- a/tests/ghost/ops.py +++ b/tests/ghost/ops.py @@ -22,10 +22,7 @@ from harness import lifecycle # noqa: E402 def _mysql(domain, sql): - cmd = ( - 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' - f'mysql -u root -N -s ghost -e "{sql}"' - ) + cmd = 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' f'mysql -u root -N -s ghost -e "{sql}"' return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() diff --git a/tests/ghost/recipe_meta.py b/tests/ghost/recipe_meta.py index 5543f0a..710f50f 100644 --- a/tests/ghost/recipe_meta.py +++ b/tests/ghost/recipe_meta.py @@ -63,7 +63,11 @@ def BACKUP_VERIFY(domain): try: out = lifecycle.exec_in_app( domain, - ["sh", "-c", "gzip -t /var/lib/mysql/backup.sql.gz && wc -c < /var/lib/mysql/backup.sql.gz"], + [ + "sh", + "-c", + "gzip -t /var/lib/mysql/backup.sql.gz && wc -c < /var/lib/mysql/backup.sql.gz", + ], service="db", timeout=60, ).strip() diff --git a/tests/ghost/test_backup.py b/tests/ghost/test_backup.py index 27ece25..4f1a5ea 100644 --- a/tests/ghost/test_backup.py +++ b/tests/ghost/test_backup.py @@ -15,14 +15,11 @@ from harness import lifecycle # noqa: E402 def _mysql(domain, sql): - cmd = ( - 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' - f'mysql -u root -N -s ghost -e "{sql}"' - ) + cmd = 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' f'mysql -u root -N -s ghost -e "{sql}"' return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() def test_backup_captures_state(live_app): - assert _mysql(live_app, "SELECT v FROM ci_marker;") == "original", ( - "the seeded ghost MySQL marker was not present at backup time" - ) + assert ( + _mysql(live_app, "SELECT v FROM ci_marker;") == "original" + ), "the seeded ghost MySQL marker was not present at backup time" diff --git a/tests/ghost/test_restore.py b/tests/ghost/test_restore.py index 413e73e..2e0fbc3 100644 --- a/tests/ghost/test_restore.py +++ b/tests/ghost/test_restore.py @@ -22,10 +22,7 @@ from harness import lifecycle # noqa: E402 def _mysql(domain, sql): - cmd = ( - 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' - f'mysql -u root -N -s ghost -e "{sql}"' - ) + cmd = 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' f'mysql -u root -N -s ghost -e "{sql}"' return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() diff --git a/tests/ghost/test_upgrade.py b/tests/ghost/test_upgrade.py index bb4efd8..efebbdb 100644 --- a/tests/ghost/test_upgrade.py +++ b/tests/ghost/test_upgrade.py @@ -14,14 +14,11 @@ from harness import lifecycle # noqa: E402 def _mysql(domain, sql): - cmd = ( - 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' - f'mysql -u root -N -s ghost -e "{sql}"' - ) + cmd = 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' f'mysql -u root -N -s ghost -e "{sql}"' return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() def test_upgrade_preserves_state(live_app): - assert _mysql(live_app, "SELECT v FROM ci_marker;") == "upgrade-survives", ( - "the seeded ghost MySQL marker did not survive the upgrade redeploy (data loss on upgrade)" - ) + assert ( + _mysql(live_app, "SELECT v FROM ci_marker;") == "upgrade-survives" + ), "the seeded ghost MySQL marker did not survive the upgrade redeploy (data loss on upgrade)" diff --git a/tests/hedgedoc/functional/test_branding.py b/tests/hedgedoc/functional/test_branding.py index 2a87a23..bd13820 100644 --- a/tests/hedgedoc/functional/test_branding.py +++ b/tests/hedgedoc/functional/test_branding.py @@ -14,7 +14,6 @@ import urllib.request sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) from harness import http as harness_http # noqa: E402 - _CTX = ssl.create_default_context() _CTX.check_hostname = False _CTX.verify_mode = ssl.CERT_NONE diff --git a/tests/hedgedoc/functional/test_health_check.py b/tests/hedgedoc/functional/test_health_check.py index 4676345..067e2da 100644 --- a/tests/hedgedoc/functional/test_health_check.py +++ b/tests/hedgedoc/functional/test_health_check.py @@ -15,7 +15,5 @@ from harness import http as harness_http # noqa: E402 def test_hedgedoc_root_serves(live_app): """GET / → 200 or 302 (login/new redirect).""" url = f"https://{live_app}/" - status, _ = harness_http.retry_http_get( - url, expect_status=(200, 302), max_wait=90, interval=5 - ) + status, _ = harness_http.retry_http_get(url, expect_status=(200, 302), max_wait=90, interval=5) assert status in (200, 302), f"GET {url} HTTP {status} (expected 200 or 302)" diff --git a/tests/immich/functional/test_asset_processing.py b/tests/immich/functional/test_asset_processing.py index b5df995..d637308 100644 --- a/tests/immich/functional/test_asset_processing.py +++ b/tests/immich/functional/test_asset_processing.py @@ -111,13 +111,13 @@ def test_immich_processes_uploaded_asset_metadata_and_statistics(live_app): if exif and exif.get("exifImageWidth"): break time.sleep(5) - assert exif and exif.get("exifImageWidth") == 1 and exif.get("exifImageHeight") == 1, ( - f"immich metadata-extraction did not populate the 1x1 PNG dimensions in exifInfo: {exif!r}" - ) + assert ( + exif and exif.get("exifImageWidth") == 1 and exif.get("exifImageHeight") == 1 + ), f"immich metadata-extraction did not populate the 1x1 PNG dimensions in exifInfo: {exif!r}" # the asset is catalogued into the owner's library statistics (list-back in aggregate) sst, stats = harness_http.http_request("GET", f"{base}/api/assets/statistics", headers=auth) assert sst == 200 and isinstance(stats, dict), f"statistics HTTP {sst}: {stats!r}" - assert stats.get("images", 0) >= 1 and stats.get("total", 0) >= 1, ( - f"uploaded asset not reflected in library statistics: {stats!r}" - ) + assert ( + stats.get("images", 0) >= 1 and stats.get("total", 0) >= 1 + ), f"uploaded asset not reflected in library statistics: {stats!r}" diff --git a/tests/immich/functional/test_asset_upload.py b/tests/immich/functional/test_asset_upload.py index 05b7131..0339401 100644 --- a/tests/immich/functional/test_asset_upload.py +++ b/tests/immich/functional/test_asset_upload.py @@ -121,6 +121,6 @@ def test_immich_upload_asset_readback_and_thumbnail(live_app): if thumb == 200: break time.sleep(5) - assert thumb == 200, ( - f"immich did not generate a thumbnail/derivative for the uploaded asset (last HTTP {thumb})" - ) + assert ( + thumb == 200 + ), f"immich did not generate a thumbnail/derivative for the uploaded asset (last HTTP {thumb})" diff --git a/tests/immich/functional/test_health_check.py b/tests/immich/functional/test_health_check.py index cc5534e..c5b05ab 100644 --- a/tests/immich/functional/test_health_check.py +++ b/tests/immich/functional/test_health_check.py @@ -16,5 +16,11 @@ from harness import http as harness_http # noqa: E402 def test_immich_returns_200(live_app): url = f"https://{live_app}/" - status, _ = harness_http.retry_http_get(url, expect_status=(200, 301, 302), max_wait=60, interval=3) - assert status in (200, 301, 302), f"immich at {url} returned HTTP {status} (expected 200/301/302)" + status, _ = harness_http.retry_http_get( + url, expect_status=(200, 301, 302), max_wait=60, interval=3 + ) + assert status in ( + 200, + 301, + 302, + ), f"immich at {url} returned HTTP {status} (expected 200/301/302)" diff --git a/tests/immich/ops.py b/tests/immich/ops.py index a0bdb03..daa4d7d 100644 --- a/tests/immich/ops.py +++ b/tests/immich/ops.py @@ -35,4 +35,7 @@ def pre_backup(domain, meta): def pre_restore(domain, meta): _psql(domain, "DROP TABLE ci_marker;") - assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in ("", "NULL"), "drop did not take" + assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in ( + "", + "NULL", + ), "drop did not take" diff --git a/tests/immich/test_backup.py b/tests/immich/test_backup.py index e0a8af5..1dc8756 100644 --- a/tests/immich/test_backup.py +++ b/tests/immich/test_backup.py @@ -14,4 +14,6 @@ def _psql(domain, sql): def test_backup_captures_state(live_app): - assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", "seeded postgres state not present at backup time" + assert ( + _psql(live_app, "SELECT v FROM ci_marker;") == "original" + ), "seeded postgres state not present at backup time" diff --git a/tests/immich/test_install.py b/tests/immich/test_install.py index 7552ff2..9785107 100644 --- a/tests/immich/test_install.py +++ b/tests/immich/test_install.py @@ -7,7 +7,8 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) -from harness import browser as harness_browser, generic, lifecycle # noqa: E402 +from harness import browser as harness_browser # noqa: E402 +from harness import generic, lifecycle def test_serving_and_frontend(live_app, meta): @@ -25,7 +26,11 @@ def test_serving_and_frontend(live_app, meta): resp = harness_browser.goto_with_retry( page, url, accept_statuses=(200, 301, 302), goto_timeout_ms=60_000 ) - assert resp is not None and resp.status in (200, 301, 302), f"page status {resp and resp.status}" + assert resp is not None and resp.status in ( + 200, + 301, + 302, + ), f"page status {resp and resp.status}" assert "-<6hex>" so concurrent dependents never collide. - assert re.fullmatch(r"lasuite-docs-[0-9a-f]{6}", kc["realm"]), ( - f"realm {kc['realm']!r} not the per-run namespaced form lasuite-docs-<6hex>" - ) + assert re.fullmatch( + r"lasuite-docs-[0-9a-f]{6}", kc["realm"] + ), f"realm {kc['realm']!r} not the per-run namespaced form lasuite-docs-<6hex>" assert kc["client_id"] == "lasuite-docs" assert isinstance(kc["client_secret"], str) and len(kc["client_secret"]) >= 16 assert isinstance(kc["password"], str) and len(kc["password"]) >= 16 @@ -74,16 +74,14 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds): # Password grant → real JWT token = sso.oidc_password_grant(creds) - assert isinstance(token, str) and token.count(".") == 2, ( - f"access_token is not a JWT: {token!r}" - ) + assert isinstance(token, str) and token.count(".") == 2, f"access_token is not a JWT: {token!r}" payload = json.loads(_b64url_decode(token.split(".")[1])) assert payload.get("iss") == expected_iss, f"JWT iss={payload.get('iss')!r} != {expected_iss!r}" - assert payload.get("azp") == kc["client_id"], ( - f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}" - ) + assert ( + payload.get("azp") == kc["client_id"] + ), f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}" assert payload.get("typ") == "Bearer", f"JWT typ={payload.get('typ')!r} != 'Bearer'" exp = payload.get("exp") - assert isinstance(exp, int) and exp > time.time(), ( - f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})" - ) + assert ( + isinstance(exp, int) and exp > time.time() + ), f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})" diff --git a/tests/lasuite-docs/setup_custom_tests.sh b/tests/lasuite-docs/setup_custom_tests.sh index 469f783..8f61331 100755 --- a/tests/lasuite-docs/setup_custom_tests.sh +++ b/tests/lasuite-docs/setup_custom_tests.sh @@ -21,15 +21,24 @@ set -euo pipefail : "${CCCI_APP_DOMAIN:?missing}" : "${CCCI_DEPS_FILE:?missing}" -test -s "$CCCI_DEPS_FILE" || { echo " setup_custom_tests: deps file empty"; exit 1; } +test -s "$CCCI_DEPS_FILE" || { + echo " setup_custom_tests: deps file empty" + exit 1 +} # Read keycloak dep info via jq -KC_DOMAIN=$(jq -r '.keycloak.domain' "$CCCI_DEPS_FILE") -KC_REALM=$( jq -r '.keycloak.realm' "$CCCI_DEPS_FILE") -KC_CLIENT=$(jq -r '.keycloak.client_id' "$CCCI_DEPS_FILE") -KC_SECRET=$(jq -r '.keycloak.client_secret' "$CCCI_DEPS_FILE") -[ -n "$KC_DOMAIN" ] && [ "$KC_DOMAIN" != "null" ] || { echo " setup_custom_tests: no keycloak.domain in deps"; exit 1; } -[ -n "$KC_SECRET" ] && [ "$KC_SECRET" != "null" ] || { echo " setup_custom_tests: no keycloak.client_secret"; exit 1; } +KC_DOMAIN=$(jq -r '.keycloak.domain' "$CCCI_DEPS_FILE") +KC_REALM=$(jq -r '.keycloak.realm' "$CCCI_DEPS_FILE") +KC_CLIENT=$(jq -r '.keycloak.client_id' "$CCCI_DEPS_FILE") +KC_SECRET=$(jq -r '.keycloak.client_secret' "$CCCI_DEPS_FILE") +if [ -z "$KC_DOMAIN" ] || [ "$KC_DOMAIN" = "null" ]; then + echo " setup_custom_tests: no keycloak.domain in deps" + exit 1 +fi +if [ -z "$KC_SECRET" ] || [ "$KC_SECRET" = "null" ]; then + echo " setup_custom_tests: no keycloak.client_secret" + exit 1 +fi echo " lasuite-docs setup_custom_tests: wiring OIDC against keycloak dep ${KC_DOMAIN}" @@ -39,12 +48,15 @@ echo " lasuite-docs setup_custom_tests: wiring OIDC against keycloak dep ${KC_D # update SECRET_OIDC_RPCS_VERSION in the .env to point at the new one. ENV_PATH="$HOME/.abra/servers/default/${CCCI_APP_DOMAIN}.env" CUR_VER=$(grep -E '^\s*SECRET_OIDC_RPCS_VERSION=' "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1") -NEW_NUM=$(( ${CUR_VER#v} + 1 )) +NEW_NUM=$((${CUR_VER#v} + 1)) NEW_VER="v${NEW_NUM}" -INSERT_LOG=$(abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o 2>&1) \ - || INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) \ - || { echo " setup_custom_tests: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"; exit 1; } +INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) || + INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) || + { + echo " setup_custom_tests: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG" + exit 1 + } # Repoint the env var to the new version sed -i "s|^\s*SECRET_OIDC_RPCS_VERSION=.*|SECRET_OIDC_RPCS_VERSION=$NEW_VER|" "$ENV_PATH" echo " setup_custom_tests: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)" @@ -52,25 +64,25 @@ echo " setup_custom_tests: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER) # 2) Write OIDC env vars to the app's .env (names per lasuite-docs's .env.sample). # Ensure the file ends with a newline FIRST so our appends don't concatenate onto the last line # (we saw `TIMEOUT=900OIDC_REALM=...` malformed by a missing-trailing-newline file). -[ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >> "$ENV_PATH" -write_env () { +[ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH" +write_env() { local key="$1" val="$2" # remove any existing key (commented or live) then append the live key=val sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH" # Re-ensure trailing newline after each delete (sed may leave the file without one) - [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >> "$ENV_PATH" - printf '%s=%s\n' "$key" "$val" >> "$ENV_PATH" + [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH" + printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH" } -write_env OIDC_REALM "$KC_REALM" -write_env OIDC_OP_DISCOVERY_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/.well-known/openid-configuration" -write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth" -write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token" -write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo" -write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout" -write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs" -write_env OIDC_RP_CLIENT_ID "$KC_CLIENT" -write_env OIDC_RP_SIGN_ALGO "RS256" -write_env OIDC_RP_SCOPES "openid email profile" +write_env OIDC_REALM "$KC_REALM" +write_env OIDC_OP_DISCOVERY_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/.well-known/openid-configuration" +write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth" +write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token" +write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo" +write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout" +write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs" +write_env OIDC_RP_CLIENT_ID "$KC_CLIENT" +write_env OIDC_RP_SIGN_ALGO "RS256" +write_env OIDC_RP_SCOPES "openid email profile" # 3) Trigger an in-place redeploy so the env update takes effect. --force re-deploys even when # the recipe hasn't changed; --chaos avoids the chaos prompt; --no-input non-interactive. diff --git a/tests/lasuite-docs/test_install.py b/tests/lasuite-docs/test_install.py index 9bf94e3..fa35a11 100644 --- a/tests/lasuite-docs/test_install.py +++ b/tests/lasuite-docs/test_install.py @@ -10,7 +10,8 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) -from harness import browser as harness_browser, generic, lifecycle # noqa: E402 +from harness import browser as harness_browser # noqa: E402 +from harness import generic, lifecycle def test_serving_and_frontend(live_app, meta): diff --git a/tests/lasuite-drive/functional/test_health_check.py b/tests/lasuite-drive/functional/test_health_check.py index 0be8b14..06ec229 100644 --- a/tests/lasuite-drive/functional/test_health_check.py +++ b/tests/lasuite-drive/functional/test_health_check.py @@ -25,6 +25,8 @@ def test_lasuite_drive_returns_200(live_app): status, _ = harness_http.retry_http_get( url, expect_status=(200, 301, 302), max_wait=60, interval=3 ) - assert status in (200, 301, 302), ( - f"lasuite-drive at {url} returned HTTP {status} (expected 200/301/302)" - ) + assert status in ( + 200, + 301, + 302, + ), f"lasuite-drive at {url} returned HTTP {status} (expected 200/301/302)" diff --git a/tests/lasuite-drive/functional/test_minio_storage.py b/tests/lasuite-drive/functional/test_minio_storage.py index 3c3d15a..b11d22d 100644 --- a/tests/lasuite-drive/functional/test_minio_storage.py +++ b/tests/lasuite-drive/functional/test_minio_storage.py @@ -29,8 +29,8 @@ BUCKET = "drive-media-storage" def _mc(domain: str, script: str) -> str: """Run an `mc` shell script inside the minio container (root creds from /run/secrets).""" prelude = ( - 'set -e; ' - 'U=$(cat /run/secrets/minio_ru); P=$(cat /run/secrets/minio_rp); ' + "set -e; " + "U=$(cat /run/secrets/minio_ru); P=$(cat /run/secrets/minio_rp); " 'mc alias set ccci http://localhost:9000 "$U" "$P" >/dev/null 2>&1; ' ) return lifecycle.exec_in_app(domain, ["sh", "-c", prelude + script], service="minio") @@ -49,13 +49,13 @@ def test_minio_bucket_present_and_object_roundtrip(live_app): domain, # upload via stdin; list the object; read it back (tagged); then delete. f'printf %s "{marker}" | mc pipe ccci/{BUCKET}/{key} >/dev/null 2>&1; ' - f'mc ls ccci/{BUCKET}/{key}; ' + f"mc ls ccci/{BUCKET}/{key}; " f'echo "READBACK:$(mc cat ccci/{BUCKET}/{key})"; ' - f'mc rm ccci/{BUCKET}/{key} >/dev/null 2>&1', + f"mc rm ccci/{BUCKET}/{key} >/dev/null 2>&1", ) # The object was listed (its key appears) and its content round-tripped intact. assert f"{marker}.txt" in out, f"uploaded object not listed in bucket: {out!r}" - assert f"READBACK:{marker}" in out, ( - f"object content did not round-trip through MinIO; got: {out!r}" - ) + assert ( + f"READBACK:{marker}" in out + ), f"object content did not round-trip through MinIO; got: {out!r}" diff --git a/tests/lasuite-drive/functional/test_oidc_with_keycloak.py b/tests/lasuite-drive/functional/test_oidc_with_keycloak.py index eb2820f..e2aaef7 100644 --- a/tests/lasuite-drive/functional/test_oidc_with_keycloak.py +++ b/tests/lasuite-drive/functional/test_oidc_with_keycloak.py @@ -46,9 +46,9 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds): # Creds shape. WC1: realm is per-run namespaced "-<6hex>"; client_id stays the parent. assert kc["domain"] - assert re.fullmatch(r"lasuite-drive-[0-9a-f]{6}", kc["realm"]), ( - f"realm {kc['realm']!r} not the per-run namespaced form lasuite-drive-<6hex>" - ) + assert re.fullmatch( + r"lasuite-drive-[0-9a-f]{6}", kc["realm"] + ), f"realm {kc['realm']!r} not the per-run namespaced form lasuite-drive-<6hex>" assert kc["client_id"] == "lasuite-drive" assert isinstance(kc["client_secret"], str) and len(kc["client_secret"]) >= 16 assert isinstance(kc["password"], str) and len(kc["password"]) >= 16 @@ -77,16 +77,14 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds): # Password grant → real JWT token = sso.oidc_password_grant(creds) - assert isinstance(token, str) and token.count(".") == 2, ( - f"access_token is not a JWT: {token!r}" - ) + assert isinstance(token, str) and token.count(".") == 2, f"access_token is not a JWT: {token!r}" payload = json.loads(_b64url_decode(token.split(".")[1])) assert payload.get("iss") == expected_iss, f"JWT iss={payload.get('iss')!r} != {expected_iss!r}" - assert payload.get("azp") == kc["client_id"], ( - f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}" - ) + assert ( + payload.get("azp") == kc["client_id"] + ), f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}" assert payload.get("typ") == "Bearer", f"JWT typ={payload.get('typ')!r} != 'Bearer'" exp = payload.get("exp") - assert isinstance(exp, int) and exp > time.time(), ( - f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})" - ) + assert ( + isinstance(exp, int) and exp > time.time() + ), f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})" diff --git a/tests/lasuite-drive/install_steps.sh b/tests/lasuite-drive/install_steps.sh index 7ce2a52..c864ff3 100755 --- a/tests/lasuite-drive/install_steps.sh +++ b/tests/lasuite-drive/install_steps.sh @@ -28,7 +28,7 @@ if [ -z "${CCCI_DEPS_FILE:-}" ] || [ ! -s "${CCCI_DEPS_FILE}" ]; then exit 0 fi KC_DOMAIN=$(jq -r '.keycloak.domain // empty' "$CCCI_DEPS_FILE") -KC_REALM=$( jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE") +KC_REALM=$(jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE") KC_CLIENT=$(jq -r '.keycloak.client_id // empty' "$CCCI_DEPS_FILE") KC_SECRET=$(jq -r '.keycloak.client_secret // empty' "$CCCI_DEPS_FILE") if [ -z "$KC_DOMAIN" ] || [ -z "$KC_SECRET" ]; then @@ -43,35 +43,38 @@ echo " lasuite-drive install_steps: wiring OIDC at install against keycloak ${K # point SECRET_OIDC_RPCS_VERSION at it. (The app is not deployed yet — a swarm secret can be created # independently of a running stack — so the single deploy below picks up v2.) CUR_VER=$(grep -E '^\s*SECRET_OIDC_RPCS_VERSION=' "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1") -NEW_NUM=$(( ${CUR_VER#v} + 1 )) +NEW_NUM=$((${CUR_VER#v} + 1)) NEW_VER="v${NEW_NUM}" -INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) \ - || INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) \ - || { echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"; exit 1; } +INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) || + INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) || + { + echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG" + exit 1 + } sed -i "s|^\s*SECRET_OIDC_RPCS_VERSION=.*|SECRET_OIDC_RPCS_VERSION=$NEW_VER|" "$ENV_PATH" echo " install_steps: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)" # 2) Write the OIDC env vars (explicit endpoints — deterministic, no reliance on ${AUTH_DOMAIN} # expansion). Mirrors the recipe-maintainer impress/La Suite OIDC env contract. -write_env () { +write_env() { local key="$1" val="$2" sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH" - [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >> "$ENV_PATH" - printf '%s=%s\n' "$key" "$val" >> "$ENV_PATH" + [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH" + printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH" } -write_env AUTH_DOMAIN "$KC_DOMAIN" -write_env OIDC_REALM "$KC_REALM" -write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs" -write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth" -write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token" -write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo" -write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout" -write_env OIDC_RP_CLIENT_ID "$KC_CLIENT" -write_env OIDC_RP_SIGN_ALGO "RS256" -write_env OIDC_RP_SCOPES "openid email profile" -write_env OIDC_REDIRECT_ALLOWED_HOSTS "[\"https://${KC_DOMAIN}\", \"https://${CCCI_APP_DOMAIN}\"]" +write_env AUTH_DOMAIN "$KC_DOMAIN" +write_env OIDC_REALM "$KC_REALM" +write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs" +write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth" +write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token" +write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo" +write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout" +write_env OIDC_RP_CLIENT_ID "$KC_CLIENT" +write_env OIDC_RP_SIGN_ALGO "RS256" +write_env OIDC_RP_SCOPES "openid email profile" +write_env OIDC_REDIRECT_ALLOWED_HOSTS "[\"https://${KC_DOMAIN}\", \"https://${CCCI_APP_DOMAIN}\"]" # The recipe default acr_values=eidas1 is FranceConnect-specific; keycloak can't satisfy it and it # would break the interactive auth flow. Clear it so the keycloak OIDC client works. -write_env OIDC_AUTH_REQUEST_EXTRA_PARAMS "{}" +write_env OIDC_AUTH_REQUEST_EXTRA_PARAMS "{}" echo " lasuite-drive install_steps: OIDC env wired into .env (deploy will pick it up, no reconverge)" diff --git a/tests/lasuite-drive/setup_custom_tests.sh b/tests/lasuite-drive/setup_custom_tests.sh index 1bd3d34..65d84f2 100755 --- a/tests/lasuite-drive/setup_custom_tests.sh +++ b/tests/lasuite-drive/setup_custom_tests.sh @@ -29,7 +29,7 @@ docker service scale --detach "${STACK}_minio-createbuckets=1" >/dev/null 2>&1 | for i in $(seq 1 30); do MC_CID=$(docker ps -q -f "name=${STACK}_minio.1" | head -1) if [ -n "$MC_CID" ] && docker exec "$MC_CID" sh -c \ - 'mc alias set _c http://localhost:9000 "$(cat /run/secrets/minio_ru)" "$(cat /run/secrets/minio_rp)" >/dev/null 2>&1 && mc ls _c/drive-media-storage >/dev/null 2>&1'; then + 'mc alias set _c http://localhost:9000 "$(cat /run/secrets/minio_ru)" "$(cat /run/secrets/minio_rp)" >/dev/null 2>&1 && mc ls _c/drive-media-storage >/dev/null 2>&1'; then echo " setup: bucket drive-media-storage present after ${i} poll(s)" break fi diff --git a/tests/lasuite-drive/test_install.py b/tests/lasuite-drive/test_install.py index 6115bff..b0f7869 100644 --- a/tests/lasuite-drive/test_install.py +++ b/tests/lasuite-drive/test_install.py @@ -10,7 +10,8 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) -from harness import browser as harness_browser, generic, lifecycle # noqa: E402 +from harness import browser as harness_browser # noqa: E402 +from harness import generic, lifecycle def test_serving_and_frontend(live_app, meta): diff --git a/tests/lasuite-meet/functional/test_health_check.py b/tests/lasuite-meet/functional/test_health_check.py index b854916..c7e2132 100644 --- a/tests/lasuite-meet/functional/test_health_check.py +++ b/tests/lasuite-meet/functional/test_health_check.py @@ -21,6 +21,8 @@ def test_lasuite_meet_returns_200(live_app): status, _ = harness_http.retry_http_get( url, expect_status=(200, 301, 302), max_wait=60, interval=3 ) - assert status in (200, 301, 302), ( - f"lasuite-meet at {url} returned HTTP {status} (expected 200/301/302)" - ) + assert status in ( + 200, + 301, + 302, + ), f"lasuite-meet at {url} returned HTTP {status} (expected 200/301/302)" diff --git a/tests/lasuite-meet/functional/test_meeting_flow.py b/tests/lasuite-meet/functional/test_meeting_flow.py index b34156e..333f6a4 100644 --- a/tests/lasuite-meet/functional/test_meeting_flow.py +++ b/tests/lasuite-meet/functional/test_meeting_flow.py @@ -28,7 +28,8 @@ import sys import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) -from harness import http as harness_http, sso # noqa: E402 +from harness import http as harness_http # noqa: E402 +from harness import sso def _b64url(seg: str) -> bytes: @@ -74,33 +75,40 @@ def test_create_room_get_livekit_token_and_read_back(live_app, deps_creds): lk_room = livekit.get("room") lk_token = livekit.get("token") assert room_id, f"room created but no id: {body!r}" - assert lk_token and isinstance(lk_token, str) and lk_token.count(".") == 2, ( - f"room created but no LiveKit JWT token: {livekit!r}" - ) + assert ( + lk_token and isinstance(lk_token, str) and lk_token.count(".") == 2 + ), f"room created but no LiveKit JWT token: {livekit!r}" try: # --- read it back (a fresh authenticated GET of the created room) --- - status, got = harness_http.http_request("GET", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth) + status, got = harness_http.http_request( + "GET", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth + ) assert status == 200, f"room read-back returned HTTP {status} (expected 200); body={got!r}" - assert isinstance(got, dict) and got.get("id") == room_id, ( - f"read-back room id mismatch: {got!r}" - ) - got_lk = (got.get("livekit") or {}) + assert ( + isinstance(got, dict) and got.get("id") == room_id + ), f"read-back room id mismatch: {got!r}" + got_lk = got.get("livekit") or {} assert got_lk.get("token"), f"read-back room missing LiveKit token: {got!r}" - assert got_lk.get("room") == lk_room, ( - f"read-back LiveKit room {got_lk.get('room')!r} != create-time {lk_room!r}" - ) + assert ( + got_lk.get("room") == lk_room + ), f"read-back LiveKit room {got_lk.get('room')!r} != create-time {lk_room!r}" # --- the LiveKit token is a real signaling grant for this room (WebRTC subset) --- payload = json.loads(_b64url(lk_token.split(".")[1])) video = payload.get("video") or {} - assert video.get("room") == lk_room or payload.get("room") == lk_room, ( - f"LiveKit JWT does not grant the created room {lk_room!r}: {payload!r}" - ) + assert ( + video.get("room") == lk_room or payload.get("room") == lk_room + ), f"LiveKit JWT does not grant the created room {lk_room!r}: {payload!r}" finally: # --- delete the room (cleanup + a real DELETE mutation) --- - del_status, _ = harness_http.http_request("DELETE", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth) - assert del_status in (204, 200), f"room delete returned HTTP {del_status} (expected 204/200)" + del_status, _ = harness_http.http_request( + "DELETE", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth + ) + assert del_status in ( + 204, + 200, + ), f"room delete returned HTTP {del_status} (expected 204/200)" # --- best-effort: confirm the delete took (404 on re-GET). The §4.3 floor (create-an-object + # read-it-back + LiveKit-token issuance) is already proven by the hard assertions above; this @@ -112,7 +120,9 @@ def test_create_room_get_livekit_token_and_read_back(live_app, deps_creds): gone = False for _ in range(5): - status, _ = harness_http.http_request("GET", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth) + status, _ = harness_http.http_request( + "GET", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth + ) if status == 404: gone = True break diff --git a/tests/lasuite-meet/functional/test_oidc_with_keycloak.py b/tests/lasuite-meet/functional/test_oidc_with_keycloak.py index ef59d54..3335d2c 100644 --- a/tests/lasuite-meet/functional/test_oidc_with_keycloak.py +++ b/tests/lasuite-meet/functional/test_oidc_with_keycloak.py @@ -46,9 +46,9 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds): # Creds shape. WC1: realm is per-run namespaced "-<6hex>"; client_id stays the parent. assert kc["domain"] - assert re.fullmatch(r"lasuite-meet-[0-9a-f]{6}", kc["realm"]), ( - f"realm {kc['realm']!r} not the per-run namespaced form lasuite-meet-<6hex>" - ) + assert re.fullmatch( + r"lasuite-meet-[0-9a-f]{6}", kc["realm"] + ), f"realm {kc['realm']!r} not the per-run namespaced form lasuite-meet-<6hex>" assert kc["client_id"] == "lasuite-meet" assert isinstance(kc["client_secret"], str) and len(kc["client_secret"]) >= 16 assert isinstance(kc["password"], str) and len(kc["password"]) >= 16 @@ -77,16 +77,14 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds): # Password grant → real JWT token = sso.oidc_password_grant(creds) - assert isinstance(token, str) and token.count(".") == 2, ( - f"access_token is not a JWT: {token!r}" - ) + assert isinstance(token, str) and token.count(".") == 2, f"access_token is not a JWT: {token!r}" payload = json.loads(_b64url_decode(token.split(".")[1])) assert payload.get("iss") == expected_iss, f"JWT iss={payload.get('iss')!r} != {expected_iss!r}" - assert payload.get("azp") == kc["client_id"], ( - f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}" - ) + assert ( + payload.get("azp") == kc["client_id"] + ), f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}" assert payload.get("typ") == "Bearer", f"JWT typ={payload.get('typ')!r} != 'Bearer'" exp = payload.get("exp") - assert isinstance(exp, int) and exp > time.time(), ( - f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})" - ) + assert ( + isinstance(exp, int) and exp > time.time() + ), f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})" diff --git a/tests/lasuite-meet/install_steps.sh b/tests/lasuite-meet/install_steps.sh index 73b02a4..8d310eb 100755 --- a/tests/lasuite-meet/install_steps.sh +++ b/tests/lasuite-meet/install_steps.sh @@ -26,7 +26,7 @@ if [ -z "${CCCI_DEPS_FILE:-}" ] || [ ! -s "${CCCI_DEPS_FILE}" ]; then exit 0 fi KC_DOMAIN=$(jq -r '.keycloak.domain // empty' "$CCCI_DEPS_FILE") -KC_REALM=$( jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE") +KC_REALM=$(jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE") KC_CLIENT=$(jq -r '.keycloak.client_id // empty' "$CCCI_DEPS_FILE") KC_SECRET=$(jq -r '.keycloak.client_secret // empty' "$CCCI_DEPS_FILE") if [ -z "$KC_DOMAIN" ] || [ -z "$KC_SECRET" ]; then @@ -40,31 +40,34 @@ echo " lasuite-meet install_steps: wiring OIDC at install against keycloak ${KC # forbids overwriting a secret at the same version). The app is not deployed yet — a swarm secret can # be created independently — so the single deploy below picks up v2. CUR_VER=$(grep -E '^\s*SECRET_OIDC_RPCS_VERSION=' "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1") -NEW_NUM=$(( ${CUR_VER#v} + 1 )) +NEW_NUM=$((${CUR_VER#v} + 1)) NEW_VER="v${NEW_NUM}" -INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) \ - || INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) \ - || { echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"; exit 1; } +INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) || + INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) || + { + echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG" + exit 1 + } sed -i "s|^\s*SECRET_OIDC_RPCS_VERSION=.*|SECRET_OIDC_RPCS_VERSION=$NEW_VER|" "$ENV_PATH" echo " install_steps: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)" # 2) Write the OIDC env vars (explicit endpoints — deterministic). Meet's .env.sample templates the # endpoints off ${AUTH_DOMAIN}; set AUTH_DOMAIN + override each endpoint with the concrete realm URL. -write_env () { +write_env() { local key="$1" val="$2" sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH" - [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >> "$ENV_PATH" - printf '%s=%s\n' "$key" "$val" >> "$ENV_PATH" + [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH" + printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH" } -write_env AUTH_DOMAIN "$KC_DOMAIN" -write_env OIDC_REALM "$KC_REALM" -write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs" -write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth" -write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token" -write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo" -write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout" -write_env OIDC_RP_CLIENT_ID "$KC_CLIENT" -write_env OIDC_RP_SIGN_ALGO "RS256" -write_env OIDC_RP_SCOPES "openid email" +write_env AUTH_DOMAIN "$KC_DOMAIN" +write_env OIDC_REALM "$KC_REALM" +write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs" +write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth" +write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token" +write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo" +write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout" +write_env OIDC_RP_CLIENT_ID "$KC_CLIENT" +write_env OIDC_RP_SIGN_ALGO "RS256" +write_env OIDC_RP_SCOPES "openid email" echo " lasuite-meet install_steps: OIDC env wired into .env (deploy will pick it up, no reconverge)" diff --git a/tests/lasuite-meet/test_install.py b/tests/lasuite-meet/test_install.py index d2e4d40..fe9c686 100644 --- a/tests/lasuite-meet/test_install.py +++ b/tests/lasuite-meet/test_install.py @@ -10,7 +10,8 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) -from harness import browser as harness_browser, generic, lifecycle # noqa: E402 +from harness import browser as harness_browser # noqa: E402 +from harness import generic, lifecycle def test_serving_and_frontend(live_app, meta): @@ -33,9 +34,11 @@ def test_serving_and_frontend(live_app, meta): resp = harness_browser.goto_with_retry( page, url, accept_statuses=(200, 301, 302), goto_timeout_ms=60_000 ) - assert resp is not None and resp.status in (200, 301, 302), ( - f"page status {resp and resp.status}" - ) + assert resp is not None and resp.status in ( + 200, + 301, + 302, + ), f"page status {resp and resp.status}" assert " 0, ( - f"server.version is not a non-empty string: {version!r}" - ) + assert ( + isinstance(version, str) and len(version) > 0 + ), f"server.version is not a non-empty string: {version!r}" diff --git a/tests/matrix-synapse/functional/test_health_check.py b/tests/matrix-synapse/functional/test_health_check.py index e655c41..01f1e10 100644 --- a/tests/matrix-synapse/functional/test_health_check.py +++ b/tests/matrix-synapse/functional/test_health_check.py @@ -11,7 +11,6 @@ Runs in the custom tier against the shared post-install live deployment. from __future__ import annotations -import json import os import sys @@ -24,6 +23,6 @@ def test_synapse_client_versions_returns_json(live_app): url = f"https://{live_app}/_matrix/client/versions" status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=60, interval=3) assert status == 200, f"GET {url} HTTP {status} (expected 200)" - assert isinstance(body, dict) and isinstance(body.get("versions"), list) and body["versions"], ( - f"GET {url} did not return Matrix client-versions document: {body!r}" - ) + assert ( + isinstance(body, dict) and isinstance(body.get("versions"), list) and body["versions"] + ), f"GET {url} did not return Matrix client-versions document: {body!r}" diff --git a/tests/matrix-synapse/functional/test_register_and_message.py b/tests/matrix-synapse/functional/test_register_and_message.py index a00249e..b5c8067 100644 --- a/tests/matrix-synapse/functional/test_register_and_message.py +++ b/tests/matrix-synapse/functional/test_register_and_message.py @@ -42,7 +42,8 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) -from harness import http as harness_http, lifecycle # noqa: E402 +from harness import http as harness_http # noqa: E402 +from harness import lifecycle def _registration_secret(domain: str) -> str: @@ -101,8 +102,11 @@ def _admin_register(domain: str, secret: str, username: str, password: str, admi r = _container_curl(domain, "GET", "/_synapse/admin/v1/register") if r["status"] in (500, 502, 503, 504, 0): last = r - print(f" [register] {username}: nonce GET transient {r['status']} " - f"(attempt {attempt}, synapse recovering) — retrying", flush=True) + print( + f" [register] {username}: nonce GET transient {r['status']} " + f"(attempt {attempt}, synapse recovering) — retrying", + flush=True, + ) time.sleep(5) continue assert r["status"] == 200, f"nonce GET failed: status={r['status']} raw={r['raw'][:200]!r}" @@ -122,13 +126,19 @@ def _admin_register(domain: str, secret: str, username: str, password: str, admi r = _container_curl(domain, "POST", "/_synapse/admin/v1/register", body=payload) if r["status"] == 200: if attempt > 1: - print(f" [register] {username}: succeeded on attempt {attempt} " - f"(synapse recovered)", flush=True) + print( + f" [register] {username}: succeeded on attempt {attempt} " + f"(synapse recovered)", + flush=True, + ) return r["body"] or {} if r["status"] in (500, 502, 503, 504, 0): last = r - print(f" [register] {username}: POST transient {r['status']} " - f"(attempt {attempt}, synapse recovering) — retrying", flush=True) + print( + f" [register] {username}: POST transient {r['status']} " + f"(attempt {attempt}, synapse recovering) — retrying", + flush=True, + ) time.sleep(5) continue # a 4xx is a real rejection — fail fast, do not retry @@ -167,9 +177,9 @@ def test_register_two_users_send_receive_message(live_app): create + invite + join a room; send and read a message.""" domain = live_app secret = _registration_secret(domain) - assert secret and len(secret) >= 16, ( - f"registration shared secret missing/short: len={len(secret) if secret else 0}" - ) + assert ( + secret and len(secret) >= 16 + ), f"registration shared secret missing/short: len={len(secret) if secret else 0}" suffix = uuid.uuid4().hex[:8] user_a = f"alice{suffix}" diff --git a/tests/mattermost-lts/functional/test_create_message.py b/tests/mattermost-lts/functional/test_create_message.py index a3efdd0..0c066a2 100644 --- a/tests/mattermost-lts/functional/test_create_message.py +++ b/tests/mattermost-lts/functional/test_create_message.py @@ -41,9 +41,9 @@ def test_create_message_roundtrip(live_app): headers=auth, timeout=30, ) - assert status in (200, 201) and isinstance(team, dict) and team.get("id"), ( - f"team creation failed: HTTP {status}, body={team!r}" - ) + assert ( + status in (200, 201) and isinstance(team, dict) and team.get("id") + ), f"team creation failed: HTTP {status}, body={team!r}" status, chan = harness_http.http_post( f"{base}/channels", data={ @@ -55,9 +55,9 @@ def test_create_message_roundtrip(live_app): headers=auth, timeout=30, ) - assert status in (200, 201) and isinstance(chan, dict) and chan.get("id"), ( - f"channel creation failed: HTTP {status}, body={chan!r}" - ) + assert ( + status in (200, 201) and isinstance(chan, dict) and chan.get("id") + ), f"channel creation failed: HTTP {status}, body={chan!r}" # 4) POST a unique marker message. marker = f"ccci-marker-{uniq}-roundtrip" @@ -67,13 +67,13 @@ def test_create_message_roundtrip(live_app): headers=auth, timeout=30, ) - assert status in (200, 201) and isinstance(post, dict) and post.get("id"), ( - f"post creation failed: HTTP {status}, body={post!r}" - ) + assert ( + status in (200, 201) and isinstance(post, dict) and post.get("id") + ), f"post creation failed: HTTP {status}, body={post!r}" # 5) Read it back by id and assert the message survived the round-trip. status, got = harness_http.http_get(f"{base}/posts/{post['id']}", headers=auth, timeout=30) assert status == 200 and isinstance(got, dict), f"read-back failed: HTTP {status}, body={got!r}" - assert got.get("message") == marker, ( - f"message did not round-trip: sent {marker!r}, got {got.get('message')!r}" - ) + assert ( + got.get("message") == marker + ), f"message did not round-trip: sent {marker!r}, got {got.get('message')!r}" diff --git a/tests/mattermost-lts/functional/test_health_check.py b/tests/mattermost-lts/functional/test_health_check.py index abdfcbe..605caa2 100644 --- a/tests/mattermost-lts/functional/test_health_check.py +++ b/tests/mattermost-lts/functional/test_health_check.py @@ -18,9 +18,7 @@ from harness import http as harness_http # noqa: E402 def test_root_serves(live_app): """GET / → 200 or 302 (mattermost web app shell / login redirect).""" url = f"https://{live_app}/" - status, _ = harness_http.retry_http_get( - url, expect_status=(200, 302), max_wait=60, interval=3 - ) + status, _ = harness_http.retry_http_get(url, expect_status=(200, 302), max_wait=60, interval=3) assert status in (200, 302), f"GET {url} HTTP {status} (expected 200/302)" @@ -28,10 +26,8 @@ def test_system_ping_ok(live_app): """GET /api/v4/system/ping → 200 with JSON {"status":"OK"} — the mattermost server's own liveness endpoint (distinguishes a live mattermost API from a Traefik fallback / dead backend).""" url = f"https://{live_app}/api/v4/system/ping" - status, body = harness_http.retry_http_get( - url, expect_status=200, max_wait=120, interval=3 - ) + status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=120, interval=3) assert status == 200, f"GET {url} HTTP {status} (expected 200)" - assert isinstance(body, dict) and body.get("status") == "OK", ( - f"/api/v4/system/ping did not report status=OK; got {body!r}" - ) + assert ( + isinstance(body, dict) and body.get("status") == "OK" + ), f"/api/v4/system/ping did not report status=OK; got {body!r}" diff --git a/tests/mattermost-lts/functional/test_multiuser_message.py b/tests/mattermost-lts/functional/test_multiuser_message.py index b9903dc..387e436 100644 --- a/tests/mattermost-lts/functional/test_multiuser_message.py +++ b/tests/mattermost-lts/functional/test_multiuser_message.py @@ -51,7 +51,12 @@ def test_second_user_reads_first_users_message(live_app): assert status in (200, 201) and team.get("id"), f"team create HTTP {status}: {team!r}" status, chan = harness_http.http_post( f"{base}/channels", - data={"team_id": team["id"], "name": f"c{uniq}", "display_name": f"chan {uniq}", "type": "O"}, + data={ + "team_id": team["id"], + "name": f"c{uniq}", + "display_name": f"chan {uniq}", + "type": "O", + }, headers=auth_a, timeout=30, ) @@ -60,7 +65,10 @@ def test_second_user_reads_first_users_message(live_app): # 2) user_a posts a unique marker marker = f"ccci-multiuser-{uniq}" status, post = harness_http.http_post( - f"{base}/posts", data={"channel_id": chan["id"], "message": marker}, headers=auth_a, timeout=30 + f"{base}/posts", + data={"channel_id": chan["id"], "message": marker}, + headers=auth_a, + timeout=30, ) assert status in (200, 201) and post.get("id"), f"post create HTTP {status}: {post!r}" @@ -97,6 +105,6 @@ def test_second_user_reads_first_users_message(live_app): # 5) user_b sees user_a's marker (cross-user delivery, not a self read-back) messages = [p.get("message") for p in (posts.get("posts") or {}).values()] - assert marker in messages, ( - f"user_b did not see user_a's message {marker!r} in the channel; saw {messages!r}" - ) + assert ( + marker in messages + ), f"user_b did not see user_a's message {marker!r} in the channel; saw {messages!r}" diff --git a/tests/mattermost-lts/test_install.py b/tests/mattermost-lts/test_install.py index fd73f9b..e8a40b4 100644 --- a/tests/mattermost-lts/test_install.py +++ b/tests/mattermost-lts/test_install.py @@ -15,6 +15,4 @@ def test_serving_and_api(live_app, meta): generic.assert_serving(live_app, meta) # ... then the recipe-specific assertion: the mattermost REST liveness endpoint answers 200. status = lifecycle.http_get(live_app, "/api/v4/system/ping") - assert status == 200, ( - f"expected 200 from {live_app}/api/v4/system/ping, got {status}" - ) + assert status == 200, f"expected 200 from {live_app}/api/v4/system/ping, got {status}" diff --git a/tests/mumble/functional/_mumble_proto.py b/tests/mumble/functional/_mumble_proto.py index a98846c..daaa69e 100644 --- a/tests/mumble/functional/_mumble_proto.py +++ b/tests/mumble/functional/_mumble_proto.py @@ -12,6 +12,7 @@ cc-ci host (`mode: host`); tests run on-host via cc-ci-run, so they connect to 1 from __future__ import annotations +import contextlib import socket import ssl import struct @@ -29,8 +30,14 @@ MSG_USERSTATE = 9 MSG_SERVERCONFIG = 24 REJECT_TYPES = { - 0: "None", 1: "WrongVersion", 2: "InvalidUsername", 3: "WrongUserPW", - 4: "WrongServerPW", 5: "UsernameInUse", 6: "ServerFull", 7: "NoCertificate", + 0: "None", + 1: "WrongVersion", + 2: "InvalidUsername", + 3: "WrongUserPW", + 4: "WrongServerPW", + 5: "UsernameInUse", + 6: "ServerFull", + 7: "NoCertificate", 8: "AuthenticatorFail", } @@ -81,7 +88,7 @@ def _dec_fields(data: bytes) -> dict: off += 8 elif wire == 2: length, off = _dec_varint(data, off) - raw = data[off:off + length] + raw = data[off : off + length] off += length try: value = raw.decode("utf-8") @@ -120,9 +127,11 @@ def _recv(sock, timeout: float) -> tuple[int, bytes]: def _build_version() -> bytes: v = (1 << 16) | (5 << 8) | 0 # pretend client 1.5.0 - return (_enc_field_varint(1, v) - + _enc_field_string(2, "cc-ci mumble probe 1.0") - + _enc_field_string(3, "Linux")) + return ( + _enc_field_varint(1, v) + + _enc_field_string(2, "cc-ci mumble probe 1.0") + + _enc_field_string(3, "Linux") + ) def _build_authenticate(username: str, password: str = "") -> bytes: @@ -133,18 +142,29 @@ def _build_authenticate(username: str, password: str = "") -> bytes: return payload -def handshake(host: str = "127.0.0.1", port: int = PORT, username: str = "cc-ci-probe", - password: str = "", timeout: float = 20.0) -> dict: +def handshake( + host: str = "127.0.0.1", + port: int = PORT, + username: str = "cc-ci-probe", + password: str = "", + timeout: float = 20.0, +) -> dict: """Full Mumble control-channel handshake. Returns a result dict: - tls_connect (bool), server_version (dict|None), auth_accepted (bool), channels (list[str]), - users (list[str]), server_sync (bool), welcome_text (str|None), server_config (dict), - error (str|None). + tls_connect (bool), server_version (dict|None), auth_accepted (bool), channels (list[str]), + users (list[str]), server_sync (bool), welcome_text (str|None), server_config (dict), + error (str|None). """ result = { - "tls_connect": False, "server_version": None, "auth_accepted": False, - "channels": [], "users": [], "server_sync": False, "welcome_text": None, - "server_config": {}, "error": None, + "tls_connect": False, + "server_version": None, + "auth_accepted": False, + "channels": [], + "users": [], + "server_sync": False, + "welcome_text": None, + "server_config": {}, + "error": None, } raw = tls = None try: @@ -181,19 +201,21 @@ def handshake(host: str = "127.0.0.1", port: int = PORT, username: str = "cc-ci- break try: msg_type, payload = _recv(tls, timeout=remaining) - except (socket.timeout, ConnectionError): + except (TimeoutError, ConnectionError): break if msg_type == MSG_VERSION: f = _dec_fields(payload) v1 = f.get(1, 0) result["server_version"] = { "string": f"{(v1 >> 16) & 0xFF}.{(v1 >> 8) & 0xFF}.{v1 & 0xFF}", - "release": f.get(2, ""), "os": f.get(3, ""), + "release": f.get(2, ""), + "os": f.get(3, ""), } elif msg_type == MSG_REJECT: f = _dec_fields(payload) - result["error"] = (f"Rejected: {REJECT_TYPES.get(f.get(1, 0), 'Unknown')} " - f"— {f.get(2, '')}") + result["error"] = ( + f"Rejected: {REJECT_TYPES.get(f.get(1, 0), 'Unknown')} " f"— {f.get(2, '')}" + ) return result elif msg_type == MSG_CHANNELSTATE: f = _dec_fields(payload) @@ -209,9 +231,12 @@ def handshake(host: str = "127.0.0.1", port: int = PORT, username: str = "cc-ci- # ServerConfig fields: 1 max_bandwidth, 2 welcome_text, 3 allow_html, # 4 message_length, 5 image_message_length, 6 max_users result["server_config"] = { - "max_bandwidth": f.get(1), "welcome_text": f.get(2), - "allow_html": f.get(3), "message_length": f.get(4), - "image_message_length": f.get(5), "max_users": f.get(6), + "max_bandwidth": f.get(1), + "welcome_text": f.get(2), + "allow_html": f.get(3), + "message_length": f.get(4), + "image_message_length": f.get(5), + "max_users": f.get(6), } elif msg_type == MSG_SERVERSYNC: f = _dec_fields(payload) @@ -230,10 +255,8 @@ def handshake(host: str = "127.0.0.1", port: int = PORT, username: str = "cc-ci- result["error"] = f"{type(e).__name__}: {e}" finally: if tls is not None: - try: + with contextlib.suppress(OSError): tls.shutdown(socket.SHUT_RDWR) - except OSError: - pass tls.close() elif raw is not None: raw.close() diff --git a/tests/mumble/functional/test_protocol_handshake.py b/tests/mumble/functional/test_protocol_handshake.py index a0aa49f..7bc314a 100644 --- a/tests/mumble/functional/test_protocol_handshake.py +++ b/tests/mumble/functional/test_protocol_handshake.py @@ -25,7 +25,7 @@ def test_handshake_completes_with_channel_presence(live_app): assert r["server_version"] is not None, "server did not send a Version message" assert r["auth_accepted"], f"authentication not accepted — {r.get('error')}" # Channel presence: the server must expose at least the root channel (beyond a bare TCP open). - assert len(r["channels"]) >= 1, ( - f"server reported no channels (expected >=1 root channel) — {r!r}" - ) + assert ( + len(r["channels"]) >= 1 + ), f"server reported no channels (expected >=1 root channel) — {r!r}" assert r["server_sync"], f"ServerSync handshake did not complete — {r.get('error')}" diff --git a/tests/mumble/functional/test_server_config_limits.py b/tests/mumble/functional/test_server_config_limits.py index a6e2fbb..c84f2aa 100644 --- a/tests/mumble/functional/test_server_config_limits.py +++ b/tests/mumble/functional/test_server_config_limits.py @@ -32,6 +32,7 @@ def test_configured_max_users_surfaces_in_serverconfig(live_app): ) # allow_html defaults true in the recipe; assert it is present/boolean to prove the field set # is the real ServerConfig (not an empty/garbled decode). - assert cfg.get("allow_html") in (0, 1), ( - f"ServerConfig.allow_html unexpected: {cfg.get('allow_html')!r}" - ) + assert cfg.get("allow_html") in ( + 0, + 1, + ), f"ServerConfig.allow_html unexpected: {cfg.get('allow_html')!r}" diff --git a/tests/mumble/recipe_meta.py b/tests/mumble/recipe_meta.py index 1b99031..228b5b9 100644 --- a/tests/mumble/recipe_meta.py +++ b/tests/mumble/recipe_meta.py @@ -25,10 +25,10 @@ # WELCOME_TEXT -> MUMBLE_CONFIG_WELCOMETEXT, surfaced in the ServerSync welcome_text. # USERS -> MUMBLE_CONFIG_USERS (max users), surfaced in the ServerConfig.max_users. -HEALTH_PATH = "/" # mumble-web client UI (present on both 0.2.0 base and 1.0.0 latest) +HEALTH_PATH = "/" # mumble-web client UI (present on both 0.2.0 base and 1.0.0 latest) HEALTH_OK = (200,) -DEPLOY_TIMEOUT = 900 # two images to pull (mumble-server + mumble-web) on a cold node +DEPLOY_TIMEOUT = 900 # two images to pull (mumble-server + mumble-web) on a cold node HTTP_TIMEOUT = 300 # A unique, stable welcome-text marker the round-trip test asserts surfaces over the protocol. diff --git a/tests/mumble/test_backup.py b/tests/mumble/test_backup.py index 334f251..40c051a 100644 --- a/tests/mumble/test_backup.py +++ b/tests/mumble/test_backup.py @@ -23,6 +23,6 @@ def _sqlite(domain, sql): def test_backup_captures_state(live_app): - assert _sqlite(live_app, "SELECT v FROM ci_marker;") == "original", ( - "the seeded mumble sqlite marker was not present at backup time" - ) + assert ( + _sqlite(live_app, "SELECT v FROM ci_marker;") == "original" + ), "the seeded mumble sqlite marker was not present at backup time" diff --git a/tests/mumble/test_restore.py b/tests/mumble/test_restore.py index 6cfa458..9606ad5 100644 --- a/tests/mumble/test_restore.py +++ b/tests/mumble/test_restore.py @@ -25,6 +25,6 @@ def _sqlite(domain, sql): def test_restore_returns_state(live_app): - assert _sqlite(live_app, "SELECT v FROM ci_marker;") == "original", ( - "restore did not return the pre-mutation mumble sqlite marker (data-integrity failure)" - ) + assert ( + _sqlite(live_app, "SELECT v FROM ci_marker;") == "original" + ), "restore did not return the pre-mutation mumble sqlite marker (data-integrity failure)" diff --git a/tests/n8n/functional/test_login_state.py b/tests/n8n/functional/test_login_state.py index 7e6fbe7..4768442 100644 --- a/tests/n8n/functional/test_login_state.py +++ b/tests/n8n/functional/test_login_state.py @@ -91,6 +91,6 @@ def test_login_endpoint_returns_json(live_app): assert body is not None, f"/rest/login returned no parseable JSON: state={state}" # If it's a dict, it's the expected envelope; if it's a list, n8n shouldn't do that on this # endpoint, but accept either; only reject obvious non-shapes. - assert isinstance(body, (dict, list)), ( - f"/rest/login returned unexpected JSON type {type(body).__name__}: {body!r}" - ) + assert isinstance( + body, dict | list + ), f"/rest/login returned unexpected JSON type {type(body).__name__}: {body!r}" diff --git a/tests/n8n/functional/test_rest_settings.py b/tests/n8n/functional/test_rest_settings.py index 792a730..bb459fa 100644 --- a/tests/n8n/functional/test_rest_settings.py +++ b/tests/n8n/functional/test_rest_settings.py @@ -72,9 +72,9 @@ def test_rest_settings_returns_json_with_known_keys(live_app): # (e.g. version 3.2.0+2.20.6). assert isinstance(body, dict), f"/rest/settings returned non-dict JSON: {type(body).__name__}" data = body.get("data") if "data" in body else body - assert isinstance(data, dict), ( - f"/rest/settings response missing 'data' envelope: keys={list(body.keys())[:10]}" - ) + assert isinstance( + data, dict + ), f"/rest/settings response missing 'data' envelope: keys={list(body.keys())[:10]}" # Bootstrap keys the editor SPA relies on across versions: # - `userManagement`: the auth-mode dict (whether owner-setup is needed, smtp/email mode). # - `defaultLocale`: i18n bootstrap; present on every n8n install. diff --git a/tests/n8n/functional/test_workflow_roundtrip.py b/tests/n8n/functional/test_workflow_roundtrip.py index d408696..ba8d866 100644 --- a/tests/n8n/functional/test_workflow_roundtrip.py +++ b/tests/n8n/functional/test_workflow_roundtrip.py @@ -136,7 +136,9 @@ def _get_workflow(domain: str, cookie_header: str, workflow_id) -> dict: return json.loads(resp.read()) except urllib.error.HTTPError as e: body = e.read().decode(errors="replace") - raise AssertionError(f"GET /rest/workflows/{workflow_id} HTTP {e.code}: {body[:200]}") from e + raise AssertionError( + f"GET /rest/workflows/{workflow_id} HTTP {e.code}: {body[:200]}" + ) from e def test_workflow_create_and_read_back(live_app): @@ -172,18 +174,21 @@ def test_workflow_create_and_read_back(live_app): # Read it back and prove the round-trip fetched = _get_workflow(domain, cookie, workflow_id) fpayload = fetched.get("data") if isinstance(fetched.get("data"), dict) else fetched - assert fpayload.get("id") in (workflow_id, str(workflow_id)), ( - f"GET workflow id={fpayload.get('id')!r} != created id={workflow_id!r}" - ) - assert fpayload.get("name") == name, ( - f"workflow name didn't round-trip: created={name!r}, fetched={fpayload.get('name')!r}" - ) + assert fpayload.get("id") in ( + workflow_id, + str(workflow_id), + ), f"GET workflow id={fpayload.get('id')!r} != created id={workflow_id!r}" + assert ( + fpayload.get("name") == name + ), f"workflow name didn't round-trip: created={name!r}, fetched={fpayload.get('name')!r}" nodes = fpayload.get("nodes") or [] - assert isinstance(nodes, list) and len(nodes) == 1, ( - f"workflow nodes didn't round-trip: expected 1 node, got {len(nodes)}" - ) + assert ( + isinstance(nodes, list) and len(nodes) == 1 + ), f"workflow nodes didn't round-trip: expected 1 node, got {len(nodes)}" node = nodes[0] - assert node.get("type") == "n8n-nodes-base.manualTrigger", ( - f"node type didn't round-trip: {node.get('type')!r}" - ) - assert node.get("name") == "Manual Trigger", f"node name didn't round-trip: {node.get('name')!r}" + assert ( + node.get("type") == "n8n-nodes-base.manualTrigger" + ), f"node type didn't round-trip: {node.get('type')!r}" + assert ( + node.get("name") == "Manual Trigger" + ), f"node name didn't round-trip: {node.get('name')!r}" diff --git a/tests/n8n/test_install.py b/tests/n8n/test_install.py index 9d614cd..3e6885a 100644 --- a/tests/n8n/test_install.py +++ b/tests/n8n/test_install.py @@ -8,7 +8,8 @@ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) -from harness import browser as harness_browser, generic, lifecycle # noqa: E402 +from harness import browser as harness_browser # noqa: E402 +from harness import generic, lifecycle def test_serving_and_editor(live_app, meta): @@ -33,9 +34,10 @@ def test_serving_and_editor(live_app, meta): ctx = browser.new_context(ignore_https_errors=True) page = ctx.new_page() resp = harness_browser.goto_with_retry(page, url, accept_statuses=(200, 304)) - assert resp is not None and resp.status in (200, 304), ( - f"page status {resp and resp.status}" - ) + assert resp is not None and resp.status in ( + 200, + 304, + ), f"page status {resp and resp.status}" body = page.content().lower() assert "n8n" in body or "/ URLs assert "![cc-ci result card](https://ci.example/runs/42/summary.png)" in b assert "https://ci.example/runs/42/badge.svg" in b - assert "(https://drone.x/cc-ci/42)" in b # links to the run + assert "(https://drone.x/cc-ci/42)" in b # links to the run def test_result_comment_text_fallback_when_card_missing(monkeypatch): # Render failed / not served → MUST degrade to text, never a broken image (R7). monkeypatch.setattr(bridge, "artifact_available", lambda url: False) - b = bridge.result_comment_body("hedgedoc", "abc1234def", "9", "https://drone.x/cc-ci/9", "failure") - assert "summary.png" not in b # no image embed - assert "![" not in b # no markdown image at all + b = bridge.result_comment_body( + "hedgedoc", "abc1234def", "9", "https://drone.x/cc-ci/9", "failure" + ) + assert "summary.png" not in b # no image embed + assert "![" not in b # no markdown image at all assert "❌" in b and "failure" in b assert "https://drone.x/cc-ci/9" in b def test_find_existing_comment_matches_marker(monkeypatch): - monkeypatch.setattr(bridge, "list_comments", lambda fn, n: [ - {"id": 1, "body": "just a normal comment"}, - {"id": 2, "body": bridge.COMMENT_MARKER + "\n🌻 old run"}, - ]) + monkeypatch.setattr( + bridge, + "list_comments", + lambda fn, n: [ + {"id": 1, "body": "just a normal comment"}, + {"id": 2, "body": bridge.COMMENT_MARKER + "\n🌻 old run"}, + ], + ) assert bridge.find_existing_comment("org/repo", 5) == 2 diff --git a/tests/unit/test_canonical.py b/tests/unit/test_canonical.py index f25dd77..d31a193 100644 --- a/tests/unit/test_canonical.py +++ b/tests/unit/test_canonical.py @@ -48,7 +48,9 @@ def test_is_enrolled_reads_flag(tmp_path, monkeypatch): def test_registry_roundtrip(tmp_path, monkeypatch): monkeypatch.setenv("CCCI_WARM_ROOT", str(tmp_path)) assert canonical.read_registry("custom-html") is None - rec = canonical.write_registry("custom-html", version="1.10.0+x", commit="abc123", status="idle") + rec = canonical.write_registry( + "custom-html", version="1.10.0+x", commit="abc123", status="idle" + ) assert rec["domain"] == "warm-custom-html.ci.commoninternet.net" assert rec["version"] == "1.10.0+x" and rec["commit"] == "abc123" and rec["status"] == "idle" back = canonical.read_registry("custom-html") @@ -66,9 +68,11 @@ def test_enrolled_recipes_scans_meta(tmp_path, monkeypatch): fake_harness = tmp_path / "runner" / "harness" fake_harness.mkdir(parents=True) monkeypatch.setattr(canonical, "__file__", str(fake_harness / "canonical.py")) - for name, body in (("aaa", "WARM_CANONICAL = True\n"), - ("bbb", "DEPS=['x']\n"), - ("ccc", "WARM_CANONICAL = True\n")): + for name, body in ( + ("aaa", "WARM_CANONICAL = True\n"), + ("bbb", "DEPS=['x']\n"), + ("ccc", "WARM_CANONICAL = True\n"), + ): d = tmp_path / "tests" / name d.mkdir(parents=True) (d / "recipe_meta.py").write_text(body) @@ -85,12 +89,13 @@ def test_prune_stale_drops_deenrolled_only(tmp_path, monkeypatch): # enrolled canonical (keep), de-enrolled canonical (prune), reconciler dir (keep), alerts (keep) for name in ("keepme", "gone"): (tmp_path / name).mkdir() - (tmp_path / name / "canonical.json").write_text('{"recipe":"%s"}' % name) - (tmp_path / "keycloak").mkdir(); (tmp_path / "keycloak" / "last_good").write_text("v1") # reconciler + (tmp_path / name / "canonical.json").write_text(f'{{"recipe":"{name}"}}') + (tmp_path / "keycloak").mkdir() + (tmp_path / "keycloak" / "last_good").write_text("v1") # reconciler (tmp_path / "alerts").mkdir() pruned = canonical.prune_stale() assert pruned == ["gone"] assert not (tmp_path / "gone").exists() assert (tmp_path / "keepme").exists() - assert (tmp_path / "keycloak").exists() # no canonical.json → not a canonical → kept + assert (tmp_path / "keycloak").exists() # no canonical.json → not a canonical → kept assert (tmp_path / "alerts").exists() diff --git a/tests/unit/test_dashboard.py b/tests/unit/test_dashboard.py index 944f3e7..c1b17ff 100644 --- a/tests/unit/test_dashboard.py +++ b/tests/unit/test_dashboard.py @@ -12,9 +12,8 @@ import os import sys import tempfile -_tok = tempfile.NamedTemporaryFile("w", delete=False, suffix=".tok") -_tok.write("test-token") -_tok.close() +with tempfile.NamedTemporaryFile("w", delete=False, suffix=".tok") as _tok: + _tok.write("test-token") os.environ["DRONE_TOKEN_FILE"] = _tok.name sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "dashboard")) @@ -23,10 +22,17 @@ import dashboard # noqa: E402 def _row(**kw): base = { - "recipe": "custom-html", "status": "success", "number": 4, "ref": "db9a9502", - "version": "db9a95024e9d", "level": 4, "level_cap_reason": "", - "has_screenshot": True, "flags": {"clean_teardown": True, "no_secret_leak": True}, - "finished": 0, "url": "https://drone.x/cc-ci/4", + "recipe": "custom-html", + "status": "success", + "number": 4, + "ref": "db9a9502", + "version": "db9a95024e9d", + "level": 4, + "level_cap_reason": "", + "has_screenshot": True, + "flags": {"clean_teardown": True, "no_secret_leak": True}, + "finished": 0, + "url": "https://drone.x/cc-ci/4", } base.update(kw) return base @@ -43,24 +49,33 @@ def test_level_color_ramp_and_fallback(): def test_overview_grid_mirrors_results(): out = dashboard.render_overview([_row()]) assert "custom-html" in out - assert "level 4" in out # the corner level pill - assert dashboard.level_color(4) in out # coloured by level - assert "db9a95024e9d" in out # version from results.json - assert "/runs/4/screenshot.png" in out # thumbnail - assert "/runs/4/summary.png" in out # links to full card - assert "/recipe/custom-html" in out # history link + assert "level 4" in out # the corner level pill + assert dashboard.level_color(4) in out # coloured by level + assert "db9a95024e9d" in out # version from results.json + assert "/runs/4/screenshot.png" in out # thumbnail + assert "/runs/4/summary.png" in out # links to full card + assert "/recipe/custom-html" in out # history link assert "✔ teardown" in out and "✔ no-leak" in out def test_overview_never_greener_than_data(): # A failed run at level 0 must show level 0 + the failure pill — never a green/high level. - out = dashboard.render_overview([_row(status="failure", level=0, has_screenshot=False, - flags={}, level_cap_reason="L1 install FAILED")]) + out = dashboard.render_overview( + [ + _row( + status="failure", + level=0, + has_screenshot=False, + flags={}, + level_cap_reason="L1 install FAILED", + ) + ] + ) assert "level 0" in out - assert dashboard.level_color(0) in out # red + assert dashboard.level_color(0) in out # red assert dashboard._COLORS["failure"] in out assert "level 4" not in out and "level 5" not in out and "level 6" not in out - assert "no screenshot" in out # placeholder, no broken image + assert "no screenshot" in out # placeholder, no broken image def test_level_pill_unknown_when_no_results(): @@ -74,7 +89,7 @@ def test_history_table_lists_runs(): assert "#4" in out and "#3" in out assert "L4" in out and "L2" in out assert "← all recipes" in out - assert "/runs/4/summary.png" in out # per-run card link + assert "/runs/4/summary.png" in out # per-run card link def test_history_empty(): @@ -83,12 +98,24 @@ def test_history_empty(): def test_build_row_projects_results(monkeypatch): - monkeypatch.setattr(dashboard, "_results_for", lambda n: { - "version": "1.2.3", "level": 2, "level_cap_reason": "cap", - "screenshot": "screenshot.png", "flags": {"clean_teardown": True}, - }) - b = {"number": 7, "status": "success", "event": "custom", - "params": {"RECIPE": "n8n", "REF": "abcdef1234567890"}, "finished": 10} + monkeypatch.setattr( + dashboard, + "_results_for", + lambda n: { + "version": "1.2.3", + "level": 2, + "level_cap_reason": "cap", + "screenshot": "screenshot.png", + "flags": {"clean_teardown": True}, + }, + ) + b = { + "number": 7, + "status": "success", + "event": "custom", + "params": {"RECIPE": "n8n", "REF": "abcdef1234567890"}, + "finished": 10, + } r = dashboard._build_row(b) assert r["recipe"] == "n8n" and r["number"] == 7 assert r["level"] == 2 and r["version"] == "1.2.3" @@ -99,11 +126,16 @@ def test_build_row_projects_results(monkeypatch): def test_build_row_degrades_without_results(monkeypatch): # No results.json (e.g. an old run): grid still renders from Drone fields, level absent. monkeypatch.setattr(dashboard, "_results_for", lambda n: {}) - b = {"number": 9, "status": "running", "event": "custom", - "params": {"RECIPE": "ghost", "REF": "deadbeefcafe1234567890"}, "finished": 0} + b = { + "number": 9, + "status": "running", + "event": "custom", + "params": {"RECIPE": "ghost", "REF": "deadbeefcafe1234567890"}, + "finished": 0, + } r = dashboard._build_row(b) assert r["level"] is None and r["has_screenshot"] is False - assert r["version"] == "deadbeefcafe" # ref[:12] fallback + assert r["version"] == "deadbeefcafe" # ref[:12] fallback # render must not crash or claim a level assert "level —" in dashboard.render_overview([r]) @@ -111,7 +143,7 @@ def test_build_row_degrades_without_results(monkeypatch): def test_level_badge_shows_level_coloured(monkeypatch): svg = dashboard.render_level_badge("custom-html", 4) assert "custom-html" in svg and "level 4" in svg - assert dashboard.level_color(4) in svg # coloured by level + assert dashboard.level_color(4) in svg # coloured by level assert svg.startswith("
RunStatusLevelVersionWhenCard
' - f'{html.escape(RUNG_LABEL.get(rung, rung))}intentional skip
{html.escape(reason)}
{html.escape(reason)}
' - f'{html.escape(RUNG_LABEL.get(rung, rung))}unintentional skip