Fixtures: sparkyfitness, rallly and outline seed through their own front door (R-462, R-624)

- Sparkyfitness: better-auth sign-up/sign-in, a check-in weight stored and read back; a wrong
  password and an empty date must read as absent. Waits out the app's own 429 (one client
  address behind traefik).
- Rallly: sign-up, the six-digit e-mail code READ (select only) from the app's own
  verifications row in place of a mailbox, verify-email, sign-in, polls.make, readback by the
  public polls.get; an unknown id must be not found.
- Outline: the self-hosted first-run route installation.create (workspace + admin, refused once a
  team exists), an API key with Outline's own CSRF pair, a document, readback by documents.info;
  an unknown id must 404 and a wrong key 401.
- outline and rallly leave the NoRoute list: both had a front-door route after all.

Measured on the bench (LXC 9401) and on 9202 2026-09-30:
felhom.eu/documentation/audits/pg-last-six-2026-09-30/

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-30 12:53:35 +02:00
parent 6446197925
commit e6f3ec2087
2 changed files with 256 additions and 4 deletions
+254
View File
@@ -1163,7 +1163,261 @@ class Wishlist:
say(f" wishlist: sign-in as the seeded user type={good.get('type')} ok={ok} (wrong password refused)")
return ok
# =============================================================================================
def _set_cookies(out):
"""The `name=value` pairs of every Set-Cookie in a `curl -D -` answer (headers + body), joined for
a Cookie header. Kept in the fixture's own memory only; never printed."""
pairs = re.findall(r"(?im)^set-cookie:\s*([^=;\s]+=[^;\r\n]*)", out or "")
return "; ".join(pairs)
class Sparkyfitness:
"""SparkyFitness's OWN better-auth API: `POST /api/auth/sign-up/email` makes the first account (the
household's own first-run route — the box's sign-up block goes up only AFTER the setup, decision 47),
`POST /api/auth/sign-in/email` gives the session cookie, `POST /api/measurements/check-in` stores a
weight for one date, `GET /api/measurements/check-in/<date>` reads it back. Measured on the bench
2026-09-30 (v0.17.3, PostgreSQL 15): sign-up 200, check-in 200, readback equal, an empty date → `{}`,
a wrong password → 401.
THE FIXTURE PROVES ITSELF ON EVERY CALL: verify() also requires a wrong password to be refused and a
date with no check-in to read back without the weight.
"""
sub = "sparky"
def _signin(self, w, sub, email, pw):
# better-auth rate-limits sign-in per client address (a box's traefik is ONE address): a 429 is waited
# out, never read as a verdict (measured on 9202 2026-09-30: seed sign-in + wrong + right within 1 s → 429).
for _ in range(4):
rc, code, out = w.app_curl(sub, "/api/auth/sign-in/email", "-D", "-", "-H", "Content-Type: application/json",
"-H", f"Origin: https://{sub}.{w.DOMAIN}",
data=json.dumps({"email": email, "password": pw}), method="POST")
if code != "429":
break
time.sleep(15)
return code, _set_cookies(out)
def seed(self, w, sub, say):
if not w.wait_app(sub, "/api/health", want=("200",), tries=120):
if not w.wait_app(sub, "/", want=("200",), tries=30):
return None
email = "drill" + secrets.token_hex(3) + "@gate.invalid"
pw = "Drill-" + secrets.token_hex(10)
weight = round(50 + secrets.randbelow(4000) / 100, 2)
rc, code, out = w.app_curl(sub, "/api/auth/sign-up/email", "-H", "Content-Type: application/json",
"-H", f"Origin: https://{sub}.{w.DOMAIN}",
data=json.dumps({"email": email, "password": pw, "name": "Drill"}), method="POST")
say(f" sparkyfitness: sign-up http={code}")
if code not in ("200", "201"):
self.tried = f"POST /api/auth/sign-up/email -> {code} {out[:120]}"
return None
code, ck = self._signin(w, sub, email, pw)
if code != "200" or not ck:
self.tried = f"POST /api/auth/sign-in/email -> {code}"
return None
rc, code, out = w.app_curl(sub, "/api/measurements/check-in", "-H", f"Cookie: {ck}",
"-H", "Content-Type: application/json", "-H", f"Origin: https://{sub}.{w.DOMAIN}",
data=json.dumps({"entry_date": "2026-09-01", "weight": weight}), method="POST")
say(f" sparkyfitness: check-in http={code}")
if code != "200":
self.tried = f"POST /api/measurements/check-in -> {code} {out[:120]}"
return None
say(f" sparkyfitness: seeded user {email.split('@')[0]} with a weight of {weight} on 2026-09-01")
return {"email": email, "pw": pw, "weight": weight}
def verify(self, w, sub, t, say):
if not w.wait_app(sub, "/", want=("200",), tries=120):
say(" sparkyfitness: the app never served /")
return False
code, _ = self._signin(w, sub, t["email"], "wrong-" + secrets.token_hex(6))
if code == "200":
say(" sparkyfitness: READBACK UNUSABLE — a wrong password signed in")
return False
code, ck = self._signin(w, sub, t["email"], t["pw"])
if code != "200" or not ck:
say(f" sparkyfitness: the seeded user could not sign in (http {code})")
return False
rc, code, out = w.app_curl(sub, "/api/measurements/check-in/1999-01-01", "-H", f"Cookie: {ck}")
if code != "200" or '"weight"' in (out or ""):
say(f" sparkyfitness: READBACK UNUSABLE — an empty date answered {code} {out[:80]}")
return False
rc, code, out = w.app_curl(sub, "/api/measurements/check-in/2026-09-01", "-H", f"Cookie: {ck}")
try:
got = json.loads(out).get("weight")
except Exception:
got = None
say(f" sparkyfitness: readback of the seeded weight http={code} equal={got == t['weight']}")
return got == t["weight"]
class Rallly:
"""Rallly's OWN better-auth API and its own tRPC, the household's route: `POST /api/better-auth/sign-up/email`
makes the account, which then needs the six-digit code Rallly e-mails. **The one step that is not the front
door:** the code is READ (a SELECT, never a write) from the app's own `verifications` row, standing in for the
household's mailbox — this venue has none. The code is then given back through the front door
(`POST /api/better-auth/email-otp/verify-email`), the session comes from `sign-in/email`, and a poll is made
with `polls.make` (tRPC). The readback is the public `polls.get` by the poll's id. Nothing is written by hand
(R-156). Measured on the bench 2026-09-30 (v4.11.1, PostgreSQL 16): sign-up 200 (the mail send fails —
ESOCKET — and the code is stored anyway), verify 200, `polls.make` 200, `polls.get` 200 with the title, an
unknown id → 404 "Poll not found".
THE FIXTURE PROVES ITSELF ON EVERY CALL: verify() also requires an id that cannot exist to be not found.
"""
sub = "poll"
def _auth(self, w, sub, path, body):
return w.app_curl(sub, "/api/better-auth/" + path, "-D", "-", "-H", "Content-Type: application/json",
"-H", f"Origin: https://{sub}.{w.DOMAIN}", data=json.dumps(body), method="POST")
def _get(self, w, sub, poll_id):
q = json.dumps({"json": {"urlId": poll_id}}, separators=(",", ":"))
import urllib.parse
return w.app_curl(sub, "/api/trpc/polls.get?input=" + urllib.parse.quote(q))
def seed(self, w, sub, say):
if not w.wait_app(sub, "/login", want=("200",), tries=90):
return None
email = "drill" + secrets.token_hex(3) + "@gate.invalid"
pw = "Drill-" + secrets.token_hex(10)
rc, code, out = self._auth(w, sub, "sign-up/email", {"email": email, "password": pw, "name": "Drill"})
say(f" rallly: sign-up http={code}")
if code != "200":
self.tried = f"POST /api/better-auth/sign-up/email -> {code}"
return None
otp = ""
for _ in range(10):
otp = w.guest("docker exec rallly-postgres psql -U rallly -d rallly -Atc "
f"\"select split_part(value,':',1) from verifications where identifier="
f"'email-verification-otp-{email}' order by created_at desc limit 1\" 2>&1").strip()
if re.fullmatch(r"\d{6}", otp):
break
time.sleep(2)
if not re.fullmatch(r"\d{6}", otp):
self.tried = "the e-mail code was not in the app's own verifications table"
say(" rallly: no e-mail code found in the app's own table")
return None
rc, code, out = self._auth(w, sub, "email-otp/verify-email", {"email": email, "otp": otp})
say(f" rallly: verify-email with the code http={code}")
if code != "200":
self.tried = f"POST /api/better-auth/email-otp/verify-email -> {code}"
return None
rc, code, out = self._auth(w, sub, "sign-in/email", {"email": email, "password": pw})
ck = _set_cookies(out)
if code != "200" or "session_token" not in ck:
self.tried = f"POST /api/better-auth/sign-in/email -> {code}"
return None
title = "drillpoll-" + secrets.token_hex(4)
rc, code, out = w.app_curl(sub, "/api/trpc/polls.make", "-H", f"Cookie: {ck}", "-H", "Content-Type: application/json",
"-H", f"Origin: https://{sub}.{w.DOMAIN}",
data=json.dumps({"json": {"title": title, "timeZone": "Europe/Budapest",
"options": [{"startDate": "2026-12-01"}]}}), method="POST")
try:
pid = json.loads(out)["result"]["data"]["json"]["data"]["id"]
except Exception:
self.tried = f"POST /api/trpc/polls.make -> {code} {out[:120]}"
say(f" rallly: polls.make -> {code} {out[:120]}")
return None
say(f" rallly: seeded poll {title} ({pid})")
return {"id": pid, "title": title}
def verify(self, w, sub, t, say):
if not w.wait_app(sub, "/login", want=("200",), tries=90):
say(" rallly: the app never served /login")
return False
rc, code, out = self._get(w, sub, "Nope" + secrets.token_hex(4))
if code != "404":
say(f" rallly: READBACK UNUSABLE — an id that cannot exist answered {code}")
return False
rc, code, out = self._get(w, sub, t["id"])
found = code == "200" and t["title"] in (out or "")
say(f" rallly: readback of the seeded poll http={code} found={found}")
return found
class Outline:
"""Outline's OWN first-run API, the household's route while the app holds no workspace:
`POST /api/installation.create` makes the workspace and its admin and signs them in (Outline mounts it
only on self-hosted installs, and refuses it once a team exists). The session makes an API key
(`apiKeys.create`, with Outline's own CSRF cookie + header), the key makes a collection and a published
document, and the readback is `documents.info` with the key. Measured on the bench 2026-09-30 (v1.9.1,
PostgreSQL 16): every call 200, the title and body read back, an unknown id → 404, a wrong key → 401.
THE FIXTURE PROVES ITSELF ON EVERY CALL: verify() also requires an unknown document id to be not found
and a wrong key to be refused.
"""
sub = "kb"
ZERO = "00000000-0000-4000-8000-000000000000"
def _api(self, w, sub, call, body, key):
return w.app_curl(sub, "/api/" + call, "-H", f"Authorization: Bearer {key}", "-H",
"Content-Type: application/json", data=json.dumps(body), method="POST")
def seed(self, w, sub, say):
if not w.wait_app(sub, "/_health", want=("200",), tries=90):
return None
o = f"https://{sub}.{w.DOMAIN}"
rc, code, out = w.app_curl(sub, "/api/installation.create", "-D", "-", "-H", "Content-Type: application/json",
"-H", f"Origin: {o}",
data=json.dumps({"teamName": "Drill", "userName": "Drill",
"userEmail": "drill" + secrets.token_hex(3) + "@gate.invalid"}),
method="POST")
ck = _set_cookies(out)
say(f" outline: installation.create http={code}")
if code not in ("200", "302") or "accessToken=" not in ck:
self.tried = f"POST /api/installation.create -> {code}"
return None
rc, code, out = w.app_curl(sub, "/home", "-D", "-", "-o", "/dev/null", "-H", f"Cookie: {ck}")
csrf = re.search(r"(?im)^set-cookie:\s*csrfToken=([^;\r\n]+)", out or "")
if not csrf:
self.tried = "no csrfToken cookie from GET /home"
return None
cs = csrf.group(1)
rc, code, out = w.app_curl(sub, "/api/apiKeys.create", "-H", f"Cookie: {ck}; csrfToken={cs}", "-H", f"x-csrf-token: {cs}",
"-H", "Content-Type: application/json", "-H", f"Origin: {o}",
data=json.dumps({"name": "drill"}), method="POST")
try:
key = json.loads(out)["data"]["value"]
except Exception:
self.tried = f"POST /api/apiKeys.create -> {code} {out[:120]}"
return None
rc, code, out = self._api(w, sub, "collections.create", {"name": "Drill"}, key)
try:
col = json.loads(out)["data"]["id"]
except Exception:
self.tried = f"POST /api/collections.create -> {code} {out[:120]}"
return None
title, body = "drilldoc-" + secrets.token_hex(4), "drill body " + secrets.token_hex(6)
rc, code, out = self._api(w, sub, "documents.create",
{"title": title, "text": body, "collectionId": col, "publish": True}, key)
try:
did = json.loads(out)["data"]["id"]
except Exception:
self.tried = f"POST /api/documents.create -> {code} {out[:120]}"
return None
say(f" outline: seeded document {title}")
return {"key": key, "id": did, "title": title, "body": body}
def verify(self, w, sub, t, say):
if not w.wait_app(sub, "/_health", want=("200",), tries=90):
say(" outline: the app never served /_health")
return False
rc, code, _ = self._api(w, sub, "documents.info", {"id": self.ZERO}, t["key"])
if code != "404":
say(f" outline: READBACK UNUSABLE — an unknown document answered {code}")
return False
rc, code, _ = self._api(w, sub, "documents.info", {"id": t["id"]}, "ol_api_" + secrets.token_hex(19))
if code != "401":
say(f" outline: READBACK UNUSABLE — a wrong key answered {code}")
return False
rc, code, out = self._api(w, sub, "documents.info", {"id": t["id"]}, t["key"])
found = code == "200" and t["title"] in (out or "") and t["body"] in (out or "")
say(f" outline: readback of the seeded document http={code} found={found}")
return found
FIXTURES = {
"sparkyfitness": Sparkyfitness(),
"rallly": Rallly(),
"outline": Outline(),
"home-assistant": HomeAssistant(),
"romm": Romm(),
"vikunja": Vikunja(),
+2 -4
View File
@@ -407,8 +407,6 @@ FIXTURES28.update({
"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": NoRoute("outline", "outline", "sign-in requires an external identity provider "
"(OIDC/Slack/Google); no local sign-up route exists"),
"rallly": NoRoute("rallly", "rallly", "sign-in is an e-mail magic link; this venue has no "
"mailbox the harness can read"),
# 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.
})