/** * @file user-storage.test.js * @description The Users page's Storage column (server/lib/user-storage.js, * `GET /api/users/storage`): the walk counts allocated blocks, a hard link * once and never follows a symlink; a server-mode user's folder under * AGENTHUB_USERS_ROOT is measured in the background and reported to staff, * a member is refused, and a user with no folder of their own reads null. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const path = require("node:path"); const fs = require("node:fs"); const os = require("node:os"); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "user-storage-")); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.DASHBOARD_LIVENESS_PROBE = "0"; const USERS_ROOT = path.join(TMP, "users"); fs.mkdirSync(USERS_ROOT); const { createApp, startServer } = require("../index"); const auth = require("../lib/auth"); const { walk } = require("../lib/user-storage"); let server; let BASE; const ids = {}; async function login(username, password) { const res = await fetch(BASE + "/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); assert.equal(res.status, 200); return res.headers.get("set-cookie").split(";")[0]; } async function storage(cookie, q = "") { const res = await fetch(`${BASE}/api/users/storage${q}`, { headers: { cookie } }); return { status: res.status, body: await res.json().catch(() => null) }; } before(async () => { process.env.AGENTHUB_MODE = "server"; process.env.AGENTHUB_USERS_ROOT = USERS_ROOT; server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; ids.boss = (await auth.createUser("boss", "admin-pass-1", "admin")).id; ids.mia = (await auth.createUser("mia", "member-pass-1", "developer")).id; const home = path.join(USERS_ROOT, "mia"); fs.mkdirSync(path.join(home, "src"), { recursive: true }); fs.writeFileSync(path.join(home, "src", "a.bin"), Buffer.alloc(64 * 1024, 1)); }); after(() => { delete process.env.AGENTHUB_MODE; delete process.env.AGENTHUB_USERS_ROOT; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); describe("walk", () => { it("counts blocks, a hard link once, and never follows a symlink", () => { const dir = fs.mkdtempSync(path.join(TMP, "walk-")); fs.writeFileSync(path.join(dir, "big"), Buffer.alloc(128 * 1024, 1)); fs.linkSync(path.join(dir, "big"), path.join(dir, "big-link")); fs.symlinkSync("/usr", path.join(dir, "usr")); const r = walk(dir, Date.now() + 10_000); assert.equal(r.partial, false); assert.equal(r.files, 2); // big + the symlink itself; the hard link is skipped assert.ok(r.bytes >= 128 * 1024 && r.bytes < 1024 * 1024, `bytes ${r.bytes}`); }); it("stops at the deadline as a lower bound", () => { const r = walk(TMP, Date.now() - 1); assert.equal(r.partial, true); }); }); describe("GET /api/users/storage", () => { it("measures a user's folder in the background for staff", async () => { const admin = await login("boss", "admin-pass-1"); let r = await storage(admin); assert.equal(r.status, 200); for (let i = 0; i < 50 && (r.body.measuring || !r.body.storage[ids.mia]); i++) { await new Promise((ok) => setTimeout(ok, 100)); r = await storage(admin); } const mia = r.body.storage[ids.mia]; assert.ok(mia, "mia's folder was measured"); assert.ok(mia.bytes >= 64 * 1024); assert.equal(mia.files, 1); assert.equal(mia.partial, false); assert.ok(mia.measuredAt); // The admin has no folder under the users root: nothing to measure. assert.equal(r.body.storage[ids.boss], null); }); it("refuses a developer", async () => { const mia = await login("mia", "member-pass-1"); const r = await storage(mia); assert.equal(r.status, 403); }); });