200 lines
6.5 KiB
TypeScript
200 lines
6.5 KiB
TypeScript
import {
|
|
afterEach,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi,
|
|
} from "@suaveplan/testing/runner";
|
|
import type { FetchLike } from "@suaveplan/types";
|
|
import {
|
|
STORAGE_CDN_BUNNYCDN_CODES,
|
|
StorageCdnBunnycdnError,
|
|
} from "../error-codes/error-codes.js";
|
|
import { setBunnyMeterProvider } from "../otel/otel.js";
|
|
import { BunnyCdnAdapter } from "./bunny-cdn-adapter.js";
|
|
|
|
function makeFetch(
|
|
...responses: Response[]
|
|
): FetchLike & { calls: { url: string; init: RequestInit | undefined }[] } {
|
|
let i = 0;
|
|
const calls: { url: string; init: RequestInit | undefined }[] = [];
|
|
const fn: FetchLike = async (input, init) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
calls.push({ url, init });
|
|
return responses[i++] ?? new Response(null, { status: 200 });
|
|
};
|
|
(fn as FetchLike & { calls: typeof calls }).calls = calls;
|
|
return fn as FetchLike & { calls: typeof calls };
|
|
}
|
|
|
|
function makeAdapter(
|
|
fetch: FetchLike,
|
|
overrides: Record<string, unknown> = {}
|
|
) {
|
|
return new BunnyCdnAdapter({
|
|
storageZone: "myz",
|
|
storageAccessKey: "ak",
|
|
pullZoneHostname: "mycdn.b-cdn.net",
|
|
storageRegion: "de",
|
|
tokenAuthKey: "secret",
|
|
purgeApiKey: "purgek",
|
|
fetch,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
describe("BunnyCdnAdapter extended coverage", () => {
|
|
beforeEach(() => setBunnyMeterProvider(undefined));
|
|
afterEach(() => setBunnyMeterProvider(undefined));
|
|
|
|
it("accepts custom timers in retry path", { timeout: 5000 }, async () => {
|
|
const setSpy = vi.fn((handler: () => void, ms: number) =>
|
|
Number(globalThis.setTimeout(handler, ms))
|
|
);
|
|
const clearSpy = vi.fn((id: number) => globalThis.clearTimeout(id));
|
|
const fetchFn = makeFetch(
|
|
new Response(null, { status: 503 }),
|
|
new Response(new Uint8Array([1]), { status: 200 })
|
|
);
|
|
const storage = makeAdapter(fetchFn, {
|
|
retry: { maxAttempts: 2, baseDelayMs: 1, honourRetryAfter: false },
|
|
timers: { setTimer: setSpy, clearTimer: clearSpy },
|
|
});
|
|
const bytes = await storage.get("k");
|
|
expect(Array.from(bytes as Uint8Array)).toEqual([1]);
|
|
expect(setSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it("accepts originBaseUrl and edgeBaseUrl options", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const fetchFn = makeFetch();
|
|
const storage = new BunnyCdnAdapter({
|
|
storageZone: "myz",
|
|
storageAccessKey: "ak",
|
|
pullZoneHostname: "mycdn.b-cdn.net",
|
|
originBaseUrl: "https://example.com/",
|
|
edgeBaseUrl: "https://e.example.com/",
|
|
fetch: fetchFn,
|
|
});
|
|
expect(storage).toBeInstanceOf(BunnyCdnAdapter);
|
|
});
|
|
|
|
it("rejects originBaseUrl without trailing slash", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
new BunnyCdnAdapter({
|
|
storageZone: "myz",
|
|
storageAccessKey: "ak",
|
|
pullZoneHostname: "mycdn.b-cdn.net",
|
|
originBaseUrl: "https://example.com",
|
|
});
|
|
expect.fail("expected throw");
|
|
} catch (err) {
|
|
expect((err as StorageCdnBunnycdnError).code).toBe(
|
|
STORAGE_CDN_BUNNYCDN_CODES.INVALID_OPTIONS
|
|
);
|
|
}
|
|
});
|
|
|
|
it("threads signal into successful get fetch init", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFetch(
|
|
new Response(new Uint8Array([1]), { status: 200 })
|
|
);
|
|
const c = new AbortController();
|
|
await makeAdapter(fetchFn).get("k", c.signal);
|
|
expect(fetchFn.calls[0]?.init?.signal).toBe(c.signal);
|
|
});
|
|
|
|
it("rejects edgeBaseUrl without trailing slash", { timeout: 5000 }, () => {
|
|
try {
|
|
new BunnyCdnAdapter({
|
|
storageZone: "myz",
|
|
storageAccessKey: "ak",
|
|
pullZoneHostname: "mycdn.b-cdn.net",
|
|
edgeBaseUrl: "https://e.example.com",
|
|
});
|
|
expect.fail("expected throw");
|
|
} catch (err) {
|
|
expect((err as StorageCdnBunnycdnError).code).toBe(
|
|
STORAGE_CDN_BUNNYCDN_CODES.INVALID_OPTIONS
|
|
);
|
|
}
|
|
});
|
|
|
|
it("PUT emits writes_total with provider and storage_region attrs", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFetch(new Response(null, { status: 201 }));
|
|
await makeAdapter(fetchFn).set("k", new Uint8Array([1]));
|
|
expect(
|
|
fetchFn.calls.some(
|
|
(c) =>
|
|
c.init?.method === "PUT" &&
|
|
c.url.startsWith("https://storage.bunnycdn.com")
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
it("createReadStream errors mid-stream on non-ok", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFetch(new Response(null, { status: 500 }));
|
|
const reader = makeAdapter(fetchFn).createReadStream("k").getReader();
|
|
try {
|
|
await reader.read();
|
|
expect.fail("expected throw");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(StorageCdnBunnycdnError);
|
|
}
|
|
});
|
|
|
|
it("createReadStream closes when body is null", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFetch(new Response(null, { status: 200 }));
|
|
const reader = makeAdapter(fetchFn).createReadStream("k").getReader();
|
|
expect((await reader.read()).done).toBe(true);
|
|
});
|
|
|
|
it("default fetch resolves via globalThis.fetch when omitted", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const spy = vi
|
|
.spyOn(globalThis, "fetch")
|
|
.mockResolvedValue(
|
|
new Response(new Uint8Array([1]), { status: 200 })
|
|
);
|
|
try {
|
|
const storage = new BunnyCdnAdapter({
|
|
storageZone: "myz",
|
|
storageAccessKey: "ak",
|
|
pullZoneHostname: "mycdn.b-cdn.net",
|
|
});
|
|
await storage.get("k");
|
|
expect(spy).toHaveBeenCalled();
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("createBunnyCdnAdapter factory returns an instance", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { createBunnyCdnAdapter } = await import(
|
|
"./bunny-cdn-adapter.js"
|
|
);
|
|
const storage = createBunnyCdnAdapter({
|
|
storageZone: "myz",
|
|
storageAccessKey: "ak",
|
|
pullZoneHostname: "mycdn.b-cdn.net",
|
|
fetch: makeFetch(),
|
|
});
|
|
expect(storage).toBeInstanceOf(BunnyCdnAdapter);
|
|
});
|
|
});
|