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:
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user