Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 28s
CI / tests-unit (push) Successful in 1m39s
CI / tests-integration (push) Successful in 1m41s
CI / tests-ui (push) Successful in 2m27s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m35s
Rebuild production nginx from the repo (deployment/proxy/*) with a single Cache-Control owner per route: the app stays the source, nginx only manages headers, and Cloudflare stores the public API allowlist at the edge. - deployment/proxy: nginx.conf, mime.types, nginx-cms.conf and the blue/green upstream snippet; config backed by scripts/nginx-sync.sh (idempotent install + reload, --check/--force). - nginx serves Cache-Tag headers on the public allowlist (cms-public), gamedata, client and camera responses so the edge and purge stay in sync. - src/lib/edge-cache.ts + tests: coalesced, fire-and-forget edge purges that no-op unless Cloudflare is configured; scripts/cf-purge.sh and cf-setup-cache.sh create and purge the cache rule. - src/lib/cloudflare-api.ts: purgeCacheByTags/purgeCacheByUrls. - Purge hooks after catalog exports (public + gamedata) and on shop, team, guild, photo and rare-values edits; ci-deploy purges after each release. - src/proxy.ts excludes the imaging/images docs from the middleware matcher.
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const purgeCacheByTags = vi.fn();
|
|
const cloudflareEnabled = vi.fn();
|
|
|
|
vi.mock("@/lib/cloudflare-api", () => ({
|
|
purgeCacheByTags: (...args: unknown[]) => purgeCacheByTags(...args),
|
|
cloudflareEnabled: (...args: unknown[]) => cloudflareEnabled(...args),
|
|
}));
|
|
|
|
import {
|
|
EDGE_CACHE_TAGS,
|
|
purgeEdgeCache,
|
|
resetEdgeCacheCoalescing,
|
|
} from "@/lib/edge-cache";
|
|
|
|
describe("purgeEdgeCache", () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
resetEdgeCacheCoalescing();
|
|
});
|
|
|
|
it("is a no-op when Cloudflare is not configured", async () => {
|
|
cloudflareEnabled.mockReturnValue(false);
|
|
await purgeEdgeCache(["cms-public"], "test");
|
|
expect(purgeCacheByTags).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("purges the requested tags when enabled", async () => {
|
|
cloudflareEnabled.mockReturnValue(true);
|
|
await purgeEdgeCache([EDGE_CACHE_TAGS.public], "shop edited");
|
|
expect(purgeCacheByTags).toHaveBeenCalledExactlyOnceWith(["cms-public"]);
|
|
});
|
|
|
|
it("coalesces repeated purges of the same tag within 30s", async () => {
|
|
cloudflareEnabled.mockReturnValue(true);
|
|
await purgeEdgeCache(["cms-public"], "first");
|
|
await purgeEdgeCache(["cms-public"], "second");
|
|
expect(purgeCacheByTags).toHaveBeenCalledTimes(1);
|
|
expect(purgeCacheByTags).toHaveBeenCalledWith(["cms-public"]);
|
|
});
|
|
|
|
it("purges again after the coalescing window has passed", async () => {
|
|
cloudflareEnabled.mockReturnValue(true);
|
|
await purgeEdgeCache(["cms-gamedata"], "first");
|
|
await purgeEdgeCache(["cms-gamedata"], "second");
|
|
expect(purgeCacheByTags).toHaveBeenCalledTimes(1);
|
|
resetEdgeCacheCoalescing();
|
|
await purgeEdgeCache(["cms-gamedata"], "third");
|
|
expect(purgeCacheByTags).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("never throws on an upstream failure", async () => {
|
|
cloudflareEnabled.mockReturnValue(true);
|
|
purgeCacheByTags.mockRejectedValue(new Error("boom"));
|
|
await expect(
|
|
purgeEdgeCache(["cms-public"], "fails"),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|