test: add device auth store coverage

This commit is contained in:
Peter Steinberger 2026-03-13 20:20:21 +00:00
parent e7fb2fea5c
commit 35cf3d0ce5
2 changed files with 315 additions and 0 deletions

View File

@ -0,0 +1,109 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { withTempDir } from "../test-utils/temp-dir.js";
import {
clearDeviceAuthToken,
loadDeviceAuthToken,
storeDeviceAuthToken,
} from "./device-auth-store.js";
function createEnv(stateDir: string): NodeJS.ProcessEnv {
return {
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_TEST_FAST: "1",
};
}
function deviceAuthFile(stateDir: string): string {
return path.join(stateDir, "identity", "device-auth.json");
}
describe("infra/device-auth-store", () => {
it("stores and loads device auth tokens under the configured state dir", async () => {
await withTempDir("openclaw-device-auth-", async (stateDir) => {
vi.spyOn(Date, "now").mockReturnValue(1234);
const entry = storeDeviceAuthToken({
deviceId: "device-1",
role: " operator ",
token: "secret",
scopes: [" operator.write ", "operator.read", "operator.read"],
env: createEnv(stateDir),
});
expect(entry).toEqual({
token: "secret",
role: "operator",
scopes: ["operator.read", "operator.write"],
updatedAtMs: 1234,
});
expect(
loadDeviceAuthToken({
deviceId: "device-1",
role: "operator",
env: createEnv(stateDir),
}),
).toEqual(entry);
const raw = await fs.readFile(deviceAuthFile(stateDir), "utf8");
expect(raw.endsWith("\n")).toBe(true);
expect(JSON.parse(raw)).toEqual({
version: 1,
deviceId: "device-1",
tokens: {
operator: entry,
},
});
});
});
it("returns null for missing, invalid, or mismatched stores", async () => {
await withTempDir("openclaw-device-auth-", async (stateDir) => {
const env = createEnv(stateDir);
expect(loadDeviceAuthToken({ deviceId: "device-1", role: "operator", env })).toBeNull();
await fs.mkdir(path.dirname(deviceAuthFile(stateDir)), { recursive: true });
await fs.writeFile(deviceAuthFile(stateDir), '{"version":2,"deviceId":"device-1"}\n', "utf8");
expect(loadDeviceAuthToken({ deviceId: "device-1", role: "operator", env })).toBeNull();
await fs.writeFile(
deviceAuthFile(stateDir),
'{"version":1,"deviceId":"device-2","tokens":{"operator":{"token":"x","role":"operator","scopes":[],"updatedAtMs":1}}}\n',
"utf8",
);
expect(loadDeviceAuthToken({ deviceId: "device-1", role: "operator", env })).toBeNull();
});
});
it("clears only the requested role and leaves unrelated tokens intact", async () => {
await withTempDir("openclaw-device-auth-", async (stateDir) => {
const env = createEnv(stateDir);
storeDeviceAuthToken({
deviceId: "device-1",
role: "operator",
token: "operator-token",
env,
});
storeDeviceAuthToken({
deviceId: "device-1",
role: "node",
token: "node-token",
env,
});
clearDeviceAuthToken({
deviceId: "device-1",
role: " operator ",
env,
});
expect(loadDeviceAuthToken({ deviceId: "device-1", role: "operator", env })).toBeNull();
expect(loadDeviceAuthToken({ deviceId: "device-1", role: "node", env })).toMatchObject({
token: "node-token",
});
});
});
});

View File

@ -0,0 +1,206 @@
import { describe, expect, it, vi } from "vitest";
import {
clearDeviceAuthTokenFromStore,
loadDeviceAuthTokenFromStore,
storeDeviceAuthTokenInStore,
type DeviceAuthStoreAdapter,
} from "./device-auth-store.js";
function createAdapter(initialStore: ReturnType<DeviceAuthStoreAdapter["readStore"]> = 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 ",
}),
).toMatchObject({ token: "secret" });
expect(
loadDeviceAuthTokenFromStore({
adapter,
deviceId: "device-2",
role: "operator",
}),
).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("replaces stale stores from other devices instead of merging them", () => {
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: expect.any(Number),
},
},
});
});
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,
},
},
});
});
});