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