Files
boss/tests/device-revocation-auth.test.ts
2026-05-17 02:20:08 +08:00

194 lines
6.9 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 deviceHeartbeatRoute: (typeof import("../src/app/api/device-heartbeat/route"))["POST"];
let claimMasterTaskRoute: (typeof import("../src/app/api/v1/master-agent/tasks/claim/route"))["POST"];
let getBossAgentOtaRoute: (typeof import("../src/app/api/v1/boss-agent/ota/route"))["GET"];
let postAdminAccessRoute: (typeof import("../src/app/api/v1/admin/access/route"))["POST"];
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-revocation-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [dataModule, authModule, heartbeatModule, claimModule, otaModule, adminAccessModule] =
await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
import("../src/app/api/device-heartbeat/route.ts"),
import("../src/app/api/v1/master-agent/tasks/claim/route.ts"),
import("../src/app/api/v1/boss-agent/ota/route.ts"),
import("../src/app/api/v1/admin/access/route.ts"),
]);
data = dataModule;
authCookie = authModule.AUTH_SESSION_COOKIE;
deviceHeartbeatRoute = heartbeatModule.POST;
claimMasterTaskRoute = claimModule.POST;
getBossAgentOtaRoute = otaModule.GET;
postAdminAccessRoute = adminAccessModule.POST;
}
test.after(async () => {
if (runtimeRoot) await rm(runtimeRoot, { recursive: true, force: true });
});
test.beforeEach(async () => {
await setup();
await rm(runtimeRoot, { recursive: true, force: true });
});
function heartbeatRequest(body: Record<string, unknown>) {
return new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: "mac-studio",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
status: "online",
quota5h: 80,
quota7d: 90,
projects: ["Boss"],
...body,
}),
});
}
async function highestAdminRequest(body: Record<string, unknown>) {
const session = await data.createAuthSession({
account: "krisolo",
role: "highest_admin",
displayName: "Boss 超级管理员",
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),
});
}
test("existing device heartbeat requires its device token and cannot be kept alive by an empty token request", async () => {
const state = await data.readState();
const device = state.devices.find((item) => item.id === "mac-studio");
assert.ok(device);
device.status = "offline";
device.lastSeenAt = "2026-05-17T01:00:00.000Z";
device.projects = ["原项目"];
await data.writeState(state);
const response = await deviceHeartbeatRoute(heartbeatRequest({ token: undefined }));
assert.equal(response.status, 401);
const payload = await response.json();
assert.equal(payload.message, "DEVICE_TOKEN_REQUIRED");
const nextState = await data.readState();
const nextDevice = nextState.devices.find((item) => item.id === "mac-studio");
assert.equal(nextDevice?.status, "offline");
assert.equal(nextDevice?.lastSeenAt, "2026-05-17T01:00:00.000Z");
assert.deepEqual(nextDevice?.projects, ["原项目"]);
});
test("revoked device token is rejected by heartbeat, master task claim, and boss-agent ota", async () => {
const initial = await data.readState();
const device = initial.devices.find((item) => item.id === "mac-studio");
assert.ok(device?.token);
const oldToken = device.token;
const revokeResponse = await postAdminAccessRoute(
await highestAdminRequest({
action: "revoke_device",
deviceId: "mac-studio",
reason: "客户解绑设备",
}),
);
assert.equal(revokeResponse.status, 200);
const revokePayload = await revokeResponse.json();
assert.equal(revokePayload.device.id, "mac-studio");
assert.ok(revokePayload.device.revokedAt);
assert.equal(revokePayload.device.status, "offline");
assert.equal(await data.verifyDeviceToken("mac-studio", oldToken), false);
const queued = await data.queueMasterAgentTask({
taskId: "revoked-device-task",
requestMessageId: "message-revoked",
requestText: "打开浏览器",
executionPrompt: "打开浏览器",
requestedBy: "krisolo",
requestedByAccount: "krisolo",
deviceId: "mac-studio",
});
assert.equal(queued.status, "queued");
const claimResponse = await claimMasterTaskRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/master-agent/tasks/claim", {
method: "POST",
headers: {
"content-type": "application/json",
"x-boss-device-token": oldToken,
},
body: JSON.stringify({ deviceId: "mac-studio" }),
}),
);
assert.equal(claimResponse.status, 401);
const heartbeatResponse = await deviceHeartbeatRoute(heartbeatRequest({ token: oldToken }));
assert.equal(heartbeatResponse.status, 403);
assert.equal((await heartbeatResponse.json()).message, "DEVICE_REVOKED");
const otaResponse = await getBossAgentOtaRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/boss-agent/ota?deviceId=mac-studio", {
method: "GET",
headers: { "x-boss-device-token": oldToken },
}),
);
assert.equal(otaResponse.status, 401);
const nextState = await data.readState();
const nextTask = nextState.masterAgentTasks.find((item) => item.taskId === "revoked-device-task");
const audit = nextState.permissionAuditLogs.find((item) => item.action === "device.revoked");
assert.equal(nextTask?.status, "queued");
assert.equal(nextState.devices.find((item) => item.id === "mac-studio")?.status, "offline");
assert.equal(audit?.deviceId, "mac-studio");
assert.equal(audit?.actorAccount, "krisolo");
});
test("a new device cannot self-register through heartbeat without a prepared enrollment", async () => {
const response = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: "rogue-mac",
token: "rogue-token",
name: "Rogue Mac",
avatar: "R",
account: "krisolo",
status: "online",
quota5h: 1,
quota7d: 1,
projects: ["Boss"],
}),
}),
);
assert.equal(response.status, 401);
assert.equal((await response.json()).message, "DEVICE_ENROLLMENT_REQUIRED");
assert.equal((await data.readState()).devices.some((item) => item.id === "rogue-mac"), false);
});