Files
cloudflare__vinext/tests/cache-control.test.ts
T
James Anderson 1500fe61d7 fix(pages): align prerender functional parity (#2471)
* fix(pages): align prerender functional parity

* fix(pages): harden preview bypass comparison
2026-07-02 19:57:40 +01:00

152 lines
5.5 KiB
TypeScript

import { describe, expect, it, afterEach } from "vite-plus/test";
import {
applyCdnResponseHeaders,
BROWSER_REVALIDATE_CACHE_CONTROL,
buildCachedRevalidateCacheControl,
buildRevalidateCacheControl,
shouldUseNextDeployCacheControl,
} from "../packages/vinext/src/server/cache-control.js";
import {
setCdnCacheAdapter,
DefaultCdnCacheAdapter,
type CdnCacheAdapter,
type CdnCacheableHeaderInput,
} from "../packages/vinext/src/shims/cdn-cache.js";
describe("cache-control helpers", () => {
it("uses Next.js expire minus revalidate for finite SWR windows", () => {
expect(buildRevalidateCacheControl(60, 300)).toBe("s-maxage=60, stale-while-revalidate=240");
});
it("omits stale-while-revalidate when expire does not exceed revalidate", () => {
expect(buildRevalidateCacheControl(300, 300)).toBe("s-maxage=300");
});
it("preserves vinext's legacy unbounded SWR header when expire is unknown", () => {
expect(buildRevalidateCacheControl(60)).toBe("s-maxage=60, stale-while-revalidate");
});
it("uses route policy for STALE cached responses when expire is known", () => {
expect(buildCachedRevalidateCacheControl("STALE", 60, 300)).toBe(
"s-maxage=60, stale-while-revalidate=240",
);
});
it("uses route policy for HIT cached responses when expire is known", () => {
expect(buildCachedRevalidateCacheControl("HIT", 60, 300)).toBe(
"s-maxage=60, stale-while-revalidate=240",
);
});
it("uses static cache-control for cached indefinite responses", () => {
expect(buildCachedRevalidateCacheControl("HIT", Infinity)).toBe(
"s-maxage=31536000, stale-while-revalidate",
);
});
it("preserves legacy STALE cached response headers when expire is unknown", () => {
expect(buildCachedRevalidateCacheControl("STALE", 60)).toBe(
"s-maxage=0, stale-while-revalidate",
);
});
it("keeps the full expire window when revalidate is zero", () => {
expect(buildRevalidateCacheControl(0, 300)).toBe("s-maxage=0, stale-while-revalidate=300");
});
});
describe("applyCdnResponseHeaders", () => {
const CDN_KEY = Symbol.for("vinext.cdnCacheAdapter");
const originalNextDeployCacheControl = process.env.VINEXT_NEXT_DEPLOY_CACHE_CONTROL;
afterEach(() => {
delete (globalThis as Record<PropertyKey, unknown>)[CDN_KEY];
if (originalNextDeployCacheControl === undefined) {
delete process.env.VINEXT_NEXT_DEPLOY_CACHE_CONTROL;
} else {
process.env.VINEXT_NEXT_DEPLOY_CACHE_CONTROL = originalNextDeployCacheControl;
}
});
it("default adapter sets a single Cache-Control identical to the input", () => {
const headers = new Headers();
applyCdnResponseHeaders(headers, { cacheControl: "s-maxage=60, stale-while-revalidate" });
expect(headers.get("Cache-Control")).toBe("s-maxage=60, stale-while-revalidate");
expect(headers.get("CDN-Cache-Control")).toBeNull();
});
it("uses browser revalidation Cache-Control for shared cache policies in Next deploy mode", () => {
process.env.VINEXT_NEXT_DEPLOY_CACHE_CONTROL = "1";
const headers = new Headers();
applyCdnResponseHeaders(headers, { cacheControl: "s-maxage=60, stale-while-revalidate" });
expect(shouldUseNextDeployCacheControl()).toBe(true);
expect(headers.get("Cache-Control")).toBe(BROWSER_REVALIDATE_CACHE_CONTROL);
expect(headers.get("CDN-Cache-Control")).toBeNull();
});
it("routes through the active adapter (edge: CDN-Cache-Control + Cache-Tag) and clears stale headers", () => {
// Minimal edge adapter that splits headers and emits a tag header.
const edge: CdnCacheAdapter = {
ownsBackgroundRevalidation: false,
async get() {
return null;
},
async set() {},
buildResponseHeaders(input: CdnCacheableHeaderInput) {
return {
"Cache-Control": "no-store",
"CDN-Cache-Control": input.cacheControl,
...(input.tags?.length ? { "Cache-Tag": input.tags.join(",") } : {}),
};
},
async revalidateTag() {},
};
setCdnCacheAdapter(edge);
const headers = new Headers({ "Cache-Control": "stale", "CDN-Cache-Control": "stale" });
applyCdnResponseHeaders(headers, { cacheControl: "s-maxage=60", tags: ["a", "b"] });
expect(headers.get("Cache-Control")).toBe("no-store");
expect(headers.get("CDN-Cache-Control")).toBe("s-maxage=60");
expect(headers.get("Cache-Tag")).toBe("a,b");
});
it("applies adapter-owned header removals", () => {
const edge: CdnCacheAdapter = {
ownsBackgroundRevalidation: false,
async get() {
return null;
},
async set() {},
buildResponseHeaders() {
return {
"Cache-Control": "no-store",
"CDN-Cache-Control": null,
"Cache-Tag": null,
};
},
async revalidateTag() {},
};
setCdnCacheAdapter(edge);
const headers = new Headers({
"Cache-Control": "public, max-age=3600",
"CDN-Cache-Control": "public, max-age=3600",
"Cache-Tag": "stale",
});
applyCdnResponseHeaders(headers, { cacheControl: "no-store" });
expect(headers.get("Cache-Control")).toBe("no-store");
expect(headers.get("CDN-Cache-Control")).toBeNull();
expect(headers.get("Cache-Tag")).toBeNull();
});
it("default adapter restores baseline after the edge adapter is cleared", () => {
setCdnCacheAdapter(new DefaultCdnCacheAdapter());
const headers = new Headers();
applyCdnResponseHeaders(headers, { cacheControl: "s-maxage=10" });
expect(headers.get("Cache-Control")).toBe("s-maxage=10");
});
});