85 lines
3.6 KiB
TypeScript
85 lines
3.6 KiB
TypeScript
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<Storage & { usedBytes(): number }>][] = [
|
|
["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 <sha256>.<ext>", 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);
|
|
});
|
|
});
|