155 lines
5.0 KiB
TypeScript
155 lines
5.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { parsePurgeArgs, purgeBunnyCache, resolveAbsolutePurgeUrl } from "./bunny-purge";
|
|
|
|
describe("parsePurgeArgs", () => {
|
|
it("returns defaults for an empty argv", () => {
|
|
expect(parsePurgeArgs([])).toEqual({ paths: [], dryRun: false });
|
|
});
|
|
|
|
it("parses --dry-run", () => {
|
|
expect(parsePurgeArgs(["--dry-run"])).toEqual({ paths: [], dryRun: true });
|
|
});
|
|
|
|
it("parses a single --paths value", () => {
|
|
expect(parsePurgeArgs(["--paths", "/assets/app.js"])).toEqual({
|
|
paths: ["/assets/app.js"],
|
|
dryRun: false,
|
|
});
|
|
});
|
|
|
|
it("splits comma-separated --paths into multiple entries", () => {
|
|
expect(parsePurgeArgs(["--paths", "/a, /b,/c"])).toEqual({
|
|
paths: ["/a", "/b", "/c"],
|
|
dryRun: false,
|
|
});
|
|
});
|
|
|
|
it("accepts repeated --paths flags", () => {
|
|
expect(parsePurgeArgs(["--paths", "/a", "--paths", "/b"])).toEqual({
|
|
paths: ["/a", "/b"],
|
|
dryRun: false,
|
|
});
|
|
});
|
|
|
|
it("treats --path as an alias for --paths", () => {
|
|
expect(parsePurgeArgs(["--path", "/x"])).toEqual({ paths: ["/x"], dryRun: false });
|
|
});
|
|
|
|
it("throws on an unknown argument", () => {
|
|
expect(() => parsePurgeArgs(["--unknown"])).toThrow(/Unknown argument/);
|
|
});
|
|
|
|
it("throws when --paths is supplied without a value", () => {
|
|
expect(() => parsePurgeArgs(["--paths"])).toThrow(/expects a value/);
|
|
});
|
|
});
|
|
|
|
describe("resolveAbsolutePurgeUrl", () => {
|
|
it("returns the input unchanged when already absolute", () => {
|
|
expect(resolveAbsolutePurgeUrl("https://cdn.example.com/x.js")).toBe("https://cdn.example.com/x.js");
|
|
});
|
|
|
|
it("joins relative paths against the CDN base", () => {
|
|
expect(resolveAbsolutePurgeUrl("/assets/app.js", "https://cdn.example.com")).toBe(
|
|
"https://cdn.example.com/assets/app.js"
|
|
);
|
|
});
|
|
|
|
it("throws when relative paths are passed without a CDN base", () => {
|
|
expect(() => resolveAbsolutePurgeUrl("/assets/app.js")).toThrow(/relative/);
|
|
});
|
|
|
|
it("rejects empty paths", () => {
|
|
expect(() => resolveAbsolutePurgeUrl(" ")).toThrow(/Empty path/);
|
|
});
|
|
});
|
|
|
|
describe("purgeBunnyCache", () => {
|
|
function makeOkFetch() {
|
|
return vi.fn(async () => Promise.resolve({ ok: true, status: 200 } as Response));
|
|
}
|
|
|
|
it("calls the whole-zone purge endpoint by default", async () => {
|
|
const fetchImpl = makeOkFetch();
|
|
const result = await purgeBunnyCache({
|
|
pullZoneId: "12345",
|
|
apiKey: "secret",
|
|
fetchImpl,
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledOnce();
|
|
const [url, init] = fetchImpl.mock.calls[0];
|
|
expect(String(url)).toBe("https://api.bunny.net/pullzone/12345/purgeCache");
|
|
expect(init?.method).toBe("POST");
|
|
expect((init?.headers as Record<string, string>).AccessKey).toBe("secret");
|
|
expect(result).toEqual({
|
|
url: "https://api.bunny.net/pullzone/12345/purgeCache",
|
|
requests: 1,
|
|
ok: true,
|
|
failedStatus: undefined,
|
|
});
|
|
});
|
|
|
|
it("calls /purge?url=... once per path when paths are supplied", async () => {
|
|
const fetchImpl = makeOkFetch();
|
|
const result = await purgeBunnyCache({
|
|
pullZoneId: "12345",
|
|
apiKey: "secret",
|
|
paths: ["/assets/a.js", "/assets/b.js"],
|
|
cdnBaseUrl: "https://cdn.example.com",
|
|
fetchImpl,
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
expect(String(fetchImpl.mock.calls[0][0])).toBe(
|
|
"https://api.bunny.net/purge?url=https%3A%2F%2Fcdn.example.com%2Fassets%2Fa.js"
|
|
);
|
|
expect(String(fetchImpl.mock.calls[1][0])).toBe(
|
|
"https://api.bunny.net/purge?url=https%3A%2F%2Fcdn.example.com%2Fassets%2Fb.js"
|
|
);
|
|
expect(result.ok).toBe(true);
|
|
expect(result.requests).toBe(2);
|
|
});
|
|
|
|
it("surfaces the first non-2xx status as a failure", async () => {
|
|
const fetchImpl = vi.fn(async () => Promise.resolve({ ok: false, status: 403 } as Response));
|
|
const result = await purgeBunnyCache({
|
|
pullZoneId: "12345",
|
|
apiKey: "secret",
|
|
fetchImpl,
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
expect(result.failedStatus).toBe(403);
|
|
});
|
|
|
|
it("stops iterating paths after the first failure and reports the failed status", async () => {
|
|
let calls = 0;
|
|
const fetchImpl = vi.fn(async () => {
|
|
calls += 1;
|
|
if (calls === 2) return { ok: false, status: 502 } as Response;
|
|
return { ok: true, status: 200 } as Response;
|
|
});
|
|
const result = await purgeBunnyCache({
|
|
pullZoneId: "12345",
|
|
apiKey: "secret",
|
|
paths: ["/a", "/b", "/c"],
|
|
cdnBaseUrl: "https://cdn.example.com",
|
|
fetchImpl,
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
expect(result.failedStatus).toBe(502);
|
|
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("throws when pullZoneId is missing", async () => {
|
|
await expect(purgeBunnyCache({ pullZoneId: "", apiKey: "x", fetchImpl: makeOkFetch() })).rejects.toThrow(
|
|
/BUNNY_PULL_ZONE_ID/
|
|
);
|
|
});
|
|
|
|
it("throws when apiKey is missing", async () => {
|
|
await expect(purgeBunnyCache({ pullZoneId: "1", apiKey: "", fetchImpl: makeOkFetch() })).rejects.toThrow(
|
|
/BUNNY_API_KEY/
|
|
);
|
|
});
|
|
});
|