import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtemp, readdir, rm, utimes, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { MemoryStorage, StorageFullError, type Storage } from "../src/server/storage"; import { DirStorage, RETENTION_MS } from "../src/server/storage-dir"; const name = (c: string, ext = "jpg") => `${c.repeat(64)}.${ext}`; const bytes = (n: number) => new Uint8Array(n).fill(7); let dir: string; beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), "mbseed-test-")); }); afterEach(async () => { await rm(dir, { recursive: true, force: true }); }); const kinds: [string, (max: number) => Promise][] = [ ["memory", async (max) => new MemoryStorage(max)], ["folder", (max) => DirStorage.open(dir, max)], ]; describe.each(kinds)("%s storage", (_kind, make) => { test("put then get", async () => { const s = await make(10_000); await s.put(name("a"), bytes(100)); expect((await s.get(name("a")))!.size).toBe(100); expect(await s.get(name("b"))).toBeNull(); }); test("refuses past the cap; re-uploading the same file doesn't count twice", async () => { const s = await make(1000); await s.put(name("a"), bytes(600)); await s.put(name("a"), bytes(600)); expect(s.usedBytes()).toBe(600); await expect(s.put(name("b"), bytes(600))).rejects.toBeInstanceOf(StorageFullError); expect(s.usedBytes()).toBe(600); }); test("concurrent uploads can't race past the cap", async () => { const s = await make(1000); const results = await Promise.allSettled([s.put(name("a"), bytes(600)), s.put(name("b"), bytes(600))]); expect(results.map((r) => r.status).sort()).toEqual(["fulfilled", "rejected"]); expect(s.usedBytes()).toBe(600); }); test("rejects names that aren't .", async () => { const s = await make(1000); for (const bad of ["../x.jpg", `${"A".repeat(64)}.jpg`, `${"a".repeat(64)}.svg`, "x"]) { await expect(s.put(bad, bytes(1))).rejects.toThrow(); } }); test("isFull when less than 1 MiB is left", async () => { const s = await make(1024 * 1024 + 10); expect(s.isFull()).toBe(false); await s.put(name("a"), bytes(20)); expect(s.isFull()).toBe(true); }); }); describe("folder storage", () => { test("measures existing files at startup and removes stale temp files", async () => { await writeFile(join(dir, name("a")), bytes(300)); await writeFile(join(dir, `${name("b")}.123.tmp`), bytes(50)); await writeFile(join(dir, "README"), "not a cover"); const s = await DirStorage.open(dir, 10_000); expect(s.usedBytes()).toBe(300); expect((await readdir(dir)).sort()).toEqual([name("a"), "README"].sort()); }); test("sweep deletes files older than 30 days and frees their space", async () => { const now = Date.now(); const s = await DirStorage.open(dir, 10_000, () => now); await s.put(name("a"), bytes(100)); await s.put(name("b"), bytes(200)); const old = new Date(now - RETENTION_MS - 60_000); await utimes(join(dir, name("a")), old, old); expect(await s.sweep()).toBe(1); expect(await s.get(name("a"))).toBeNull(); expect(s.usedBytes()).toBe(200); }); test("a re-upload restarts the 30 days", async () => { const now = Date.now(); const s = await DirStorage.open(dir, 10_000, () => now); await s.put(name("a"), bytes(100)); const old = new Date(now - RETENTION_MS - 60_000); await utimes(join(dir, name("a")), old, old); await s.put(name("a"), bytes(100)); expect(await s.sweep()).toBe(0); }); });