From c5015ce255e8ec80d872a0827d040ab8c1e69deb Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 28 Sep 2026 10:32:45 +0200 Subject: [PATCH] =?UTF-8?q?harness:=20claper=20fixture=20=E2=80=94=20its?= =?UTF-8?q?=20own=20release=20CLI=20(rpc=20register=5Fuser=20/=20authentic?= =?UTF-8?q?ate),=20proven=20on=209202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- scripts/upgrade_fixtures_box.py | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/scripts/upgrade_fixtures_box.py b/scripts/upgrade_fixtures_box.py index 336723c..ecd290b 100644 --- a/scripts/upgrade_fixtures_box.py +++ b/scripts/upgrade_fixtures_box.py @@ -376,6 +376,61 @@ class Django: return found is True +# ============================================================================================= +class Claper: + """Claper's OWN release CLI inside its own container: `bin/claper rpc` runs Elixir code in the + RUNNING node, against the app's own `Claper.Accounts` context (its changeset, its password hash). + Not SQL, not a planted file (R-156). `eval` would boot a second, app-less VM — `rpc` asks the + live one. Measured on 9202 2026-09-28 (claper 2.5.1, PostgreSQL 16): register_user → {:ok, id=2}; + the readback authenticated with the right password and REFUSED a wrong one. + + THE FIXTURE PROVES ITSELF ON EVERY CALL: verify() also asks the same function with a password + that was never set and requires False — a readback that has broken into "always found" fails. + """ + container = "claper" + + def _rpc(self, w, code): + # ELIXIR_ERL_OPTIONS=+fnu: the release otherwise warns about latin1 on every call (noise only). + out = w.guest(f"docker exec -e ELIXIR_ERL_OPTIONS=+fnu {self.container} /app/bin/claper rpc " + f"{json.dumps(code)} 2>&1", timeout=240) + return " ".join(out.split()) + + def _auth(self, w, email, pw): + out = self._rpc(w, f'IO.puts("DRILL_ANSWER=#{{Claper.Accounts.get_user_by_email_and_password({json.dumps(email)}, {json.dumps(pw)}) != nil}}")') + m = re.search(r"DRILL_ANSWER=(true|false)", out) + return (m.group(1) == "true") if m else None, out[-300:] + + def seed(self, w, sub, say): + if not w.wait_app(sub, "/", want=("200", "302"), tries=90): + return None + email = f"drill-{secrets.token_hex(4)}@gate.invalid" + pw = "Drill-" + secrets.token_hex(10) + out = self._rpc(w, ('case Claper.Accounts.register_user(%{email: ' + json.dumps(email) + ', password: ' + + json.dumps(pw) + '}) do {:ok, u} -> IO.puts("DRILL_SEEDED=#{u.id}"); ' + '{:error, cs} -> IO.inspect(cs.errors, label: "DRILL_REFUSED") end')) + say(f" claper: register_user :: {out[-160:]}") + got, detail = self._auth(w, email, pw) + if got is not True: + say(f" claper: the account does not authenticate in the app's own context :: {detail[:200]}") + return None + say(f" claper: seeded user {email}") + return {"email": email, "pw": pw} + + def verify(self, w, sub, t, say): + if not w.wait_app(sub, "/", want=("200", "302"), tries=90): + say(" claper: the app never served /") + return False + wrong, detail = self._auth(w, t["email"], "definitely-" + secrets.token_hex(8)) + if wrong is not False: + say(f" claper: READBACK UNUSABLE — a wrong password did not read as refused ({wrong}) :: {detail[:200]}") + return False + ok, detail = self._auth(w, t["email"], t["pw"]) + say(f" claper: readback — the seeded account authenticates={ok}") + if ok is not True: + say(f" claper: :: {detail[:250]}") + return ok is True + + # ============================================================================================= class Nextcloud: """Nextcloud's OWN admin CLI, `occ`, inside its own container: its own code, its own user @@ -1089,4 +1144,5 @@ FIXTURES = { "navidrome": Navidrome(), "vaultwarden": Vaultwarden(), "wishlist": Wishlist(), + "claper": Claper(), }