# PORTED 2026-09-23 (night shift, R-462) VERBATIM from felhom.eu/documentation/audits/the-28-2026-09-22/ # fixtures28.py. See upgrade_fixtures_box.py. #!/usr/bin/env python3 """fixtures28.py — seed/verify for the twenty-eight, same rule as `fixtures.py` (R-156). *Nothing is ever seeded into a volume by hand.* Every seed goes in through the app's OWN interface: its HTTP API through the household's real front door, or its own CLI inside its own container. A raw SQL INSERT or a planted file is never used. An app with no non-browser route returns None from `seed()` and carries a `tried` string naming what was attempted. That is a RESULT — `inconclusive` — not a gap to be papered over. Every `verify()` that can prove itself does so on the same call: it also asks for something that MUST be absent, so a readback that has broken into always answering "found" fails instead of passing everything. """ import json, re, secrets def _gx(w, container, *cmd, timeout=240): import shlex return w.guest(f"docker exec {container} " + " ".join(shlex.quote(c) for c in cmd) + " 2>&1", timeout=timeout) # ── the *arr family: their own v3 API, key read from their own config ──────────────────────────── class _Arr: """radarr / sonarr. The API key is minted by the app into its own config.xml; reading it is how a household's own client authenticates, and the tag endpoints are ordinary app data.""" api = "v3" def _key(self, w): out = w.guest(f"docker exec {self.name} cat /config/config.xml 2>/dev/null") m = re.search(r"([0-9a-f]+)", out or "") return m.group(1) if m else None def seed(self, w, sub, say): if not w.wait_app(sub, "/", want=("200", "302", "401")): return None k = self._key(w) if not k: self.tried = "read ApiKey from the app's own /config/config.xml — not present yet" say(f" {self.name}: no ApiKey in config.xml yet") return None label = "drill" + secrets.token_hex(4) rc, code, out = w.app_curl(sub, f"/api/{self.api}/tag", "-H", f"X-Api-Key: {k}", "-H", "Content-Type: application/json", data=json.dumps({"label": label}), method="POST") if code not in ("200", "201", "202"): self.tried = f"POST /api/{self.api}/tag with the app's own key -> {code}" say(f" {self.name}: POST tag -> {code} {out[:150]}") return None say(f" {self.name}: seeded tag {label}") return {"label": label, "key": k} def verify(self, w, sub, t, say): k = self._key(w) or t["key"] rc, code, out = w.app_curl(sub, f"/api/{self.api}/tag", "-H", f"X-Api-Key: {k}") found = t["label"] in (out or "") # negative control, EVERY call: a label that cannot exist must read as absent absent = ("drillnope" + secrets.token_hex(6)) not in (out or "") if not absent: say(f" {self.name}: READBACK UNUSABLE — an impossible label read as present") return None say(f" {self.name}: readback found={found} (http {code}, control passed)") return found class Radarr(_Arr): name = "radarr"; sub = "radarr"; route = "its own /api/v3/tag with the app's own ApiKey" class Sonarr(_Arr): name = "sonarr"; sub = "sonarr"; route = "its own /api/v3/tag with the app's own ApiKey" # ── kimai — its own console, the route the app documents ───────────────────────────────────────── class Kimai: sub = "kimai"; route = "its own `bin/console kimai:user:create`" def seed(self, w, sub, say): u = "drill" + secrets.token_hex(4) out = _gx(w, "kimai", "/opt/kimai/bin/console", "kimai:user:create", u, f"{u}@example.invalid", "ROLE_USER", "Drill-" + secrets.token_hex(6) + "!aA") if "success" not in (out or "").lower() and "created" not in (out or "").lower(): self.tried = "its own `bin/console kimai:user:create` -> " + (out or "")[:200] say(f" kimai: console create said: {(out or '')[:200]}") return None say(f" kimai: seeded user {u}") return {"user": u} def verify(self, w, sub, t, say): out = _gx(w, "kimai", "/opt/kimai/bin/console", "kimai:user:list") or "" found = t["user"] in out absent = ("nope" + secrets.token_hex(6)) not in out if not absent: say(" kimai: READBACK UNUSABLE — an impossible user read as present") return None say(f" kimai: readback found={found} (control passed)") return found # ── gramps-web — its own CLI ───────────────────────────────────────────────────────────────────── class GrampsWeb: sub = "gramps"; route = "its own `python3 -m gramps_webapi user add`" def seed(self, w, sub, say): u = "drill" + secrets.token_hex(4) out = _gx(w, "gramps-web", "python3", "-m", "gramps_webapi", "--config", "/app/config/config.cfg", "user", "add", u, "Drill-" + secrets.token_hex(6)) if "error" in (out or "").lower() or "traceback" in (out or "").lower(): self.tried = "its own `gramps_webapi user add` -> " + (out or "")[:200] say(f" gramps-web: {(out or '')[:200]}") return None say(f" gramps-web: seeded user {u}") return {"user": u} def verify(self, w, sub, t, say): out = _gx(w, "gramps-web", "python3", "-m", "gramps_webapi", "--config", "/app/config/config.cfg", "user", "list") or "" found = t["user"] in out absent = ("nope" + secrets.token_hex(6)) not in out if not absent: say(" gramps-web: READBACK UNUSABLE") return None say(f" gramps-web: readback found={found} (control passed)") return found # ── homebox — its own registration + item API ──────────────────────────────────────────────────── class Homebox: sub = "homebox"; route = "its own /api/v1/users/register + /api/v1/locations" def seed(self, w, sub, say): if not w.wait_app(sub, "/", want=("200", "302")): return None u = "drill" + secrets.token_hex(4) + "@example.invalid" pw = "Drill-" + secrets.token_hex(8) + "!aA" rc, code, out = w.app_curl(sub, "/api/v1/users/register", "-H", "Content-Type: application/json", data=json.dumps({"name": "drill", "email": u, "password": pw}), method="POST") if code not in ("200", "201", "204"): self.tried = f"POST /api/v1/users/register -> {code} {out[:150]}" say(f" homebox: register -> {code} {out[:150]}") return None rc, code, out = w.app_curl(sub, "/api/v1/users/login", "-H", "Content-Type: application/json", data=json.dumps({"username": u, "password": pw}), method="POST") try: tokv = json.loads(out)["token"] except Exception: self.tried = f"POST /api/v1/users/login -> {code} {out[:150]}" say(f" homebox: login -> {code} {out[:150]}") return None name = "drillloc" + secrets.token_hex(4) rc, code, out = w.app_curl(sub, "/api/v1/locations", "-H", f"Authorization: {tokv}", "-H", "Content-Type: application/json", data=json.dumps({"name": name, "description": "drill"}), method="POST") if code not in ("200", "201"): self.tried = f"POST /api/v1/locations -> {code} {out[:150]}" say(f" homebox: create location -> {code} {out[:150]}") return None say(f" homebox: seeded location {name}") return {"name": name, "tok": tokv, "u": u, "pw": pw} def verify(self, w, sub, t, say): rc, code, out = w.app_curl(sub, "/api/v1/users/login", "-H", "Content-Type: application/json", data=json.dumps({"username": t["u"], "password": t["pw"]}), method="POST") try: tokv = json.loads(out)["token"] except Exception: tokv = t["tok"] rc, code, out = w.app_curl(sub, "/api/v1/locations", "-H", f"Authorization: {tokv}") found = t["name"] in (out or "") absent = ("nope" + secrets.token_hex(6)) not in (out or "") if not absent: say(" homebox: READBACK UNUSABLE") return None say(f" homebox: readback found={found} (http {code}, control passed)") return found FIXTURES28 = { "radarr": Radarr(), "sonarr": Sonarr(), "kimai": Kimai(), "gramps-web": GrampsWeb(), "homebox": Homebox(), } # ── apps whose front door is a SIGN-UP or SETUP call ───────────────────────────────────────────── def _neg(w, sub, path, hdr, say, name): """The negative control every verify() runs: something that CANNOT exist must read absent.""" rc, code, out = w.app_curl(sub, path, *hdr) return ("nope" + secrets.token_hex(6)) not in (out or ""), out, code class Termix: sub = "termix"; route = "its own /users/create sign-up, then /users/me" def seed(self, w, sub, say): if not w.wait_app(sub, "/", want=("200", "302")): return None u = "drill" + secrets.token_hex(4) pw = "Drill-" + secrets.token_hex(8) + "!aA" for p in ("/users/create", "/api/users/create", "/users/register"): rc, code, out = w.app_curl(sub, p, "-H", "Content-Type: application/json", data=json.dumps({"username": u, "password": pw}), method="POST") if code in ("200", "201"): say(f" termix: seeded user {u} via {p}") return {"u": u, "pw": pw, "path": p} self.tried = "POST /users/create, /api/users/create, /users/register — none accepted" say(f" termix: no sign-up route accepted (last {code} {out[:120]})") return None def verify(self, w, sub, t, say): rc, code, out = w.app_curl(sub, "/users/login", "-H", "Content-Type: application/json", data=json.dumps({"username": t["u"], "password": t["pw"]}), method="POST") found = code in ("200", "201") and ("token" in (out or "") or t["u"] in (out or "")) rc2, code2, out2 = w.app_curl(sub, "/users/login", "-H", "Content-Type: application/json", data=json.dumps({"username": "nope" + secrets.token_hex(6), "password": t["pw"]}), method="POST") if code2 in ("200", "201"): say(" termix: READBACK UNUSABLE — an impossible user logged in") return None say(f" termix: readback found={found} (http {code}, control refused as it must)") return found class Ghost: sub = "blog"; route = "its own /ghost/api/admin/authentication/setup/" def seed(self, w, sub, say): if not w.wait_app(sub, "/", want=("200", "301", "302")): return None title = "Drill-" + secrets.token_hex(6) u = "drill" + secrets.token_hex(4) + "@example.invalid" pw = "Drill-" + secrets.token_hex(8) + "aA1" body = json.dumps({"setup": [{"name": "Drill", "email": u, "password": pw, "blogTitle": title}]}) rc, code, out = w.app_curl(sub, "/ghost/api/admin/authentication/setup/", "-H", "Content-Type: application/json", "-H", "Accept-Version: v5.0", data=body, method="POST") if code not in ("200", "201"): self.tried = f"POST /ghost/api/admin/authentication/setup/ -> {code} {out[:150]}" say(f" ghost: setup -> {code} {out[:160]}") return None say(f" ghost: seeded site title {title}") return {"title": title, "u": u} def verify(self, w, sub, t, say): rc, code, out = w.app_curl(sub, "/", "-L") found = t["title"] in (out or "") absent = ("Drill-nope" + secrets.token_hex(6)) not in (out or "") if not absent: say(" ghost: READBACK UNUSABLE") return None say(f" ghost: readback found={found} (http {code}, control passed)") return found class Komga: sub = "komga"; route = "its own POST /api/v1/claim, then GET /api/v2/users/me" def seed(self, w, sub, say): if not w.wait_app(sub, "/", want=("200", "302", "401")): return None u = "drill" + secrets.token_hex(4) + "@example.invalid" pw = "Drill-" + secrets.token_hex(8) rc, code, out = w.app_curl(sub, "/api/v1/claim", "-H", f"X-Komga-Email: {u}", "-H", f"X-Komga-Password: {pw}", method="POST") if code not in ("200", "201"): self.tried = f"POST /api/v1/claim -> {code} {out[:150]}" say(f" komga: claim -> {code} {out[:150]}") return None say(f" komga: claimed the server as {u}") return {"u": u, "pw": pw} def verify(self, w, sub, t, say): import base64 as _b a = _b.b64encode(f"{t['u']}:{t['pw']}".encode()).decode() rc, code, out = w.app_curl(sub, "/api/v2/users/me", "-H", f"Authorization: Basic {a}") # v2 since komga 1.x; v1 answers 404 (measured 2026-09-23) found = code == "200" and t["u"] in (out or "") bad = _b.b64encode(f"nope{secrets.token_hex(6)}:{t['pw']}".encode()).decode() rc2, code2, _ = w.app_curl(sub, "/api/v2/users/me", "-H", f"Authorization: Basic {bad}") if code2 == "200": say(" komga: READBACK UNUSABLE — an impossible user authenticated") return None say(f" komga: readback found={found} (http {code}, control refused {code2})") return found class Immich: sub = "photos"; route = "its own /api/auth/admin-sign-up, then an album" def seed(self, w, sub, say): if not w.wait_app(sub, "/", want=("200", "302"), tries=90): return None u = "drill" + secrets.token_hex(4) + "@example.invalid" pw = "Drill-" + secrets.token_hex(8) rc, code, out = w.app_curl(sub, "/api/auth/admin-sign-up", "-H", "Content-Type: application/json", data=json.dumps({"email": u, "password": pw, "name": "Drill"}), method="POST") if code not in ("200", "201"): self.tried = f"POST /api/auth/admin-sign-up -> {code} {out[:150]}" say(f" immich: sign-up -> {code} {out[:160]}") return None rc, code, out = w.app_curl(sub, "/api/auth/login", "-H", "Content-Type: application/json", data=json.dumps({"email": u, "password": pw}), method="POST") try: at = json.loads(out)["accessToken"] except Exception: self.tried = f"POST /api/auth/login -> {code} {out[:150]}" return None name = "drillalbum" + secrets.token_hex(4) rc, code, out = w.app_curl(sub, "/api/albums", "-H", f"Authorization: Bearer {at}", "-H", "Content-Type: application/json", data=json.dumps({"albumName": name}), method="POST") if code not in ("200", "201"): self.tried = f"POST /api/albums -> {code} {out[:150]}" say(f" immich: album -> {code} {out[:150]}") return None say(f" immich: seeded album {name}") return {"name": name, "u": u, "pw": pw} def verify(self, w, sub, t, say): rc, code, out = w.app_curl(sub, "/api/auth/login", "-H", "Content-Type: application/json", data=json.dumps({"email": t["u"], "password": t["pw"]}), method="POST") try: at = json.loads(out)["accessToken"] except Exception: say(f" immich: could not log back in (http {code})") return False rc, code, out = w.app_curl(sub, "/api/albums", "-H", f"Authorization: Bearer {at}") found = t["name"] in (out or "") absent = ("nope" + secrets.token_hex(6)) not in (out or "") if not absent: say(" immich: READBACK UNUSABLE") return None say(f" immich: readback found={found} (http {code}, control passed)") return found class _MediaServer: """jellyfin / emby — the startup wizard IS the front door on a fresh install.""" def seed(self, w, sub, say): if not w.wait_app(sub, "/", want=("200", "302"), tries=90): return None u = "drill" + secrets.token_hex(4) pw = "Drill-" + secrets.token_hex(8) rc, code, out = w.app_curl(sub, "/Startup/User", "-H", "Content-Type: application/json", data=json.dumps({"Name": u, "Password": pw}), method="POST") if code not in ("200", "204"): self.tried = f"POST /Startup/User -> {code} {out[:150]}" say(f" {self.name}: /Startup/User -> {code} {out[:150]}") return None w.app_curl(sub, "/Startup/Complete", method="POST") say(f" {self.name}: seeded first user {u}") return {"u": u} def verify(self, w, sub, t, say): rc, code, out = w.app_curl(sub, "/Users/Public") found = t["u"] in (out or "") absent = ("nope" + secrets.token_hex(6)) not in (out or "") if not absent: say(f" {self.name}: READBACK UNUSABLE") return None say(f" {self.name}: readback found={found} (http {code}, control passed)") return found class Jellyfin(_MediaServer): name = "jellyfin"; sub = "jellyfin"; route = "its own /Startup/User wizard, then /Users/Public" class Emby(_MediaServer): name = "emby"; sub = "emby"; route = "its own /Startup/User wizard, then /Users/Public" class NoRoute: """An app whose only way in is a browser. The fixture RUNS, states what it tried, and returns None. `inconclusive` with the attempts named is a result; a blank is not.""" def __init__(self, name, sub, tried): self.name, self.sub, self.tried = name, sub, tried self.route = "none — " + tried def seed(self, w, sub, say): w.wait_app(sub, "/", want=("200", "301", "302", "401", "403"), tries=30) say(f" {self.name}: no non-browser seed route — {self.tried}") return None def verify(self, w, sub, t, say): return False FIXTURES28.update({ "termix": Termix(), "ghost": Ghost(), "komga": Komga(), "immich": Immich(), "jellyfin": Jellyfin(), "emby": Emby(), "code-server": NoRoute("code-server", "code", "its front door is a browser IDE behind one " "password; it exposes no data API, and writing a file with docker exec " "would not be the front door (R-156)"), "onlyoffice": NoRoute("onlyoffice", "office", "a stateless document server: it holds no " "household data of its own, so there is nothing to seed"), "homepage": NoRoute("homepage", "home", "a dashboard rendered from config files in the " "template; it stores no household data"), "plex": NoRoute("plex", "plex", "the first-run claim needs a token minted at plex.tv by a " "real Plex account; no account exists for this venue"), # outline and rallly WERE NoRoute here until 2026-09-30: both have a front-door first-run route after all # (outline `installation.create`, rallly sign-up + its own e-mail code) — the fixtures are in upgrade_fixtures_box.py. })