diff --git a/src/cli/devices-cli.test.ts b/src/cli/devices-cli.test.ts index 7f89bb100d..334c50a906 100644 --- a/src/cli/devices-cli.test.ts +++ b/src/cli/devices-cli.test.ts @@ -192,3 +192,64 @@ describe("devices cli clear", () => { ); }); }); + +describe("devices cli tokens", () => { + afterEach(() => { + callGateway.mockReset(); + withProgress.mockClear(); + runtime.log.mockReset(); + runtime.error.mockReset(); + runtime.exit.mockReset(); + }); + + it("rotates a token for a device role", async () => { + callGateway.mockResolvedValueOnce({ ok: true }); + + await runDevicesCommand([ + "rotate", + "--device", + "device-1", + "--role", + "main", + "--scope", + "messages:send", + "--scope", + "messages:read", + ]); + + expect(callGateway).toHaveBeenCalledWith( + expect.objectContaining({ + method: "device.token.rotate", + params: { + deviceId: "device-1", + role: "main", + scopes: ["messages:send", "messages:read"], + }, + }), + ); + }); + + it("revokes a token for a device role", async () => { + callGateway.mockResolvedValueOnce({ ok: true }); + + await runDevicesCommand(["revoke", "--device", "device-1", "--role", "main"]); + + expect(callGateway).toHaveBeenCalledWith( + expect.objectContaining({ + method: "device.token.revoke", + params: { + deviceId: "device-1", + role: "main", + }, + }), + ); + }); + + it("rejects blank device or role values", async () => { + await runDevicesCommand(["rotate", "--device", " ", "--role", "main"]); + + expect(callGateway).not.toHaveBeenCalled(); + expect(runtime.error).toHaveBeenCalledWith("--device and --role required"); + expect(runtime.exit).toHaveBeenCalledWith(1); + }); +}); diff --git a/src/cli/devices-cli.ts b/src/cli/devices-cli.ts index 6539e8ff4e..b702c51824 100644 --- a/src/cli/devices-cli.ts +++ b/src/cli/devices-cli.ts @@ -110,6 +110,19 @@ function formatTokenSummary(tokens: DeviceTokenSummary[] | undefined) { return parts.join(", "); } +function resolveRequiredDeviceRole( + opts: DevicesRpcOpts, +): { deviceId: string; role: string } | null { + const deviceId = String(opts.device ?? "").trim(); + const role = String(opts.role ?? "").trim(); + if (deviceId && role) { + return { deviceId, role }; + } + defaultRuntime.error("--device and --role required"); + defaultRuntime.exit(1); + return null; +} + export function registerDevicesCli(program: Command) { const devices = program.command("devices").description("Device pairing and auth tokens"); @@ -318,16 +331,13 @@ export function registerDevicesCli(program: Command) { .requiredOption("--role ", "Role name") .option("--scope ", "Scopes to attach to the token (repeatable)") .action(async (opts: DevicesRpcOpts) => { - const deviceId = String(opts.device ?? "").trim(); - const role = String(opts.role ?? "").trim(); - if (!deviceId || !role) { - defaultRuntime.error("--device and --role required"); - defaultRuntime.exit(1); + const required = resolveRequiredDeviceRole(opts); + if (!required) { return; } const result = await callGatewayCli("device.token.rotate", opts, { - deviceId, - role, + deviceId: required.deviceId, + role: required.role, scopes: Array.isArray(opts.scope) ? opts.scope : undefined, }); defaultRuntime.log(JSON.stringify(result, null, 2)); @@ -341,16 +351,13 @@ export function registerDevicesCli(program: Command) { .requiredOption("--device ", "Device id") .requiredOption("--role ", "Role name") .action(async (opts: DevicesRpcOpts) => { - const deviceId = String(opts.device ?? "").trim(); - const role = String(opts.role ?? "").trim(); - if (!deviceId || !role) { - defaultRuntime.error("--device and --role required"); - defaultRuntime.exit(1); + const required = resolveRequiredDeviceRole(opts); + if (!required) { return; } const result = await callGatewayCli("device.token.revoke", opts, { - deviceId, - role, + deviceId: required.deviceId, + role: required.role, }); defaultRuntime.log(JSON.stringify(result, null, 2)); }),