test/scripts/static-cdn-publish.test.ts
Rama main · réplica de lectura del repositorio original.
import { assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import { FakeTime } from "@std/testing/time";
import {
publishStaticCdnAssets,
type StaticCdnAsset,
} from "#scripts/static-cdn.ts";
import {
assetResponse,
CONFIG,
STYLE_ASSET,
successfulFetcher,
verificationFetcher,
WASM_ASSET,
} from "./static-cdn-fixtures.ts";
const STALLED_REQUESTS = [
{ method: "PUT", stage: "upload" },
{ method: "POST", stage: "purge" },
{ method: "GET", stage: "verification" },
] as const;
const styleOrWasm =
(style: StaticCdnAsset) =>
(url: string): StaticCdnAsset =>
url.endsWith(STYLE_ASSET.filename) ? style : WASM_ASSET;
const completeRequest = successfulFetcher(() => STYLE_ASSET);
const rejectWhenAborted = (signal: AbortSignal): Promise<Response> =>
new Promise((_resolve, reject) => {
signal.addEventListener("abort", () => reject(signal.reason), {
once: true,
});
});
const stalledFetcher =
(stalledMethod: string, onStart: () => void): typeof fetch =>
(input, init) => {
const method = init?.method ?? "GET";
if (method !== stalledMethod) return completeRequest(input, init);
const signal = init?.signal;
if (!signal) throw new Error("Expected bounded Bunny request");
const stalled = rejectWhenAborted(signal);
onStart();
return stalled;
};
describe("publishStaticCdnAssets", () => {
test("uploads one immutable release and returns its public URLs", async () => {
const requests: Request[] = [];
const assetForUrl = styleOrWasm(STYLE_ASSET);
const fetcher: typeof fetch = (input, init) => {
const request = new Request(input, init);
requests.push(request);
const asset = assetForUrl(request.url);
return Promise.resolve(
request.method === "GET"
? assetResponse(asset)
: new Response(null, { status: 201 }),
);
};
const published = await publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET, WASM_ASSET],
fetcher,
);
expect(published.origin).toBe("https://assets.example.com");
expect(published.urls["style.css"]).toBe(
"https://assets.example.com/static/assets/d4632641703f36d87d3ae3a6fd8f4849ad16f9ff42795ad26f25c3fe42a8bf1e/style.css",
);
expect(published.urls["jpegDec.wasm"]).toBe(
"https://assets.example.com/static/assets/d4632641703f36d87d3ae3a6fd8f4849ad16f9ff42795ad26f25c3fe42a8bf1e/jpegDec.wasm",
);
expect(requests.length).toBe(5);
const cssUploadUrl =
"https://storage.bunnycdn.com/tickets-assets/static/assets/" +
"d4632641703f36d87d3ae3a6fd8f4849ad16f9ff42795ad26f25c3fe42a8bf1e/style.css";
const cssUpload = requests.find(({ url }) => url === cssUploadUrl);
if (!cssUpload) throw new Error("Expected style.css upload request");
expect(cssUpload.method).toBe("PUT");
expect(cssUpload.headers.get("accesskey")).toBe("storage-secret");
expect(cssUpload.headers.get("content-type")).toBe(
"text/css; charset=utf-8",
);
expect(cssUpload.headers.get("checksum")).toBe(
"7C98040A541657584690AE2A1CC3B42A8B53B159CC60C5D3ABBFECBAEAC6C94A",
);
expect(cssUpload.url).toBe(cssUploadUrl);
expect(requests[2]?.url).toBe(
"https://api.bunny.net/pullzone/12345/purgeCache",
);
expect(requests[2]?.method).toBe("POST");
expect(requests[2]?.headers.get("accesskey")).toBe("account-secret");
expect(requests[3]?.method).toBe("GET");
expect(requests[3]?.url).toBe(published.urls[STYLE_ASSET.filename]);
expect(requests[4]?.url).toBe(published.urls[WASM_ASSET.filename]);
});
test("release URLs ignore input order and change with content", async () => {
const original = await publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET, WASM_ASSET],
successfulFetcher(styleOrWasm(STYLE_ASSET)),
);
const reversed = await publishStaticCdnAssets(
CONFIG,
[WASM_ASSET, STYLE_ASSET],
successfulFetcher(styleOrWasm(STYLE_ASSET)),
);
const changedStyle = {
...STYLE_ASSET,
bytes: new TextEncoder().encode("x"),
};
const changed = await publishStaticCdnAssets(
CONFIG,
[changedStyle, WASM_ASSET],
successfulFetcher(styleOrWasm(changedStyle)),
);
expect(reversed.urls).toEqual(original.urls);
expect(changed.urls[STYLE_ASSET.filename]).not.toBe(
original.urls[STYLE_ASSET.filename],
);
});
test("release URLs change with content type", async () => {
const changedType = {
...STYLE_ASSET,
contentType: "text/x-css; charset=utf-8",
};
const original = await publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET],
successfulFetcher(() => STYLE_ASSET),
);
const changed = await publishStaticCdnAssets(
CONFIG,
[changedType],
successfulFetcher(() => changedType),
);
expect(changed.urls[STYLE_ASSET.filename]).not.toBe(
original.urls[STYLE_ASSET.filename],
);
});
test("uploads through the configured regional Bunny host", async () => {
let uploadUrl = "";
await publishStaticCdnAssets(
{ ...CONFIG, storageHost: "uk.storage.bunnycdn.com" },
[STYLE_ASSET],
(input, init) => {
const method = init?.method ?? "GET";
if (method === "PUT") uploadUrl = String(input);
return Promise.resolve(
method === "GET"
? assetResponse(STYLE_ASSET)
: new Response(null, { status: 201 }),
);
},
);
expect(uploadUrl).toMatch(
/^https:\/\/uk\.storage\.bunnycdn\.com\/tickets-assets\/static\/assets\/[a-f0-9]{64}\/style\.css$/,
);
});
test("fails the build when Bunny Storage rejects an upload", async () => {
await expect(
publishStaticCdnAssets(
CONFIG,
[
{
bytes: new Uint8Array([1]),
contentType: "application/javascript",
filename: "admin.js",
},
],
() => Promise.resolve(new Response("denied", { status: 401 })),
),
).rejects.toThrow("admin.js");
});
test("rejects duplicate filenames before uploading", async () => {
const asset = {
bytes: new Uint8Array([1]),
contentType: "application/javascript",
filename: "admin.js",
};
let requested = false;
await expect(
publishStaticCdnAssets(CONFIG, [asset, asset], () => {
requested = true;
return Promise.reject(new Error("Unexpected network request"));
}),
).rejects.toThrow("must be unique");
expect(requested).toBe(false);
});
test("fails the build when the pull-zone purge fails", async () => {
await expect(
publishStaticCdnAssets(
{ ...CONFIG, cdnUrl: "https://assets.example.com" },
[],
(input) =>
Promise.resolve(
new Response(null, {
status: String(input).includes("purgeCache") ? 503 : 201,
}),
),
),
).rejects.toThrow("Failed to purge static CDN: HTTP 503");
});
test("fails the build when a public CDN asset is unavailable", async () => {
await expect(
publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET],
verificationFetcher(new Response(null, { status: 404 })),
),
).rejects.toThrow("style.css: HTTP 404");
});
test("fails the build when public CDN bytes differ", async () => {
await expect(
publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET],
verificationFetcher(
new Response("different", {
headers: { "content-type": STYLE_ASSET.contentType },
}),
),
),
).rejects.toThrow("style.css does not match the uploaded file");
});
test("fails the build when the public content type differs", async () => {
await expect(
publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET],
verificationFetcher(
new Response(STYLE_ASSET.bytes, {
headers: { "content-type": "application/octet-stream" },
}),
),
),
).rejects.toThrow(
"Static CDN asset style.css has content type application/octet-stream; expected text/css",
);
});
test("fails the build when the public content type is missing", async () => {
await expect(
publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET],
verificationFetcher(new Response(STYLE_ASSET.bytes)),
),
).rejects.toThrow("Static CDN asset style.css is missing its content type");
});
test("allows Bunny requests to complete before the deadline", async () => {
const fetcher: typeof fetch = (_input, init) => {
const signal = init?.signal;
if (!signal) throw new Error("Expected bounded Bunny request");
const response =
init?.method === undefined
? assetResponse(STYLE_ASSET)
: new Response(null, { status: 201 });
return new Promise((resolve, reject) => {
const onAbort = (): void => {
clearTimeout(timer);
reject(signal.reason);
};
const timer = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve(response);
}, 5);
signal.addEventListener("abort", onAbort, { once: true });
});
};
const published = await publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET],
fetcher,
);
assertStringIncludes(
published.urls[STYLE_ASSET.filename] ?? "",
"/style.css",
);
});
for (const { method: stalledMethod, stage } of STALLED_REQUESTS) {
test(`aborts a stalled ${stage} request after 30 seconds`, async () => {
using time = new FakeTime();
const started = Promise.withResolvers<void>();
let error: unknown;
let settled = false;
const publishing = publishStaticCdnAssets(
CONFIG,
[STYLE_ASSET],
stalledFetcher(stalledMethod, () => started.resolve()),
);
const observed = publishing
.catch((caught: unknown) => {
error = caught;
})
.finally(() => {
settled = true;
});
await started.promise;
await time.tickAsync(29_999);
expect(settled).toBe(false);
await time.tickAsync(1);
await observed;
if (!(error instanceof DOMException)) {
throw new Error(`Expected ${stage} timeout DOMException`);
}
expect(error.name).toBe("TimeoutError");
});
}
});