mirror of https://github.com/openclaw/openclaw.git
test: add device auth store coverage
This commit is contained in:
parent
e7fb2fea5c
commit
35cf3d0ce5
|
|
@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue