Files
tech-log-frontend/tests/unit/public-response-cache.test.ts
T

1218 lines
37 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type {
PublicCacheAsset,
PublicCacheReleaseManifest,
} from "../../src/application/ports/browser-file-storage/cache-storage-ports.ts";
import {
createDefaultPublicCachePolicy,
type PublicCacheRuntimePolicy,
} from "../../src/adapters/cache-storage/public-cache-policy.ts";
import {
computePublicCacheManifestDigestHex,
createPublicResponseCacheAdapter,
type PublicCacheMutationLock,
} from "../../src/adapters/cache-storage/public-response-cache-adapter.ts";
class MemoryCache {
readonly responses: Array<Readonly<{
request: Request;
response: Response;
}>> = [];
async match(request: RequestInfo | URL): Promise<Response | undefined> {
const url =
request instanceof Request ? request.url : new URL(String(request)).href;
const nativeRequest =
request instanceof Request ? request : new Request(url);
return this.responses
.find(
(entry) =>
entry.request.url === url &&
varyMatches(entry.request, nativeRequest, entry.response),
)
?.response.clone();
}
async put(request: RequestInfo | URL, response: Response): Promise<void> {
const url =
request instanceof Request ? request.url : new URL(String(request)).href;
const nativeRequest =
request instanceof Request ? request.clone() : new Request(url);
const existing = this.responses.findIndex(
(entry) =>
entry.request.url === url &&
varyMatches(entry.request, nativeRequest, response),
);
const entry = {
request: nativeRequest,
response: response.clone(),
};
if (existing >= 0) this.responses.splice(existing, 1, entry);
else this.responses.push(entry);
}
}
function varyMatches(
storedRequest: Request,
incomingRequest: Request,
response: Response,
): boolean {
const vary = response.headers.get("vary");
if (!vary) return true;
return vary
.split(",")
.map((name) => name.trim().toLowerCase())
.every(
(name) =>
storedRequest.headers.get(name) ===
incomingRequest.headers.get(name),
);
}
class MemoryCacheStorage {
readonly caches = new Map<string, MemoryCache>();
async open(name: string): Promise<Cache> {
let cache = this.caches.get(name);
if (!cache) {
cache = new MemoryCache();
this.caches.set(name, cache);
}
return cache as unknown as Cache;
}
async keys(): Promise<string[]> {
return [...this.caches.keys()];
}
async delete(name: string): Promise<boolean> {
return this.caches.delete(name);
}
}
const immediateLock: PublicCacheMutationLock = {
async run(_signal, task) {
return await task();
},
};
function deferred<Value>() {
let settle: ((value: Value) => void) | undefined;
const promise = new Promise<Value>((resolve) => {
settle = resolve;
});
return Object.freeze({
promise,
resolve(value: Value): void {
settle?.(value);
},
});
}
async function digestHex(bytes: Uint8Array): Promise<string> {
const digest = await globalThis.crypto.subtle.digest(
"SHA-256",
Uint8Array.from(bytes),
);
return [...new Uint8Array(digest)]
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
async function manifestFor(
releaseRegistryId: string,
assets: readonly PublicCacheAsset[],
policy: PublicCacheRuntimePolicy = createDefaultPublicCachePolicy(
"https://assets.example.test",
),
): Promise<PublicCacheReleaseManifest> {
return {
releaseRegistryId,
assets,
manifestDigestHex: await computePublicCacheManifestDigestHex(
globalThis.crypto,
releaseRegistryId,
assets,
policy,
),
};
}
describe("public response Cache Storage adapter", () => {
it("stages, re-verifies and atomically activates an exact release", async () => {
const cacheStorage = new MemoryCacheStorage();
const bytes = new Uint8Array([1, 2, 3, 4]);
const url = "https://assets.example.test/app.js?v=release-1";
const asset: PublicCacheAsset = {
absoluteUrl: url,
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const policy: PublicCacheRuntimePolicy = {
...createDefaultPublicCachePolicy(
"https://assets.example.test",
),
allowedQueryParameterNames: ["v"],
isQueryParameterValueAllowed: (name, value) =>
name === "v" && /^release-\d+$/u.test(value),
};
const manifest = await manifestFor("release-1", [asset], policy);
const observations: unknown[] = [];
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
now: () => 10,
observer: (event) => observations.push(event),
fetcher: async () =>
new Response(bytes, {
status: 200,
headers: {
"cache-control": "public, max-age=31536000, immutable",
"content-type": "application/javascript",
"x-user-id": "must-not-be-persisted",
},
}),
});
const staged = await adapter.admin.stageRelease(manifest);
expect(staged).toEqual({
ok: true,
value: {
releaseRegistryId: "release-1",
entryCount: 1,
totalBytes: 4,
stagedAtEpochMs: 10,
},
});
expect(
await adapter.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
),
).toMatchObject({ ok: true });
const match = await adapter.responses.matchActiveExact({
absoluteUrl: url,
});
expect(match.ok).toBe(true);
if (!match.ok || !match.value) throw new Error("cache miss");
const chunks: number[] = [];
for await (const chunk of match.value.body.stream(
new AbortController().signal,
)) {
expect(chunk.ok).toBe(true);
if (chunk.ok) chunks.push(...chunk.value);
}
expect(chunks).toEqual([1, 2, 3, 4]);
expect(match.value.headers.map(([name]) => name)).not.toContain(
"x-user-id",
);
expect(
await adapter.responses.matchActiveExact({
absoluteUrl:
"https://assets.example.test/app.js?v=release-2",
}),
).toEqual({ ok: true, value: null });
expect(JSON.stringify(observations)).not.toContain(url);
expect(JSON.stringify(observations)).not.toContain(
manifest.manifestDigestHex,
);
});
it("snapshots the complete manifest before asynchronous digest verification", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([2, 4, 6, 8]);
const asset = {
absoluteUrl: "https://assets.example.test/snapshot-crypto.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256" as const,
digestHex: await digestHex(bytes),
},
};
const signed = await manifestFor(
"snapshot-crypto",
[asset],
policy,
);
const manifest = {
releaseRegistryId: signed.releaseRegistryId,
assets: [asset],
manifestDigestHex: signed.manifestDigestHex,
};
const digestStarted = deferred<void>();
const releaseDigest = deferred<void>();
let delayFirstDigest = true;
const realCrypto = globalThis.crypto;
const delayedCrypto = {
subtle: {
async digest(
algorithm: AlgorithmIdentifier,
data: BufferSource,
): Promise<ArrayBuffer> {
if (delayFirstDigest) {
delayFirstDigest = false;
digestStarted.resolve(undefined);
await releaseDigest.promise;
}
return await realCrypto.subtle.digest(algorithm, data);
},
},
} as unknown as Crypto;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: delayedCrypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
const staging = adapter.admin.stageRelease(manifest);
await digestStarted.promise;
manifest.releaseRegistryId = "mutated-during-crypto";
manifest.manifestDigestHex = "0".repeat(64);
manifest.assets[0]!.absoluteUrl =
"https://assets.example.test/mutated-crypto.js";
releaseDigest.resolve(undefined);
await expect(staging).resolves.toMatchObject({
ok: true,
value: { releaseRegistryId: "snapshot-crypto" },
});
await expect(
adapter.admin.activateRelease(
signed.releaseRegistryId,
signed.manifestDigestHex,
),
).resolves.toMatchObject({ ok: true });
});
it("keeps the original stage signal while digest verification is pending", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([8, 6, 4, 2]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/snapshot-signal.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const manifest = await manifestFor(
"snapshot-signal",
[asset],
policy,
);
const digestStarted = deferred<void>();
const releaseDigest = deferred<void>();
let delayFirstDigest = true;
const realCrypto = globalThis.crypto;
const delayedCrypto = {
subtle: {
async digest(
algorithm: AlgorithmIdentifier,
data: BufferSource,
): Promise<ArrayBuffer> {
if (delayFirstDigest) {
delayFirstDigest = false;
digestStarted.resolve(undefined);
await releaseDigest.promise;
}
return await realCrypto.subtle.digest(algorithm, data);
},
},
} as unknown as Crypto;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: delayedCrypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
const original = new AbortController();
const replacement = new AbortController();
const options = { signal: original.signal };
const staging = adapter.admin.stageRelease(manifest, options);
await digestStarted.promise;
options.signal = replacement.signal;
original.abort();
releaseDigest.resolve(undefined);
await expect(staging).resolves.toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
await expect(cacheStorage.keys()).resolves.toEqual([]);
});
it("keeps the original activation signal while waiting for the mutation lock", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([7]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/activate-signal.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const manifest = await manifestFor("activate-signal", [asset], policy);
const lockStarted = deferred<void>();
const releaseLock = deferred<void>();
let delayLock = false;
const lock: PublicCacheMutationLock = {
async run(_signal, task) {
if (delayLock) {
lockStarted.resolve(undefined);
await releaseLock.promise;
}
return await task();
},
};
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: lock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
await expect(adapter.admin.stageRelease(manifest)).resolves.toMatchObject({
ok: true,
});
delayLock = true;
const original = new AbortController();
const replacement = new AbortController();
const options = { signal: original.signal };
const activation = adapter.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
options,
);
await lockStarted.promise;
options.signal = replacement.signal;
original.abort();
releaseLock.resolve(undefined);
await expect(activation).resolves.toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
expect(await cacheStorage.keys()).not.toContain("ca-public-v1:control");
});
it("keeps the original lookup signal while cache discovery is pending", async () => {
const cacheStorage = new MemoryCacheStorage();
const baseKeys = cacheStorage.keys.bind(cacheStorage);
const lookupStarted = deferred<void>();
const releaseLookup = deferred<void>();
let delayKeys = false;
cacheStorage.keys = async () => {
if (delayKeys) {
lookupStarted.resolve(undefined);
await releaseLookup.promise;
delayKeys = false;
}
return await baseKeys();
};
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([9]);
const absoluteUrl =
"https://assets.example.test/lookup-signal.js";
const asset: PublicCacheAsset = {
absoluteUrl,
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const manifest = await manifestFor("lookup-signal", [asset], policy);
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
await expect(adapter.admin.stageRelease(manifest)).resolves.toMatchObject({
ok: true,
});
await expect(
adapter.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
),
).resolves.toMatchObject({ ok: true });
delayKeys = true;
const original = new AbortController();
const replacement = new AbortController();
const request = { absoluteUrl, signal: original.signal };
const lookup = adapter.responses.matchActiveExact(request);
await lookupStarted.promise;
request.signal = replacement.signal;
original.abort();
releaseLookup.resolve(undefined);
await expect(lookup).resolves.toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
});
it("captures runtime dependencies when the adapter is composed", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([5, 4, 3, 2, 1]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/dependency-snapshot.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const manifest = await manifestFor(
"dependency-snapshot",
[asset],
policy,
);
const mutableSubtle = {
async digest(
algorithm: AlgorithmIdentifier,
data: BufferSource,
): Promise<ArrayBuffer> {
return await globalThis.crypto.subtle.digest(algorithm, data);
},
};
const mutableCrypto = {
subtle: mutableSubtle,
} as unknown as Crypto;
const mutationLock: PublicCacheMutationLock = {
async run(_signal, task) {
return await task();
},
};
const dependencies = {
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: mutableCrypto,
mutationLock,
policy,
fetcher: async () =>
new Response(bytes, {
status: 200,
headers: {
"cache-control": "public, max-age=31536000, immutable",
"content-type": "application/javascript",
},
}),
};
const adapter = createPublicResponseCacheAdapter(dependencies);
dependencies.fetcher = async () => {
throw new TypeError("mutated dependency must not be observed");
};
cacheStorage.keys = async () => {
throw new TypeError("mutated CacheStorage method must not be observed");
};
mutationLock.run = async () => {
throw new TypeError("mutated lock method must not be observed");
};
mutableSubtle.digest = async () => {
throw new TypeError("mutated crypto method must not be observed");
};
await expect(
adapter.admin.stageRelease(manifest),
).resolves.toMatchObject({
ok: true,
value: { releaseRegistryId: "dependency-snapshot" },
});
});
it("keeps the verified manifest snapshot while waiting for the mutation lock", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([1, 3, 5, 7]);
const asset = {
absoluteUrl: "https://assets.example.test/snapshot-lock.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256" as const,
digestHex: await digestHex(bytes),
},
};
const signed = await manifestFor("snapshot-lock", [asset], policy);
const manifest = {
releaseRegistryId: signed.releaseRegistryId,
assets: [asset],
manifestDigestHex: signed.manifestDigestHex,
};
const lockStarted = deferred<void>();
const releaseLock = deferred<void>();
const delayedLock: PublicCacheMutationLock = {
async run(_signal, task) {
lockStarted.resolve(undefined);
await releaseLock.promise;
return await task();
},
};
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: delayedLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
const staging = adapter.admin.stageRelease(manifest);
await lockStarted.promise;
manifest.releaseRegistryId = "mutated-during-lock";
manifest.manifestDigestHex = "f".repeat(64);
manifest.assets[0]!.absoluteUrl =
"https://assets.example.test/mutated-lock.js";
releaseLock.resolve(undefined);
await expect(staging).resolves.toMatchObject({
ok: true,
value: { releaseRegistryId: "snapshot-lock" },
});
await expect(
adapter.admin.activateRelease(
signed.releaseRegistryId,
signed.manifestDigestHex,
),
).resolves.toMatchObject({ ok: true });
});
it("fails closed for private responses and deletes the incomplete candidate", async () => {
const cacheStorage = new MemoryCacheStorage();
const bytes = new Uint8Array([9]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/private.json",
expectedByteLength: 1,
expectedContentType: "application/json",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const manifest = await manifestFor("release-private", [asset]);
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy: createDefaultPublicCachePolicy(
"https://assets.example.test",
),
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "private, max-age=60",
"content-type": "application/json",
},
}),
});
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
expect(await cacheStorage.keys()).toEqual([]);
});
it("binds the exact expected Content-Type into the signed manifest and cached response", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([7, 7]);
const baseAsset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/content.bin",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/json",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const jsonManifest = await manifestFor(
"content-json",
[baseAsset],
policy,
);
const javascriptManifest = await manifestFor(
"content-js",
[
{
...baseAsset,
expectedContentType: "application/javascript",
},
],
policy,
);
expect(jsonManifest.manifestDigestHex).not.toBe(
javascriptManifest.manifestDigestHex,
);
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
expect(
await adapter.admin.stageRelease(jsonManifest),
).toMatchObject({
ok: false,
error: { code: "INTEGRITY_FAILED" },
});
expect(await cacheStorage.keys()).toEqual([]);
});
it("deletes only caches owned by its prefix", async () => {
const cacheStorage = new MemoryCacheStorage();
await cacheStorage.open("unrelated-cache");
await cacheStorage.open("ca-public-v1:stale");
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy: createDefaultPublicCachePolicy(
"https://assets.example.test",
),
fetcher: async () => new Response(),
});
expect(await adapter.admin.cleanupOwned()).toMatchObject({
ok: true,
value: { deletedOwnedCaches: 1 },
});
expect(await cacheStorage.keys()).toContain("unrelated-cache");
});
it("does not let callers retain an unreferenced staged release", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([3, 1, 4]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/orphan.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const manifest = await manifestFor("orphan-release", [asset], policy);
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
await expect(adapter.admin.stageRelease(manifest)).resolves.toMatchObject({
ok: true,
});
const legacyCallerRetention = {
retainReleaseRegistryIds: [manifest.releaseRegistryId],
} as unknown as Parameters<
typeof adapter.admin.cleanupOwned
>[0];
await expect(
adapter.admin.cleanupOwned(legacyCallerRetention),
).resolves.toMatchObject({
ok: true,
value: {
deletedOwnedCaches: 1,
retainedOwnedCaches: 0,
},
});
await expect(cacheStorage.keys()).resolves.toEqual([]);
});
it("keeps the original cleanup signal while waiting for the mutation lock", async () => {
const cacheStorage = new MemoryCacheStorage();
await cacheStorage.open("ca-public-v1:stale");
const lockStarted = deferred<void>();
const releaseLock = deferred<void>();
const lock: PublicCacheMutationLock = {
async run(_signal, task) {
lockStarted.resolve(undefined);
await releaseLock.promise;
return await task();
},
};
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: lock,
policy: createDefaultPublicCachePolicy(
"https://assets.example.test",
),
fetcher: async () => new Response(),
});
const original = new AbortController();
const replacement = new AbortController();
const request = { signal: original.signal };
const cleanup = adapter.admin.cleanupOwned(request);
await lockStarted.promise;
request.signal = replacement.signal;
original.abort();
releaseLock.resolve(undefined);
await expect(cleanup).resolves.toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
await expect(cacheStorage.keys()).resolves.toContain(
"ca-public-v1:stale",
);
});
it("cancels oversized streamed control JSON before reading the remainder", async () => {
let pulls = 0;
let cancelled = false;
const oversizedControlResponse = new Response(
new ReadableStream<Uint8Array>({
pull(controller) {
pulls += 1;
if (pulls === 1) {
controller.enqueue(new Uint8Array(1_500_000));
return;
}
if (pulls === 2) {
controller.enqueue(new Uint8Array(1_000_000));
return;
}
controller.enqueue(new Uint8Array([1]));
},
cancel() {
cancelled = true;
},
}),
{
headers: { "content-type": "application/json" },
},
);
const candidate = {
async match(): Promise<Response> {
return oversizedControlResponse;
},
async put(): Promise<void> {},
};
const cacheStorage = {
async open(): Promise<Cache> {
return candidate as unknown as Cache;
},
async keys(): Promise<string[]> {
return ["ca-public-v1:release:oversized:control"];
},
async delete(): Promise<boolean> {
return false;
},
};
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
policy: createDefaultPublicCachePolicy(
"https://assets.example.test",
),
});
await expect(adapter.admin.inspect()).resolves.toMatchObject({
ok: true,
value: {
ownedCacheCount: 1,
unreadableOwnedCacheCount: 1,
},
});
// A stream may prefetch one chunk at its high-water mark, but the
// consumer must not continue pulling the remaining source.
expect(pulls).toBeLessThanOrEqual(3);
expect(cancelled).toBe(true);
});
it("retains the active and previous verified release for rollback", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bodies = new Map<string, Uint8Array>();
const manifests: PublicCacheReleaseManifest[] = [];
for (const release of ["release-a", "release-b"]) {
const url = `https://assets.example.test/${release}.js`;
const bytes = new TextEncoder().encode(release);
bodies.set(url, bytes);
const asset: PublicCacheAsset = {
absoluteUrl: url,
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
manifests.push(await manifestFor(release, [asset], policy));
}
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async (request) =>
new Response(
Uint8Array.from(
bodies.get(request.url) ?? new Uint8Array(),
),
{
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
},
),
});
for (const manifest of manifests) {
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: true,
});
expect(
await adapter.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
),
).toMatchObject({ ok: true });
}
await cacheStorage.open("ca-public-v1:stale");
const cleaned = await adapter.admin.cleanupOwned();
expect(cleaned).toMatchObject({
ok: true,
value: {
deletedOwnedCaches: 1,
retainedOwnedCaches: 3,
},
});
const releaseCaches = (await cacheStorage.keys()).filter((name) =>
name.startsWith("ca-public-v1:release:"),
);
expect(releaseCaches).toHaveLength(2);
});
it("requires allowlisted exact query values and rejects duplicates", async () => {
const base = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const policy: PublicCacheRuntimePolicy = {
...base,
allowedQueryParameterNames: ["v"],
isQueryParameterValueAllowed: (name, value) =>
name === "v" && /^r\d+$/u.test(value),
};
const bytes = new Uint8Array([1]);
const allowed: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/app.js?v=r1",
expectedByteLength: 1,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
await expect(
manifestFor(
"duplicate-query",
[
{
...allowed,
absoluteUrl:
"https://assets.example.test/app.js?v=r1&v=r2",
},
],
policy,
),
).rejects.toThrow();
await expect(
manifestFor(
"signed-query",
[
{
...allowed,
absoluteUrl:
"https://assets.example.test/app.js?sig=secret",
},
],
policy,
),
).rejects.toThrow();
});
it("supports exact Vary variants and rejects missing or extra Vary", async () => {
const cacheStorage = new MemoryCacheStorage();
const base = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const policy: PublicCacheRuntimePolicy = {
...base,
allowedVaryHeaderNames: ["accept-language"],
};
const url = "https://assets.example.test/messages.json";
const variants = [
{ language: "en", bytes: new TextEncoder().encode("hello") },
{ language: "ko", bytes: new TextEncoder().encode("안녕") },
];
const assets = await Promise.all(
variants.map(async ({ language, bytes }) => ({
absoluteUrl: url,
expectedByteLength: bytes.byteLength,
expectedContentType: "application/json",
requestHeaders: [["accept-language", language]] as const,
integrity: {
algorithm: "SHA-256" as const,
digestHex: await digestHex(bytes),
},
})),
);
const manifest = await manifestFor("language-release", assets, policy);
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async (request) => {
const variant = variants.find(
({ language }) =>
language === request.headers.get("accept-language"),
);
return new Response(variant?.bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/json",
vary: "Accept-Language",
},
});
},
});
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: true,
});
expect(
await adapter.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
),
).toMatchObject({ ok: true });
for (const variant of variants) {
const match = await adapter.responses.matchActiveExact({
absoluteUrl: url,
requestHeaders: [["accept-language", variant.language]],
});
expect(match).toMatchObject({ ok: true });
if (!match.ok || !match.value) throw new Error("variant cache miss");
const chunks: number[] = [];
for await (const chunk of match.value.body.stream(
new AbortController().signal,
)) {
expect(chunk.ok).toBe(true);
if (chunk.ok) chunks.push(...chunk.value);
}
expect(chunks).toEqual([...variant.bytes]);
}
const missingVary = createPublicResponseCacheAdapter({
cacheStorage: new MemoryCacheStorage() as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(variants[0]!.bytes, {
headers: {
"cache-control": "public",
"content-type": "application/json",
},
}),
});
expect(
await missingVary.admin.stageRelease(
await manifestFor("missing-vary", [assets[0]!], policy),
),
).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
const noHeaderAsset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/plain.json",
expectedByteLength: variants[0]!.bytes.byteLength,
expectedContentType: "application/json",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(variants[0]!.bytes),
},
};
const extraVary = createPublicResponseCacheAdapter({
cacheStorage: new MemoryCacheStorage() as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(variants[0]!.bytes, {
headers: {
"cache-control": "public",
"content-type": "application/json",
vary: "Accept-Language",
},
}),
});
expect(
await extraVary.admin.stageRelease(
await manifestFor("extra-vary", [noHeaderAsset], policy),
),
).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
});
it("propagates AbortSignal to fetch and cancels a pending body read", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([1]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/slow.js",
expectedByteLength: 1,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const manifest = await manifestFor("abort-release", [asset], policy);
let networkSignal: AbortSignal | undefined;
let bodyCancelled = false;
let signalCaptured: (() => void) | undefined;
const fetchStarted = new Promise<void>((resolve) => {
signalCaptured = resolve;
});
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async (request) => {
networkSignal = request.signal;
signalCaptured?.();
return new Response(
new ReadableStream<Uint8Array>({
cancel() {
bodyCancelled = true;
},
}),
{
headers: {
"cache-control": "public",
"content-type": "application/javascript",
},
},
);
},
});
const controller = new AbortController();
const staged = adapter.admin.stageRelease(manifest, {
signal: controller.signal,
});
await fetchStarted;
controller.abort();
expect(await staged).toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
expect(networkSignal?.aborted).toBe(true);
expect(bodyCancelled).toBe(true);
expect(await cacheStorage.keys()).toEqual([]);
});
});