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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6911-a-test-pins-the-storage-cap-s-words-and → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6911-a-test-pins-the-storage-cap-s-words-and was merged into main as df476e4.

Review

Sign in to review this change.
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);

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.

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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs