web: smoke-test the built standalone app against a mock graphql cms

This commit is contained in:
2026-08-31 03:26:54 +02:00
parent 6d0932e44c
commit 389b95ed85
8 changed files with 870 additions and 1 deletions
+119
View File
@@ -0,0 +1,119 @@
// A tiny stand-in for the Wagtail CMS, reachable over HTTP so both
// `next build` and the standalone server (separate child processes) can
// query it. Serves the canned GraphQL responses from fixtures.ts, a 1x1 PNG
// for image URLs, and a state endpoint the revalidation test uses to
// "publish a change" in the CMS.
import http from "node:http";
import type { AddressInfo } from "node:net";
import { makeFixtures, type Fixtures, type MockState } from "./fixtures";
// 1x1 transparent PNG
const TINY_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"base64"
);
export type MockCms = {
url: string;
state: MockState;
graphqlRequests: number;
close(): Promise<void>;
};
function readBody(req: http.IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
}
export async function startMockCms(): Promise<MockCms> {
const state: MockState = { newsHeadlineVersion: "v1" };
let fixtures: Fixtures | null = null; // built once we know our own URL
const mock: MockCms = {
url: "",
state,
graphqlRequests: 0,
close: () =>
new Promise((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve()))
),
};
const server = http.createServer(async (req, res) => {
try {
if (req.method === "GET" && req.url?.startsWith("/media/")) {
res.writeHead(200, {
"content-type": "image/png",
"content-length": TINY_PNG.length,
});
res.end(TINY_PNG);
return;
}
if (req.method === "POST" && req.url === "/__mock/state") {
Object.assign(state, JSON.parse(await readBody(req)));
res.writeHead(200, { "content-type": "application/json" });
res.end("{}");
return;
}
if (req.url?.startsWith("/api/graphql")) {
mock.graphqlRequests += 1;
// urql sends both POST (JSON body) and GET (URL params) requests
let body: {
query?: string;
operationName?: string;
variables?: Record<string, unknown>;
};
if (req.method === "GET") {
const params = new URL(req.url, mock.url).searchParams;
body = {
query: params.get("query") ?? undefined,
operationName: params.get("operationName") ?? undefined,
variables: JSON.parse(params.get("variables") ?? "{}"),
};
} else {
body = JSON.parse(await readBody(req));
}
const name =
body.operationName ??
/(?:query|mutation)\s+(\w+)/.exec(body.query ?? "")?.[1];
const resolve = name ? fixtures?.[name] : undefined;
res.writeHead(200, { "content-type": "application/json" });
if (!resolve) {
// Loud gap detection: build-time queries throw on GraphQL errors,
// so a missing fixture fails `next build` with the name visible.
res.end(
JSON.stringify({
errors: [{ message: `mock-cms: no fixture for operation ${name}` }],
})
);
return;
}
res.end(JSON.stringify({ data: resolve(body.variables ?? {}, state) }));
return;
}
console.error(`mock-cms: unhandled ${req.method} ${req.url}`);
res.writeHead(404);
res.end();
} catch (err) {
res.writeHead(500, { "content-type": "application/json" });
res.end(JSON.stringify({ errors: [{ message: String(err) }] }));
}
});
await new Promise<void>((resolve) =>
server.listen(0, "127.0.0.1", () => resolve())
);
const { port } = server.address() as AddressInfo;
mock.url = `http://127.0.0.1:${port}`;
fixtures = makeFixtures(mock.url);
return mock;
}