Files
EpicNext-Cms/src/lib/edge-cache.test.ts
T
openhands 7697728d07
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
feat(cache): single-owner caching across nginx, edge and content edits
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.
2026-09-28 21:55:18 +02:00

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();
});
});