web: test bot responses and date-dependent query caching

This commit is contained in:
ponas committed 2026-10-14 00:36:58 +02:00
1 parent a4bfdb5657
commit 7d8bf4ba43
3 files changed
+152 -1

No files matched your search

+26
View File
@@ -121,6 +121,32 @@ describe("POST /api/revalidate", () => {
expect(followUpCalls()).toHaveLength(1);
});
it("follows up each tag on its own", async () => {
setUp();
await POST(request({ "x-revalidate-secret": "hemmelig" }));
await vi.advanceTimersByTimeAsync(2_000);
await POST(request({ "x-revalidate-secret": "hemmelig" }, "?tag=today"));
await vi.advanceTimersByTimeAsync(10_000);
expect(followUpCalls()).toEqual([
"http://127.0.0.1:4321/api/revalidate?tag=cms",
"http://127.0.0.1:4321/api/revalidate?tag=today",
]);
});
it("reports a follow-up that fails", async () => {
setUp();
const error = vi.spyOn(console, "error").mockImplementation(() => {});
fetchMock.mockResolvedValue({ ok: false, status: 503 });
await POST(request({ "x-revalidate-secret": "hemmelig" }));
await vi.advanceTimersByTimeAsync(6_000);
expect(error).toHaveBeenCalledTimes(1);
error.mockRestore();
});
it("is not scheduled by the server's own purges", async () => {
setUp();
+82
View File
@@ -0,0 +1,82 @@
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import * as generated from "@/gql/graphql";
const SRC = fileURLToPath(new URL("..", import.meta.url));
function sourceFiles(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name);
if (entry.isDirectory()) return entry.name === "gql" ? [] : sourceFiles(path);
return /\.tsx?$/.test(entry.name) && !/\.test\.tsx?$/.test(entry.name)
? [path]
: [];
});
}
const sources = sourceFiles(SRC).map((file) => ({
file: file.slice(SRC.length),
text: readFileSync(file, "utf8"),
}));
// const fooQuery = graphql(`query foo { ... }`) -> fooQuery: "foo"
const operationByConstant = new Map<string, string>();
for (const { text } of sources) {
const definition = /const (\w+) = graphql\(`\s*query (\w+)/g;
for (const [, constant, operation] of text.matchAll(definition)) {
operationByConstant.set(constant, operation);
}
}
// The generated documents have their fragments inlined, so a field counts
// even when it is selected through a fragment
function selectsFutureEvents(operation: string): boolean {
const name = `${operation[0].toUpperCase()}${operation.slice(1)}Document`;
const document = (generated as Record<string, unknown>)[name];
if (!document) throw new Error(`no generated document called ${name}`);
return JSON.stringify(document).includes('"futureEvents"');
}
function callSites(fn: "cmsQuery" | "cmsQueryForToday") {
const call = new RegExp(`\\b${fn}\\(\\s*(\\w+)`, "g");
return sources.flatMap(({ file, text }) =>
[...text.matchAll(call)]
// the definitions themselves in app/client.ts
.filter(([, constant]) => constant !== "document")
.map(([, constant]) => {
const operation = operationByConstant.get(constant);
if (!operation) {
throw new Error(`${file}: can't find the query behind ${constant}`);
}
return { file, operation };
})
);
}
// The CMS decides by today's date what futureEvents contains. A query that
// selects it must also be purged at midnight, or it shows yesterday's events
describe("CMS queries", () => {
it("that select futureEvents go through cmsQueryForToday", () => {
const wrong = callSites("cmsQuery").filter(({ operation }) =>
selectsFutureEvents(operation)
);
expect(wrong).toEqual([]);
});
it("that don't depend on the date go through cmsQuery", () => {
const wrong = callSites("cmsQueryForToday").filter(
({ operation }) => !selectsFutureEvents(operation)
);
expect(wrong).toEqual([]);
});
it("are found by this scan", () => {
expect(callSites("cmsQuery").length).toBeGreaterThan(10);
expect(callSites("cmsQueryForToday").map((c) => c.operation).sort()).toEqual(
["futureEvents", "futureEvents", "home"]
);
});
});