/** * @file server-mode-account-pool.test.js * @description The Claude/Codex account pool from the Users page: only an * admin may add or remove an account, each change needs the admin's OWN * password again (EREAUTH), the token only goes in — no answer (the add, the * list) ever carries it — the audit log names the account, not its secret, and * in local mode the routes refuse. * @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(), "server-mode-account-pool-")); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.CLAUDE_HOME = path.join(TMP, ".claude"); process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp, startServer } = require("../index"); const { db } = require("../db"); const auth = require("../lib/auth"); const claudePool = require("../lib/claude-pool"); const codexPool = require("../lib/codex-pool"); // The quota read would go to the network; the pool's list must not. require("../lib/cc-usage").fetchUsage = async () => ({}); let server; let BASE; const BOSS_PASS = "admin-pass-1"; const TOKEN = `sk-ant-oat01-${"a".repeat(40)}`; const KEY = `sk-proj-${"b".repeat(40)}`; async function call(method, p, { cookie, body } = {}) { const res = await fetch(BASE + p, { method, headers: { "Content-Type": "application/json", ...(cookie ? { cookie } : {}) }, body: body ? JSON.stringify(body) : undefined, }); return { status: res.status, body: await res.json().catch(() => null) }; } 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 }), }); return res.status === 200 ? res.headers.get("set-cookie").split(";")[0] : null; } before(async () => { process.env.AGENTHUB_MODE = "server"; server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; await auth.createUser("boss", BOSS_PASS, "admin"); await auth.createUser("ana", "analyst-pass-1", "analyst"); await auth.createUser("mia", "member-pass-1", "developer"); }); after(() => { delete process.env.AGENTHUB_MODE; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); describe("the account pool from the Users page", () => { it("lets an admin add a Claude account, never answering with its token", async () => { const boss = await login("boss", BOSS_PASS); const r = await call("POST", "/api/accounts", { cookie: boss, body: { tool: "claude", name: "team-1", token: TOKEN, currentPassword: BOSS_PASS }, }); assert.equal(r.status, 201); assert.equal(r.body.account.name, "team-1"); assert.ok(!JSON.stringify(r.body).includes(TOKEN)); assert.equal(claudePool.pickAccount().token, TOKEN, "new panes run on it"); const list = await call("GET", "/api/accounts", { cookie: boss }); assert.deepEqual( list.body.accounts.map((a) => a.name), ["team-1"] ); assert.ok(!JSON.stringify(list.body).includes(TOKEN)); const audit = db.prepare("SELECT path FROM audit_log ORDER BY id DESC LIMIT 1").get(); assert.match(audit.path, /add claude account team-1/); assert.ok(!audit.path.includes(TOKEN)); }); it("adds a Codex API key, and removes accounts again", async () => { const boss = await login("boss", BOSS_PASS); const add = await call("POST", "/api/accounts", { cookie: boss, body: { tool: "codex", name: "cx-1", token: KEY, currentPassword: BOSS_PASS }, }); assert.equal(add.status, 201); assert.equal(add.body.account.kind, "apikey"); assert.ok(!JSON.stringify(add.body).includes(KEY)); assert.equal(codexPool.__read("cx-1").key, KEY); const del = await call("DELETE", "/api/accounts/cx-1", { cookie: boss, body: { tool: "codex", currentPassword: BOSS_PASS }, }); assert.equal(del.status, 200); assert.equal(codexPool.__read("cx-1"), null); const gone = await call("DELETE", "/api/accounts/cx-1", { cookie: boss, body: { tool: "codex", currentPassword: BOSS_PASS }, }); assert.equal(gone.status, 404); }); it("refuses a wrong password, a bad token, a taken name and an unknown tool", async () => { const boss = await login("boss", BOSS_PASS); const add = (body) => call("POST", "/api/accounts", { cookie: boss, body: { tool: "claude", name: "team-2", token: TOKEN, currentPassword: BOSS_PASS, ...body }, }); assert.equal((await add({ currentPassword: "nope" })).body.error.code, "EREAUTH"); assert.equal((await add({ token: "not-a-token" })).body.error.code, "EBADTOKEN"); assert.equal((await add({ name: "team-1" })).body.error.code, "EEXISTS"); assert.equal((await add({ tool: "cursor" })).body.error.code, "EBADTOOL"); assert.equal((await add({ name: "../x" })).body.error.code, "EBADNAME"); }); it("is an admin's only: an analyst reads it, a developer neither reads nor writes", async () => { const ana = await login("ana", "analyst-pass-1"); assert.equal((await call("GET", "/api/accounts", { cookie: ana })).status, 200); const w = await call("POST", "/api/accounts", { cookie: ana, body: { tool: "claude", name: "x", token: TOKEN, currentPassword: "analyst-pass-1" }, }); assert.equal(w.status, 403); const mia = await login("mia", "member-pass-1"); assert.equal((await call("GET", "/api/accounts", { cookie: mia })).status, 403); const d = await call("DELETE", "/api/accounts/team-1", { cookie: mia, body: { tool: "claude", currentPassword: "member-pass-1" }, }); assert.equal(d.status, 403); assert.ok(claudePool.pickAccount(), "team-1 is still there"); }); });