diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index a0cc41d..ad686ff 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -31,9 +31,10 @@ jobs: # the pixel dimensions of each mirrored file, which become the # width/height attributes that stop the posts page reflowing as 5 MB # recordings arrive. ffmpeg transcodes each still image into the AVIF - # and PNG renditions the page serves it between. tools/e2e.sh asserts - # both are present — so without this package the deploy fails at the - # e2e gate rather than shipping a janky page. + # and PNG renditions the page serves it between. The + # ShouldServePostPages suite asserts both are present — so without + # this package the deploy fails at the test gate rather than + # shipping a janky page. pacman -Syu --noconfirm --needed \ nodejs \ clang lld libc++ \ @@ -121,24 +122,37 @@ jobs: dir=media fi tools/fetch-media.sh "$dir" - # Handed to the e2e step so it checks the files where they actually - # are. It used to assume ./media and reported every referenced file - # as missing here, which reads as a broken site and means a wrong path. + # Handed to the test step (E2E_MEDIA_DIR) so ShouldServePostPages + # checks the files where they actually are. It used to assume + # ./media and reported every referenced file as missing here, which + # reads as a broken site and means a wrong path. echo "dir=$dir" >> "$GITHUB_OUTPUT" - name: Build and test the backend id: srv # The server product builds Catcrafts.Shared for the host, which is the # only way to actually RUN the code that generates every byte of markup - # the site emits. --selftest is a gate: if escaping or the JSON reader - # regress, the deploy stops here rather than shipping broken pages. + # the site emits. `crafter-build test` is the gate: if escaping or the + # JSON reader regress, the deploy stops here rather than shipping + # broken pages. One suite per tests//main.cpp — the unit suites + # replaced the binary's --selftest flag, the Should{ServeRoutes,…} + # black-box suites replaced tools/e2e.sh: each spawns the freshly + # built server on its own scratch port with the fake payment rails and + # exercises it over real HTTP. ShouldBootWasmAtDepth self-skips here — + # no wasm bundle exists yet — and re-runs after the wasm build below. + # + # E2E_MEDIA_DIR points ShouldServePostPages at the mirror mount, where + # the media files actually are. # # Same refuse-to-guess rule as the wasm bundle below: a variant # directory embeds a config hash, so more than one match means the tree # is ambiguous and picking the first would deploy an arbitrary build. + env: + E2E_MEDIA_DIR: ${{ steps.media.outputs.dir }} run: | set -eux - crafter-build -- --product=server + crafter-build --product=server + crafter-build test --product=server matches=$(find bin -maxdepth 1 -type d -name 'Catcrafts.Server-*' | sort) count=$(printf '%s\n' "$matches" | grep -c . || true) if [ "$count" -ne 1 ]; then @@ -147,7 +161,6 @@ jobs: exit 1 fi echo "srv=$matches" >> "$GITHUB_OUTPUT" - "$matches/catcrafts-server" --selftest "$matches/catcrafts-server" --routes - name: Generate sitemap and Atom feed @@ -204,26 +217,18 @@ jobs: DIST: ${{ steps.out.outputs.dist }} run: tools/fix-bundle-depth.sh "$DIST" - - name: End-to-end HTTP tests - # Starts the freshly built server on a scratch port and exercises it over - # real HTTP: status codes, redirects, headers, form submission, and the - # no-JavaScript guarantee. --selftest covers the pure functions; only a - # real request can show that /nope is a 404 rather than a soft 404, that - # /projects contains its content with no "), - "<script>alert(1)</script>", "escape: script tag"); - // Non-ASCII passes through untouched — the output is UTF-8, and - // entity-encoding it would just bloat the page. - CheckEq(Escape("café ✓ 日本"), "café ✓ 日本", "escape: utf-8 passthrough"); - CheckEq(Escape(""), "", "escape: empty"); - - // ── Num ─────────────────────────────────────────────────────────── - CheckEq(Num(0), "0", "num: zero"); - CheckEq(Num(-42), "-42", "num: negative"); - CheckEq(Num(9007199254740993LL), "9007199254740993", "num: beyond double precision"); - - // ── Attr ────────────────────────────────────────────────────────── - CheckEq(Attr("class", "card"), " class=\"card\"", "attr: basic"); - CheckEq(Attr("data-x", "a\"b"), " data-x=\"a"b\"", "attr: value escaped"); - CheckEq(Attr("class", ""), "", "attr: empty value omits attribute"); - // An invalid name is a programming error, not user data. Emitting - // nothing is safer than emitting mangled markup. - CheckEq(Attr("on error", "x"), "", "attr: invalid name rejected"); - CheckEq(Attr("x>"), " href=\"#\"", "url: data: neutralised"); - // Browsers strip control characters before resolving the scheme, so a - // naive prefix check would pass this straight through. - CheckEq(Url("href", "java\tscript:alert(1)"), " href=\"#\"", "url: embedded tab"); - CheckEq(Url("href", " javascript:alert(1)"), " href=\"#\"", "url: leading space"); - CheckEq(Url("href", "//evil.example/x"), " href=\"#\"", "url: protocol-relative blocked"); - CheckEq(Url("href", "vbscript:x"), " href=\"#\"", "url: vbscript neutralised"); - - // ── Format ──────────────────────────────────────────────────────── - // The compile-time half of this guarantee (raw std::string rejected) is - // verified by the build itself — see the negative test in the notes. - CheckEq(Format("

{}

", Escape("aa<b", "format: escapes flow through"); - CheckEq(Format("{}", Url("href", "/x"), Escape("go")), - "go", "format: attr + text"); - CheckEq(Format("{}{}", Num(1), Num(2)), "12", "format: multiple args"); - CheckEq(Format("literal"), "literal", "format: no args"); - CheckEq(Format("{{literal braces}}"), "{literal braces}", "format: brace escaping"); - - // ── Join / concat ───────────────────────────────────────────────── - const std::array parts{ Escape("a"), Escape("b"), Escape("c") }; - CheckEq(Join(parts, Raw(", ")), "a, b, c", "join: separator"); - CheckEq(Join(std::span{}), "", "join: empty"); - CheckEq(Escape("a") + Escape("<"), "a<", "operator+: escapes preserved"); -} - -// The format ladder. One /