import { describe, expect, it, vi } from "vitest"; import { clearDeviceAuthTokenFromStore, coerceDeviceAuthStore, loadDeviceAuthTokenFromStore, storeDeviceAuthTokenInStore, type DeviceAuthStoreAdapter, } from "./device-auth-store.js"; function createAdapter(initialStore: ReturnType = null) { let store = initialStore; const writes: unknown[] = []; const adapter: DeviceAuthStoreAdapter = { readStore: () => store, writeStore: (next) => { store = next; writes.push(next); }, }; return { adapter, writes, readStore: () => store }; } describe("device-auth-store", () => { it("loads only matching device ids and normalized roles", () => { const { adapter } = createAdapter({ version: 1, deviceId: "device-1", tokens: { operator: { token: "secret", role: "operator", scopes: ["operator.read"], updatedAtMs: 1, }, }, }); expect( loadDeviceAuthTokenFromStore({ adapter, deviceId: "device-1", role: " operator ", }), ).toEqual({ token: "secret", role: "operator", scopes: ["operator.read"], updatedAtMs: 1, }); expect( loadDeviceAuthTokenFromStore({ adapter, deviceId: "device-2", role: "operator", }), ).toBeNull(); }); it("returns null for missing stores and malformed token entries", () => { expect( loadDeviceAuthTokenFromStore({ adapter: createAdapter().adapter, deviceId: "device-1", role: "operator", }), ).toBeNull(); const { adapter } = createAdapter({ version: 1, deviceId: "device-1", tokens: { operator: { token: 123 as unknown as string, role: "operator", scopes: [], updatedAtMs: 1, }, }, }); expect( loadDeviceAuthTokenFromStore({ adapter, deviceId: "device-1", role: "operator", }), ).toBeNull(); }); it("normalizes malformed persisted token metadata before returning entries", () => { const { adapter } = createAdapter({ version: 1, deviceId: "device-1", tokens: { operator: { token: "secret", role: { nested: "bad" }, scopes: ["operator.write", 42, "", "operator.read"], updatedAtMs: "bad-time", }, }, } as never); expect( loadDeviceAuthTokenFromStore({ adapter, deviceId: "device-1", role: "operator", }), ).toEqual({ token: "secret", role: "operator", scopes: ["operator.read", "operator.write"], updatedAtMs: 0, }); }); it("coerces raw persisted stores into canonical token maps", () => { expect( coerceDeviceAuthStore({ version: 1, deviceId: "device-1", tokens: { " operator ": { token: "operator-token", role: { nested: "bad" }, scopes: ["operator.write", "operator.read", 42], updatedAtMs: "bad-time", }, broken: { token: 123, role: "broken", scopes: [], updatedAtMs: 1, }, }, }), ).toEqual({ version: 1, deviceId: "device-1", tokens: { operator: { token: "operator-token", role: "operator", scopes: ["operator.read", "operator.write"], updatedAtMs: 0, }, }, }); expect(coerceDeviceAuthStore({ version: 2, deviceId: "device-1", tokens: {} })).toBeNull(); expect(coerceDeviceAuthStore({ version: 1, deviceId: "device-1", tokens: [] })).toBeNull(); }); it("stores normalized roles and deduped sorted scopes while preserving same-device tokens", () => { vi.spyOn(Date, "now").mockReturnValue(1234); const { adapter, writes, readStore } = createAdapter({ version: 1, deviceId: "device-1", tokens: { node: { token: "node-token", role: "node", scopes: ["node.invoke"], updatedAtMs: 10, }, }, }); const entry = storeDeviceAuthTokenInStore({ adapter, deviceId: "device-1", role: " operator ", token: "operator-token", scopes: [" operator.write ", "operator.read", "operator.read", ""], }); expect(entry).toEqual({ token: "operator-token", role: "operator", scopes: ["operator.read", "operator.write"], updatedAtMs: 1234, }); expect(writes).toHaveLength(1); expect(readStore()).toEqual({ version: 1, deviceId: "device-1", tokens: { node: { token: "node-token", role: "node", scopes: ["node.invoke"], updatedAtMs: 10, }, operator: entry, }, }); }); it("canonicalizes same-device persisted tokens while storing new entries", () => { vi.spyOn(Date, "now").mockReturnValue(5678); const { adapter, readStore } = createAdapter({ version: 1, deviceId: "device-1", tokens: { node: { token: "node-token", role: { nested: "bad" }, scopes: ["node.invoke", 123], updatedAtMs: "bad-time", }, broken: { token: 123, role: "broken", scopes: [], updatedAtMs: 1, }, }, } as never); const entry = storeDeviceAuthTokenInStore({ adapter, deviceId: "device-1", role: "operator", token: "operator-token", }); expect(readStore()).toEqual({ version: 1, deviceId: "device-1", tokens: { node: { token: "node-token", role: "node", scopes: ["node.invoke"], updatedAtMs: 0, }, operator: entry, }, }); }); it("replaces stale stores from other devices instead of merging them", () => { vi.spyOn(Date, "now").mockReturnValue(3456); const { adapter, readStore } = createAdapter({ version: 1, deviceId: "device-2", tokens: { operator: { token: "old-token", role: "operator", scopes: [], updatedAtMs: 1, }, }, }); storeDeviceAuthTokenInStore({ adapter, deviceId: "device-1", role: "node", token: "node-token", }); expect(readStore()).toEqual({ version: 1, deviceId: "device-1", tokens: { node: { token: "node-token", role: "node", scopes: [], updatedAtMs: 3456, }, }, }); }); it("overwrites existing entries for the same normalized role", () => { vi.spyOn(Date, "now").mockReturnValue(2222); const { adapter, readStore } = createAdapter({ version: 1, deviceId: "device-1", tokens: { operator: { token: "old-token", role: "operator", scopes: ["operator.read"], updatedAtMs: 10, }, }, }); const entry = storeDeviceAuthTokenInStore({ adapter, deviceId: "device-1", role: " operator ", token: "new-token", scopes: ["operator.write"], }); expect(entry).toEqual({ token: "new-token", role: "operator", scopes: ["operator.read", "operator.write"], updatedAtMs: 2222, }); expect(readStore()).toEqual({ version: 1, deviceId: "device-1", tokens: { operator: entry, }, }); }); it("avoids writes when clearing missing roles or mismatched devices", () => { const missingRole = createAdapter({ version: 1, deviceId: "device-1", tokens: {}, }); clearDeviceAuthTokenFromStore({ adapter: missingRole.adapter, deviceId: "device-1", role: "operator", }); expect(missingRole.writes).toHaveLength(0); const otherDevice = createAdapter({ version: 1, deviceId: "device-2", tokens: { operator: { token: "secret", role: "operator", scopes: [], updatedAtMs: 1, }, }, }); clearDeviceAuthTokenFromStore({ adapter: otherDevice.adapter, deviceId: "device-1", role: "operator", }); expect(otherDevice.writes).toHaveLength(0); }); it("removes normalized roles when clearing stored tokens", () => { const { adapter, writes, readStore } = createAdapter({ version: 1, deviceId: "device-1", tokens: { operator: { token: "secret", role: "operator", scopes: ["operator.read"], updatedAtMs: 1, }, node: { token: "node-token", role: "node", scopes: [], updatedAtMs: 2, }, }, }); clearDeviceAuthTokenFromStore({ adapter, deviceId: "device-1", role: " operator ", }); expect(writes).toHaveLength(1); expect(readStore()).toEqual({ version: 1, deviceId: "device-1", tokens: { node: { token: "node-token", role: "node", scopes: [], updatedAtMs: 2, }, }, }); }); });