From 098cfdf1934a39fb692236434378a3718fa0964f Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Wed, 14 Oct 2026 11:10:16 -0400 Subject: [PATCH] Catalog: setup.record-export leaves setup.record-export leaves for forgectrl's tests/host_guard.sh (84779c8 on d21e200): the record as JSON with the token (schema 1, the sheet id GET /wiz reports, no password hash, a plain read not an attachment), the download an attachment of the record named with that id, the printable page text/html with no script, the sheet id, "setup record" and an

per completed wizard, both routes 403 without a token or a login, and the sanitized export carrying system/setup.json with the same id and wizards, no $6$, and no panel token anywhere in the archive. The catalog goes from 97 to 96. One HOST_TESTED row: forgectrl src/recordhtml.*, the one path no other catalog test covers, to tests/recordhtml_test.c (no script, the sheet id, the step headings, the escaping), which forgectrl's build.yml runs at the pinned revision. src/setup.* and src/wiz.* stay on the setup family, src/logs.* on exthost.service, forgectrl.lease and the two log tests, and src/main.c, src/ui/index.html, src/ui/panel.js and src/ui/wizard.html on the tests that already name them. tests/host_guard.sh is not named: the pinned workflow does not run it. tests/test_setup_suite.py drops the id, the record-export hardware assertion, and the export lines of the lifecycle covers test (the covers set and its two assertions); the mirror's assertions stay. test_setup_suite.py passes (31 tests). The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 96 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes is clean on setup.py and test_setup_suite.py. The full forgetest suite was not run. Fingerprints: the deletion moves suite/setup.py's shared text, so 33 tests move: the 8 left in setup.py and the 25 in the modules that read it (setup_dark, setup_burns, bedsize, exthost, extcall, extcat, extsender, extwizard). The pending pin bump from c274f19 already owes 31 of them; setup.machine-name and exthost.platform are the two it adds, as before. releases/ is unchanged. --- forgetest/forgetest/manifest.py | 3 + forgetest/forgetest/suite/setup.py | 94 ----------------------------- forgetest/tests/test_setup_suite.py | 6 +- 3 files changed, 4 insertions(+), 99 deletions(-) diff --git a/forgetest/forgetest/manifest.py b/forgetest/forgetest/manifest.py index a47e65a..6348c82 100644 --- a/forgetest/forgetest/manifest.py +++ b/forgetest/forgetest/manifest.py @@ -137,6 +137,9 @@ HOST_TESTED = [ # The built-in extensions' table: the cloud entry, its refusals while it # is off, the sweep when it goes off, the list. ("forgectrl", "src/builtin.*", "forgectrl:tests/builtin_test.c", "forgectrl:.github/workflows/build.yml"), + # The setup record as a printable page: no script, the sheet id, a + # heading for each completed step, everything escaped. + ("forgectrl", "src/recordhtml.*", "forgectrl:tests/recordhtml_test.c", "forgectrl:.github/workflows/build.yml"), ] # This repository's root, when the package runs from a checkout of it; a diff --git a/forgetest/forgetest/suite/setup.py b/forgetest/forgetest/suite/setup.py index 13e36c1..991f246 100644 --- a/forgetest/forgetest/suite/setup.py +++ b/forgetest/forgetest/suite/setup.py @@ -1175,100 +1175,6 @@ def cert_page(ctx): # ------------------------------------------------------- the lifecycle -@test("setup.record-export", title="The record: JSON, the printable page, and the log bundle", - subsystem="setup", kind="auto", est_min=2, - covers=[("forgectrl", "src/setup.*"), ("forgectrl", "src/recordhtml.*"), ("forgectrl", "src/wiz.*"), - ("forgectrl", "src/logs.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/index.html"), - ("forgectrl", "src/ui/panel.js"), ("forgectrl", "src/ui/wizard.html")], - requires=["forgectrl.auth", "logs.tree-tail-export"], - description="GET /wiz/record with the token is the record (schema 1, the sheet id, the " - "wizards) with no password hash in it; with download=1 it comes as an attachment " - "named after the sheet id; GET /wiz/record.html is a page with no script that " - "carries the sheet id and every completed wizard; without the token and without " - "a login both are refused (403); and the sanitized log export carries " - "system/setup.json, parseable, with the same sheet id and no panel token.") -def record_export(ctx): - import gzip - import io - import tarfile - fc = ctx.forgectrl - ev = ctx.evidence - base = tls_base() - tok = {"X-ForgeFIRM-Token": fc.token} - w = wiz(fc) - sid = w.get("sheet_id") or "" - ev["sheet_id"] = sid - ctx.check(sid, "GET /wiz carries no sheet id") - - st, body, hdrs = request(base, "GET", "/wiz/record", headers=tok) - rec = decode(body) - ctx.log("GET /wiz/record -> %s (%d bytes)", st, len(body or b"")) - ctx.check(st == 200 and isinstance(rec, dict), "GET /wiz/record -> %s", st) - ctx.check(isinstance(rec, dict) and rec.get("schema") == 1 and rec.get("sheet_id") == sid, - "the record reads %s", rec if not isinstance(rec, dict) - else {k: rec.get(k) for k in ("schema", "sheet_id")}) - done = sorted((rec.get("wizards") or {}).keys()) if isinstance(rec, dict) else [] - ev["wizards_in_record"] = done - ctx.check(b"$6$" not in (body or b""), "the record carries a password hash") - ctx.check("attachment" not in (hdrs.get("content-disposition") or ""), "a plain read came as an attachment") - - st, body, hdrs = request(base, "GET", "/wiz/record?download=1", headers=tok) - cd = hdrs.get("content-disposition") or "" - ev["download_disposition"] = cd - ctx.check(st == 200 and "attachment" in cd and sid in cd, - "the download is not an attachment named after the sheet id: %s %r", st, cd) - ctx.check(decode(body) == rec, "the download differs from the record") - - st, body, hdrs = request(base, "GET", "/wiz/record.html", headers=tok) - page = (body or b"").decode("utf-8", "replace") - ev["page_bytes"] = len(body or b"") - ctx.log("GET /wiz/record.html -> %s (%d bytes)", st, len(body or b"")) - ctx.check(st == 200 and "text/html" in (hdrs.get("content-type") or ""), "GET /wiz/record.html -> %s %s", - st, hdrs.get("content-type")) - ctx.check("" + titles[d]) not in page] - ev["missing_on_page"] = missing - ctx.check(not missing, "completed wizards missing from the page: %s", missing) - - for path in ("/wiz/record", "/wiz/record.html"): - st, body, hdrs = request(base, "GET", path) - ctx.check(st == 403, "GET %s with no token and no login -> %s, expected 403", path, st) - - ctx.check(fc.wait_idle(timeout=30, abort=ctx.aborted), "machine not idle: the export is refused") - slow = hw.Forgectrl(token=fc.token, timeout=300.0) - t0 = time.time() - st, data = slow.post("/logs/export", raw=True) - ev["export_s"] = round(time.time() - t0, 1) - ctx.log("POST /logs/export -> %s (%d bytes, %.1f s)", st, len(data) if data else 0, ev["export_s"]) - ctx.check(st == 200, "POST /logs/export -> %s", st) - member = None - try: - tf = tarfile.open(fileobj=io.BytesIO(gzip.decompress(data))) - for m in tf.getmembers(): - if m.isfile() and m.name.endswith("system/setup.json"): - member = tf.extractfile(m).read() - except (OSError, tarfile.TarError, EOFError) as e: - ctx.fail("export is not a readable tar.gz: %s", e) - ctx.check(member is not None, "the bundle lacks system/setup.json") - if member is not None: - try: - inside = json.loads(member.decode("utf-8")) - except ValueError as e: - inside = None - ctx.check(False, "the bundled record is not JSON: %s", e) - ev["bundle_record_bytes"] = len(member) - ctx.check(isinstance(inside, dict) and inside.get("sheet_id") == sid, - "the bundled record is not this machine's: %s", - inside.get("sheet_id") if isinstance(inside, dict) else inside) - ctx.check(isinstance(inside, dict) and sorted((inside.get("wizards") or {}).keys()) == done, - "the bundled record's wizards differ from the record's") - ctx.check(fc.token.encode() not in member, "the bundled record carries the panel token") - ctx.check(b"$6$" not in member, "the bundled record carries a password hash") - ctx.log("record %d wizards; page %d bytes; bundle carries the record", len(done), ev["page_bytes"]) - - @test("setup.mirror", title="One browser drives a check; a second one follows", subsystem="setup", kind="auto", est_min=2, hardware="takeover", covers=[("forgectrl", "src/wizdark.*"), ("forgectrl", "src/wizcalc.*"), ("forgectrl", "src/wiz.*"), diff --git a/forgetest/tests/test_setup_suite.py b/forgetest/tests/test_setup_suite.py index 72c8f36..02c92a1 100644 --- a/forgetest/tests/test_setup_suite.py +++ b/forgetest/tests/test_setup_suite.py @@ -19,7 +19,7 @@ from forgetest.suite import setup IDS = ("setup.account-login", "setup.https-only-writes", "setup.ssh-until-reboot", "setup.machine-name", "setup.first-run-flow", - "setup.first-run-page", "setup.record-export", + "setup.first-run-page", "setup.mirror") OPERATOR = ("setup.first-run-flow", "setup.first-run-page") @@ -44,13 +44,9 @@ class RegistrationTests(unittest.TestCase): def test_the_record_swapping_tests_are_takeovers(self): for tid in ("setup.first-run-flow", "setup.first-run-page", "setup.mirror"): self.assertEqual(self.reg[tid].hardware, "takeover", tid) - self.assertEqual(self.reg["setup.record-export"].hardware, "api") def test_the_lifecycle_tests_cover_their_files(self): - export = set(self.reg["setup.record-export"].covers) mirror = set(self.reg["setup.mirror"].covers) - self.assertIn(("forgectrl", "src/recordhtml.*"), export) - self.assertIn(("forgectrl", "src/logs.*"), export) self.assertIn(("forgectrl", "src/wizdark.*"), mirror) self.assertIn("setup.check-sensors", self.reg["setup.mirror"].requires)