Grimmory tested but held (R-775): its fixture added; CHANGELOG, CONTEXT, REPORT for the night's new apps
gates / gates (push) Successful in 2s

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-10-01 19:51:12 +02:00
parent 72247a387e
commit 94477cba43
4 changed files with 122 additions and 13 deletions
+94
View File
@@ -2066,6 +2066,99 @@ class Dawarich:
return found
# =============================================================================================
class Grimmory:
"""Grimmory (2026-10-01, new app through NEW-APP-CHECKLIST.md). THE FRONT DOOR is the app's own API, the one its web
client calls: the first admin by `POST /api/v1/setup` (through the setup gate on the box, as the household), sign in
with `POST /api/v1/auth/login` (a bearer token), a library on `/books`, and a real EPUB (built here, with a unique
title) uploaded with `POST /api/v1/files/upload` — the app writes it into the household's book folder. Read back:
`GET /api/v1/books` lists the title. Negative controls on every readback: no token 401, a wrong password 401, and a
second `POST /api/v1/setup` refused (403) — the readback is not an open door."""
sub = "library"
@staticmethod
def _epub(title):
import io as _io, zipfile
b = _io.BytesIO()
z = zipfile.ZipFile(b, "w")
z.writestr(zipfile.ZipInfo("mimetype"), "application/epub+zip")
z.writestr("META-INF/container.xml", '<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>')
z.writestr("OEBPS/content.opf", f'<?xml version="1.0"?><package xmlns="http://www.idpf.org/2007/opf" unique-identifier="id" version="2.0"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>{title}</dc:title><dc:creator>Felhom</dc:creator><dc:identifier id="id">{title}</dc:identifier><dc:language>hu</dc:language></metadata><manifest><item id="c1" href="c1.xhtml" media-type="application/xhtml+xml"/></manifest><spine><itemref idref="c1"/></spine></package>')
z.writestr("OEBPS/c1.xhtml", '<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml"><head><title>c</title></head><body><p>Szia!</p></body></html>')
z.close()
return b.getvalue()
def _token(self, w, sub, user, pw):
rc, code, out = w.app_curl(sub, "/api/v1/auth/login", "-H", "Content-Type: application/json",
data=json.dumps({"username": user, "password": pw}), method="POST")
try:
return code, json.loads(out).get("accessToken")
except Exception:
return code, None
def seed(self, w, sub, say):
import tempfile
if not w.wait_app(sub, "/api/v1/healthcheck", want=("200",), tries=90):
self.tried = "/api/v1/healthcheck never answered 200"
return None
user, pw = "admin", "Gm-" + secrets.token_hex(10)
rc, code, out = w.app_curl(sub, "/api/v1/setup", "-H", "Content-Type: application/json",
data=json.dumps({"username": user, "password": pw, "email": "admin@felhom.invalid", "name": "Felhom"}), method="POST")
say(f" grimmory: POST /api/v1/setup (the first admin) http={code}")
if code != "200":
self.tried = f"setup -> {code} {(out or '')[:120]}"
return None
code, tok = self._token(w, sub, user, pw)
if not tok:
self.tried = f"login -> {code}"
return None
auth = ["-H", f"Authorization: Bearer {tok}"]
rc, code, out = w.app_curl(sub, "/api/v1/libraries", *auth, "-H", "Content-Type: application/json",
data=json.dumps({"name": "Konyvek", "icon": "book", "iconType": "LUCIDE", "paths": [{"path": "/books"}],
"watch": True, "metadataSource": "EMBEDDED", "organizationMode": "BOOK_PER_FILE"}), method="POST")
try:
lib = json.loads(out)
lid, pid = lib["id"], lib["paths"][0]["id"]
except Exception:
self.tried = f"create library -> {code} {(out or '')[:120]}"
return None
title = "Felhom Teszt " + secrets.token_hex(4)
fn = tempfile.mktemp(suffix=".epub")
open(fn, "wb").write(self._epub(title))
if hasattr(w, "put_file"):
fn = w.put_file(fn)
rc, code, out = w.app_curl(sub, "/api/v1/files/upload", *auth, "-F", f"file=@{fn};type=application/epub+zip",
"-F", f"libraryId={lid}", "-F", f"pathId={pid}", method="POST")
say(f" grimmory: library {lid}, upload of an EPUB http={code}")
if code not in ("200", "201", "204"):
self.tried = f"upload -> {code} {(out or '')[:120]}"
return None
return {"user": user, "pw": pw, "title": title}
def verify(self, w, sub, t, say):
if not w.wait_app(sub, "/api/v1/healthcheck", want=("200",), tries=90):
say(" grimmory: /api/v1/healthcheck never answered")
return False
rc, c_none, _ = w.app_curl(sub, "/api/v1/books")
c_wrong, _ = self._token(w, sub, t["user"], t["pw"] + "x")
rc, c_setup, _ = w.app_curl(sub, "/api/v1/setup", "-H", "Content-Type: application/json",
data=json.dumps({"username": "x" + secrets.token_hex(2), "password": "Gm-" + secrets.token_hex(8),
"email": "x@felhom.invalid", "name": "X"}), method="POST")
if c_none != "401" or c_wrong != "401" or c_setup != "403":
say(f" grimmory: READBACK UNUSABLE — no token {c_none}, wrong password {c_wrong}, second setup {c_setup}")
return False
code, tok = self._token(w, sub, t["user"], t["pw"])
found = False
for _ in range(12):
rc, code, out = w.app_curl(sub, "/api/v1/books", "-H", f"Authorization: Bearer {tok}")
found = code == "200" and t["title"] in (out or "")
if found:
break
time.sleep(5)
say(f" grimmory: readback http={code} found={found} (controls: no token {c_none}, wrong {c_wrong}, second setup {c_setup})")
return found
FIXTURES = {
"uptime-kuma": UptimeKuma(),
"crafty-controller": Crafty(),
@@ -2105,4 +2198,5 @@ FIXTURES = {
"radicale": Radicale(),
"karakeep": Karakeep(),
"dawarich": Dawarich(),
"grimmory": Grimmory(),
}