feat: unit tests

This commit is contained in:
2026-03-18 15:30:59 +07:00 Unverified
parent 6508f8244b
commit 3bd8a37bf8
5 changed files with 216 additions and 1 deletions
+83
View File
@@ -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);
});
});
+32
View File
@@ -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"] });
});
});
+95
View File
@@ -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;
});
});