146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { NextRequest } from "next/server";
|
|
|
|
let runtimeRoot = "";
|
|
let data: typeof import("../src/lib/boss-data");
|
|
let authCookie = "";
|
|
let postAdminAccess: (typeof import("../src/app/api/v1/admin/access/route"))["POST"];
|
|
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data")["readState"]>>;
|
|
|
|
async function setup() {
|
|
if (runtimeRoot) return;
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-admin-account-status-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
const [dataModule, authModule, routeModule] = await Promise.all([
|
|
import("../src/lib/boss-data.ts"),
|
|
import("../src/lib/boss-auth.ts"),
|
|
import("../src/app/api/v1/admin/access/route.ts"),
|
|
]);
|
|
data = dataModule;
|
|
authCookie = authModule.AUTH_SESSION_COOKIE;
|
|
postAdminAccess = routeModule.POST;
|
|
baseState = structuredClone(await data.readState());
|
|
}
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) await rm(runtimeRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
test.beforeEach(async () => {
|
|
await setup();
|
|
const state = structuredClone(baseState);
|
|
const now = "2026-04-27T12:00:00+08:00";
|
|
state.authAccounts = [
|
|
{
|
|
id: "account-owner",
|
|
account: "owner@acme.com",
|
|
passwordHash: "secret",
|
|
displayName: "Acme 老板",
|
|
role: "highest_admin",
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
{
|
|
id: "account-worker",
|
|
account: "worker@acme.com",
|
|
passwordHash: "secret",
|
|
displayName: "Worker",
|
|
role: "member",
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
];
|
|
state.authSessions = [];
|
|
state.permissionAuditLogs = [];
|
|
await data.writeState(state);
|
|
});
|
|
|
|
async function authedRequest(
|
|
account: string,
|
|
role: "member" | "admin" | "highest_admin",
|
|
body: Record<string, unknown>,
|
|
) {
|
|
const session = await data.createAuthSession({
|
|
account,
|
|
role,
|
|
displayName: account,
|
|
loginMethod: "password",
|
|
});
|
|
return new NextRequest("http://127.0.0.1:3000/api/v1/admin/access", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: `${authCookie}=${session.sessionToken}`,
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
async function adminPost(body: Record<string, unknown>) {
|
|
return postAdminAccess(await authedRequest("owner@acme.com", "highest_admin", body));
|
|
}
|
|
|
|
test("highest admin can disable a child account and revoke its active sessions", async () => {
|
|
const workerSession = await data.createAuthSession({
|
|
account: "worker@acme.com",
|
|
role: "member",
|
|
displayName: "Worker",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const response = await adminPost({
|
|
action: "set_account_status",
|
|
account: "worker@acme.com",
|
|
status: "disabled",
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.account.status, "disabled");
|
|
assert.equal(payload.account.passwordHash, undefined);
|
|
|
|
const state = await data.readState();
|
|
assert.equal(state.authAccounts.find((account) => account.account === "worker@acme.com")?.status, "disabled");
|
|
assert.equal(
|
|
state.authSessions.find((session) => session.sessionToken === workerSession.sessionToken)?.revokedAt !== undefined,
|
|
true,
|
|
);
|
|
assert.equal(await data.getAuthSession(workerSession.sessionToken), null);
|
|
assert.equal(state.permissionAuditLogs.at(0)?.action, "account.updated");
|
|
});
|
|
|
|
test("highest admin can re-enable a disabled child account", async () => {
|
|
await adminPost({
|
|
action: "set_account_status",
|
|
account: "worker@acme.com",
|
|
status: "disabled",
|
|
});
|
|
|
|
const response = await adminPost({
|
|
action: "set_account_status",
|
|
account: "worker@acme.com",
|
|
status: "active",
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.account.status, "active");
|
|
|
|
const state = await data.readState();
|
|
assert.equal(state.authAccounts.find((account) => account.account === "worker@acme.com")?.status, "active");
|
|
});
|
|
|
|
test("highest admin cannot disable a highest admin account", async () => {
|
|
const response = await adminPost({
|
|
action: "set_account_status",
|
|
account: "owner@acme.com",
|
|
status: "disabled",
|
|
});
|
|
assert.equal(response.status, 400);
|
|
const payload = await response.json();
|
|
assert.equal(payload.message, "CANNOT_DISABLE_HIGHEST_ADMIN");
|
|
});
|