Public: anyone can read this; the crew can change it.
A test pins the storage cap's words and figures: no limit asks nothing, a stale figure is re-measured, and the refusal names the figure #6928
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6911-a-test-pins-the-storage-cap-s-words-and → main 1 commit fork 1 thread · 1 unresolved
1 file
changed +131
−0
test/storage-cap.test.ts added +131
| @@ -0,0 +1,131 @@ | ||
| 1 | /** | |
| 2 | * The storage cap's words and figures (`core/storage.ts`): no limit asks | |
| 3 | * nothing, a stale figure is measured again, and a refusal names the figure | |
| 4 | * in human sizes and only the account's own. | |
| 5 | */ | |
| 6 | ||
| 7 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 8 | import { mkdtemp, rm, writeFile } from "node:fs/promises"; | |
| 9 | import { tmpdir } from "node:os"; | |
| 10 | import { join } from "node:path"; | |
| 11 | import { createApp, type App } from "../src/app.ts"; | |
| 12 | import { createRepo } from "../src/actions/repos.ts"; | |
| 13 | import { signUp } from "../src/actions/users.ts"; | |
| 14 | import { getUserById } from "../src/core/users.ts"; | |
| 15 | import { | |
| 16 | BYTES_PER_MB, | |
| 17 | accountBytes, | |
| 18 | formatBytes, | |
| 19 | storageLimitBytes, | |
| 20 | storageLimitMessage, | |
| 21 | storageSentence, | |
| 22 | storageStale, | |
| 23 | } from "../src/core/storage.ts"; | |
| 24 | import { repoDir } from "../src/git/repos.ts"; | |
| 25 | import { setQuota } from "../src/settings/quotas.ts"; | |
| 26 | import { fixtureSlot } from "./helpers/slot.ts"; | |
| 27 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 28 | import { proveAll } from "./helpers/verified.ts"; | |
| 29 | import { promote } from "./helpers/members.ts"; | |
| 30 | ||
| 31 | describe("storage cap", () => { | |
| 32 | let dataDir: string; | |
| 33 | let app: App; | |
| 34 | let adaId: string; | |
| 35 | let bobId: string; | |
| 36 | let carolId: string; | |
| 37 | ||
| 38 | /** Add `bytes` of loose data to the repository `owner/name`, as a push would. */ | |
| 39 | const grow = (owner: string, name: string, file: string, bytes: number) => | |
| 40 | writeFile(join(repoDir(app.reposRoot, owner, name), file), new Uint8Array(bytes)); | |
| 41 | ||
| 42 | beforeAll(async () => { | |
| 43 | dataDir = await mkdtemp(join(tmpdir(), "forge-storage-cap-")); | |
| 44 | app = createApp({ dataDir, port: 0 }); | |
| 45 | const ada = await seedAdmin(app, { username: "ada", email: "[email protected]", password: "owner-passphrase" }); | |
| 46 | const bob = await signUp(app, { username: "bob", email: "[email protected]", password: "bob-passphrase" }, { slot: fixtureSlot(app) }); | |
| 47 | const carol = await signUp(app, { username: "carol", email: "[email protected]", password: "carol-passphrase" }, { slot: fixtureSlot(app) }); | |
| 48 | if (!ada.ok || !bob.ok || !carol.ok) throw new Error("signup"); | |
| 49 | proveAll(app); | |
| 50 | promote(app.db); | |
| 51 | adaId = ada.value.id; | |
| 52 | bobId = bob.value.id; | |
| 53 | carolId = carol.value.id; | |
| 54 | for (const [id, name] of [[bobId, "bobs"], [carolId, "carols"]] as const) { | |
| 55 | const made = await createRepo(app, getUserById(app.db, id)!, { name, visibility: "private" }); | |
| 56 | if (!made.ok) throw new Error(`repo ${made.error}`); | |
| 57 | } | |
| 58 | }); | |
| 59 | ||
| 60 | afterAll(async () => { | |
| 61 | app.db.close(); | |
| 62 | await rm(dataDir, { recursive: true, force: true }); | |
| 63 | }); | |
| 64 | ||
| 65 | test("no limit is undefined, a limit is its bytes, and a change is read on the next call", () => { | |
| 66 | expect(storageLimitBytes(app)).toBeUndefined(); | |
| 67 | setQuota(app.db, "storage", 2, adaId); | |
| 68 | expect(storageLimitBytes(app)).toBe(2 * BYTES_PER_MB); | |
| 69 | setQuota(app.db, "storage", 5, adaId); | |
| 70 | expect(storageLimitBytes(app)).toBe(5 * BYTES_PER_MB); | |
| 71 | setQuota(app.db, "storage", null, adaId); | |
| 72 | expect(storageLimitBytes(app)).toBeUndefined(); | |
| 73 | }); | |
| 74 | ||
| 75 | test("a figure marked stale is measured again; until then the cached one stands", async () => { | |
| 76 | const before = await accountBytes(app, bobId); | |
| 77 | await grow("bob", "bobs", "extra.bin", 40_000); | |
| 78 | expect(await accountBytes(app, bobId)).toBe(before); | |
| 79 | storageStale(app, bobId); | |
| 80 | expect(await accountBytes(app, bobId)).toBe(before + 40_000); | |
| 81 | expect(await accountBytes(app, bobId)).toBe(before + 40_000); | |
| 82 | }); | |
| 83 | ||
| 84 | test("the message names the account's figure and the limit in human sizes, never another's", async () => { | |
| 85 | await grow("carol", "carols", "big.bin", 3_000_000); | |
| 86 | storageStale(app, carolId); | |
| 87 | const carol = await accountBytes(app, carolId); | |
| 88 | const bob = await accountBytes(app, bobId); | |
| 89 | setQuota(app.db, "storage", 2, adaId); | |
| 90 | ||
| 91 | const msg = storageLimitMessage(app, carolId); | |
| 92 | expect(msg).toBe(`this account is at the forge's storage limit: ${formatBytes(carol)} of 2 MB`); | |
| 93 | expect(msg).toContain(" MB of 2 MB"); | |
| 94 | expect(msg).not.toContain(String(carol)); | |
| 95 | expect(msg).not.toContain(String(2 * BYTES_PER_MB)); | |
| 96 | expect(msg).not.toContain(formatBytes(bob)); | |
| 97 | ||
| 98 | // Bob is under the limit: his message carries no figure, and not carol's. | |
| 99 | const bobs = storageLimitMessage(app, bobId); | |
| 100 | expect(bobs).toBe("this account is at the forge's storage limit"); | |
| 101 | expect(bobs).not.toContain(formatBytes(carol)); | |
| 102 | setQuota(app.db, "storage", null, adaId); | |
| 103 | expect(storageLimitMessage(app, carolId)).toBe("this account is at the forge's storage limit"); | |
| 104 | }); | |
| 105 | ||
| 106 | test("the sentence reads right at nothing, just under and just over the limit", async () => { | |
| 107 | setQuota(app.db, "storage", 1, adaId); | |
| 108 | // Never measured: nothing to say. | |
| 109 | expect(storageSentence(app, adaId)).toBeUndefined(); | |
| 110 | expect(await accountBytes(app, adaId)).toBe(0); | |
| 111 | expect(storageSentence(app, adaId)).toBeUndefined(); | |
| 112 | ||
| 113 | // Just under: 900 kB of 1 MB is no refusal. | |
| 114 | await grow("bob", "bobs", "extra.bin", 900_000); | |
| 115 | storageStale(app, bobId); | |
| 116 | const under = await accountBytes(app, bobId); | |
| 117 | expect(under).toBeGreaterThan(900_000); | |
| 118 | expect(under).toBeLessThan(BYTES_PER_MB); | |
| 119 | expect(storageSentence(app, bobId)).toBeUndefined(); | |
| 120 | ||
| 121 | // Just over. | |
| 122 | await grow("bob", "bobs", "extra.bin", 1_100_000); | |
| 123 | storageStale(app, bobId); | |
| 124 | const over = await accountBytes(app, bobId); | |
| 125 | expect(storageSentence(app, bobId)).toBe(`${formatBytes(over)} of 1 MB`); | |
| 126 | ||
| 127 | // No limit: no sentence, whatever is stored. | |
| 128 | setQuota(app.db, "storage", null, adaId); | |
| 129 | expect(storageSentence(app, bobId)).toBeUndefined(); | |
| 130 | }); | |
| 131 | }); |
new above
offline
The 900_000-byte file plus repo overhead must stay under 1_000_000, so this passes only while overhead is under 100 kB. Size the file from the measured baseline (e.g. BYTES_PER_MB - baseline - 50_000) so the test does not depend on git template size.