


























11// Device method tests cover pairing approval/rejection, paired-device lookup,
22// token rotation/revocation, and operator scope enforcement.
33import { beforeEach, describe, expect, it, vi } from "vitest";
4+import {
5+onInternalDiagnosticEvent,
6+resetDiagnosticEventsForTest,
7+type DiagnosticSecurityEvent,
8+} from "../../infra/diagnostic-events.js";
49import { deviceHandlers } from "./devices.js";
510import type { GatewayRequestHandlerOptions } from "./types.js";
611@@ -122,8 +127,22 @@ function expectRespondedErrorMessage(opts: GatewayRequestHandlerOptions, message
122127expect(call[2]?.message).toBe(message);
123128}
124129130+function captureSecurityEvents(): {
131+events: DiagnosticSecurityEvent[];
132+stop: () => void;
133+} {
134+const events: DiagnosticSecurityEvent[] = [];
135+const stop = onInternalDiagnosticEvent((event, metadata) => {
136+if (metadata.trusted && event.type === "security.event") {
137+events.push(event);
138+}
139+});
140+return { events, stop };
141+}
142+125143describe("deviceHandlers", () => {
126144beforeEach(() => {
145+resetDiagnosticEventsForTest();
127146vi.clearAllMocks();
128147});
129148@@ -243,15 +262,20 @@ describe("deviceHandlers", () => {
243262it("disconnects active clients after revoking a device token", async () => {
244263revokeDeviceTokenMock.mockResolvedValue({
245264ok: true,
246-entry: { role: "operator", revokedAtMs: 456 },
265+entry: { token: "raw-revoked-token", role: "operator", scopes: [], revokedAtMs: 456 },
247266});
248267const opts = createOptions("device.token.revoke", {
249268deviceId: " device-1 ",
250269role: " operator ",
251270});
271+const captured = captureSecurityEvents();
252272253-await deviceHandlers["device.token.revoke"](opts);
254-await Promise.resolve();
273+try {
274+await deviceHandlers["device.token.revoke"](opts);
275+await Promise.resolve();
276+} finally {
277+captured.stop();
278+}
255279256280expect(revokeDeviceTokenMock).toHaveBeenCalledWith({
257281deviceId: " device-1 ",
@@ -266,6 +290,21 @@ describe("deviceHandlers", () => {
266290{ deviceId: "device-1", role: "operator", revokedAtMs: 456 },
267291undefined,
268292);
293+expect(captured.events).toHaveLength(1);
294+expect(captured.events[0]).toMatchObject({
295+type: "security.event",
296+category: "auth",
297+action: "device.token.revoked",
298+outcome: "success",
299+severity: "high",
300+target: { kind: "device", idHash: expect.stringMatching(/^sha256:[a-f0-9]{12}$/u) },
301+policy: { id: "gateway.device-token", decision: "allow" },
302+control: { id: "device.token.revoke", family: "auth" },
303+attributes: { role: "operator" },
304+});
305+const serialized = JSON.stringify(captured.events);
306+expect(serialized).not.toContain("device-1");
307+expect(serialized).not.toContain("raw-revoked-token");
269308});
270309271310it("allows admin-scoped callers to revoke another device's token", async () => {
@@ -299,12 +338,37 @@ describe("deviceHandlers", () => {
299338{ deviceId: "device-1", role: "node" },
300339{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
301340);
341+const captured = captureSecurityEvents();
302342303-await deviceHandlers["device.token.revoke"](opts);
343+try {
344+await deviceHandlers["device.token.revoke"](opts);
345+} finally {
346+captured.stop();
347+}
304348305349expect(revokeDeviceTokenMock).not.toHaveBeenCalled();
306350expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();
307351expectRespondedErrorMessage(opts, "device token revocation denied");
352+expect(captured.events).toHaveLength(1);
353+expect(captured.events[0]).toMatchObject({
354+action: "device.token.revocation_denied",
355+outcome: "denied",
356+reason: "role-management-requires-admin",
357+actor: {
358+kind: "operator",
359+deviceIdHash: expect.stringMatching(/^sha256:[a-f0-9]{12}$/u),
360+role: "operator",
361+},
362+target: { kind: "device", idHash: expect.stringMatching(/^sha256:[a-f0-9]{12}$/u) },
363+policy: {
364+id: "gateway.device-token",
365+decision: "deny",
366+reason: "role-management-requires-admin",
367+},
368+control: { id: "device.token.revoke", family: "auth" },
369+attributes: { role: "node" },
370+});
371+expect(JSON.stringify(captured.events)).not.toContain("device-1");
308372});
309373310374it("treats normalized device ids as self-owned for token revocation", async () => {
@@ -823,8 +887,13 @@ describe("deviceHandlers", () => {
823887}),
824888},
825889);
890+const captured = captureSecurityEvents();
826891827-await deviceHandlers["device.pair.approve"](opts);
892+try {
893+await deviceHandlers["device.pair.approve"](opts);
894+} finally {
895+captured.stop();
896+}
828897829898expect(getPendingDevicePairingMock).not.toHaveBeenCalled();
830899expect(approveDevicePairingMock).toHaveBeenCalledWith("req-2", {
@@ -844,6 +913,25 @@ describe("deviceHandlers", () => {
844913},
845914undefined,
846915);
916+expect(captured.events).toHaveLength(1);
917+expect(captured.events[0]).toMatchObject({
918+action: "device.pairing.approved",
919+outcome: "success",
920+severity: "low",
921+actor: {
922+kind: "operator",
923+deviceIdHash: expect.stringMatching(/^sha256:[a-f0-9]{12}$/u),
924+role: "admin",
925+},
926+target: { kind: "device", idHash: expect.stringMatching(/^sha256:[a-f0-9]{12}$/u) },
927+policy: { id: "gateway.device-pairing", decision: "allow" },
928+control: { id: "device.pair.approve", family: "auth" },
929+attributes: { role_count: 0, scope_count: 0 },
930+});
931+const serialized = JSON.stringify(captured.events);
932+expect(serialized).not.toContain("device-1");
933+expect(serialized).not.toContain("device-2");
934+expect(serialized).not.toContain("pk-2");
847935});
848936849937it("allows approving the caller device from a non-admin device session", async () => {
@@ -957,11 +1045,29 @@ describe("deviceHandlers", () => {
9571045{ requestId: "req-1" },
9581046{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
9591047);
1048+const captured = captureSecurityEvents();
9601049961-await deviceHandlers["device.pair.approve"](opts);
1050+try {
1051+await deviceHandlers["device.pair.approve"](opts);
1052+} finally {
1053+captured.stop();
1054+}
96210559631056expect(approveDevicePairingMock).not.toHaveBeenCalled();
9641057expectRespondedErrorMessage(opts, "device pairing approval denied");
1058+expect(captured.events).toHaveLength(1);
1059+expect(captured.events[0]).toMatchObject({
1060+action: "device.pairing.denied",
1061+outcome: "denied",
1062+reason: "role-management-requires-admin",
1063+policy: {
1064+id: "gateway.device-pairing",
1065+decision: "deny",
1066+reason: "role-management-requires-admin",
1067+},
1068+control: { id: "device.pair.approve", family: "auth" },
1069+});
1070+expect(JSON.stringify(captured.events)).not.toContain("device-1");
9651071});
96610729671073it("rejects approving node roles from non-admin shared-auth sessions", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。