feat: unit tests
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { file, hash } from "bun";
|
||||
import { AssetManager } from "@/lib/assets";
|
||||
|
||||
describe("AssetManager", () => {
|
||||
let origCwd: string;
|
||||
let tempDir: string;
|
||||
let manager: AssetManager;
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(async () => {
|
||||
origCwd = process.cwd();
|
||||
tempDir = await mkdtemp(join(tmpdir(), "astral-api-test-"));
|
||||
process.chdir(tempDir);
|
||||
await mkdir("store", { recursive: true });
|
||||
manager = new AssetManager();
|
||||
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
process.chdir(origCwd);
|
||||
globalThis.fetch = originalFetch;
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("fetches an asset once and caches result", async () => {
|
||||
let fetchCount = 0;
|
||||
|
||||
const fetchMock = (async () => {
|
||||
fetchCount++;
|
||||
return new Response(new Uint8Array([1, 2, 3]), {
|
||||
status: 200,
|
||||
});
|
||||
}) as unknown as typeof fetch;
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
manager.load("path/to/asset.png"),
|
||||
manager.load("path/to/asset.png"),
|
||||
]);
|
||||
|
||||
expect(fetchCount).toBe(1);
|
||||
expect(first).toBe(second);
|
||||
|
||||
// Wait for the background write to complete.
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const stored = await file(
|
||||
join("store", hash("path/to/asset.png").toString(36)),
|
||||
).arrayBuffer();
|
||||
|
||||
expect(new Uint8Array(stored)).toEqual(new Uint8Array(first));
|
||||
});
|
||||
|
||||
it("caches transformed results via hook", async () => {
|
||||
let fetchCount = 0;
|
||||
let transformCount = 0;
|
||||
|
||||
const fetchMock = (async () => {
|
||||
fetchCount++;
|
||||
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
|
||||
}) as unknown as typeof fetch;
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
const transform = async (data: ArrayBuffer) => {
|
||||
transformCount++;
|
||||
const inBytes = new Uint8Array(data);
|
||||
return new Uint8Array(inBytes.map((b) => b + 1)).buffer;
|
||||
};
|
||||
|
||||
const first = await manager.hook("path/to/asset.png", transform);
|
||||
const second = await manager.hook("path/to/asset.png", transform);
|
||||
|
||||
expect(fetchCount).toBe(1);
|
||||
expect(transformCount).toBe(1);
|
||||
expect(new Uint8Array(first)).toEqual(new Uint8Array([2, 3, 4]));
|
||||
expect(first).toBe(second);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
|
||||
describe("PermissionManager", () => {
|
||||
const PUBLIC_API_KEY = "public-key";
|
||||
const PRIVATE_API_KEY = "private-key";
|
||||
|
||||
let pm: typeof import("@/lib/auth")["pm"];
|
||||
let InvalidKey: typeof import("@/lib/auth").InvalidKey;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Ensure the module reads our test keys when it validates the key list.
|
||||
process.env.PUBLIC_API_KEY = PUBLIC_API_KEY;
|
||||
process.env.PRIVATE_API_KEY = PRIVATE_API_KEY;
|
||||
|
||||
const mod = await import("@/lib/auth");
|
||||
pm = mod.pm;
|
||||
InvalidKey = mod.InvalidKey;
|
||||
});
|
||||
|
||||
it("returns InvalidKey for unknown keys", () => {
|
||||
const invalid = pm.validate("unknown");
|
||||
expect(invalid).toBeInstanceOf(InvalidKey);
|
||||
expect(invalid.has("card")).toBe(false);
|
||||
expect(invalid.toJSON()).toEqual({ valid: false, permissions: [] });
|
||||
});
|
||||
|
||||
it("returns a valid key with permissions", () => {
|
||||
const key = pm.validate(PUBLIC_API_KEY);
|
||||
expect(key.has("card")).toBe(true);
|
||||
expect(key.toJSON()).toEqual({ valid: true, permissions: ["card"] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Deduplicator } from "@/lib/dedupe";
|
||||
|
||||
describe("Deduplicator", () => {
|
||||
it("returns same promise for same key while pending", async () => {
|
||||
const dedup = new Deduplicator<number>();
|
||||
|
||||
let callCount = 0;
|
||||
|
||||
const factory = async () => {
|
||||
callCount++;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
return 42;
|
||||
};
|
||||
|
||||
const p1 = dedup.acquire("key", factory);
|
||||
const p2 = dedup.acquire("key", factory);
|
||||
|
||||
expect(p1).toBe(p2); // same promise
|
||||
expect(callCount).toBe(1); // factory only called once
|
||||
|
||||
const result = await p1;
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
it("calls factory again after promise resolves", async () => {
|
||||
const dedup = new Deduplicator<number>();
|
||||
|
||||
let callCount = 0;
|
||||
|
||||
const factory = async () => {
|
||||
callCount++;
|
||||
return 1;
|
||||
};
|
||||
|
||||
await dedup.acquire("key", factory);
|
||||
await dedup.acquire("key", factory);
|
||||
|
||||
expect(callCount).toBe(2); // should not cache after resolve
|
||||
});
|
||||
|
||||
it.todo("removes key after rejection", async () => {
|
||||
const dedup = new Deduplicator<number>();
|
||||
|
||||
let callCount = 0;
|
||||
|
||||
const factory = () =>
|
||||
new Promise<number>((_, j) => {
|
||||
callCount++;
|
||||
j("fail");
|
||||
});
|
||||
|
||||
expect(dedup.acquire("key", factory).catch(() => "fail")).resolves.toBe(
|
||||
"fail",
|
||||
);
|
||||
|
||||
// should retry after failure
|
||||
expect(dedup.acquire("key", factory).catch(() => "fail")).resolves.toBe(
|
||||
"fail",
|
||||
);
|
||||
|
||||
expect(callCount).toBe(2);
|
||||
});
|
||||
|
||||
it("has() reflects current state", async () => {
|
||||
const dedup = new Deduplicator<number>();
|
||||
|
||||
let resolve!: (v: number) => void;
|
||||
const promise = new Promise<number>((r) => (resolve = r));
|
||||
|
||||
dedup.acquire("key", () => promise);
|
||||
|
||||
expect(dedup.has("key")).toBe(true);
|
||||
|
||||
resolve(123);
|
||||
await promise;
|
||||
|
||||
// wait a tick for finally()
|
||||
await Promise.resolve();
|
||||
|
||||
expect(dedup.has("key")).toBe(false);
|
||||
});
|
||||
|
||||
it("get() returns current promise", async () => {
|
||||
const dedup = new Deduplicator<number>();
|
||||
|
||||
const p = dedup.acquire("key", async () => 5);
|
||||
|
||||
const stored = dedup.get("key");
|
||||
|
||||
expect(stored).toBe(p);
|
||||
|
||||
await p;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user