Grimmory tested but held (R-775): its fixture added; CHANGELOG, CONTEXT, REPORT for the night's new apps
gates / gates (push) Successful in 2s
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:
@@ -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(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user