mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
refactor: share plugin approval test helpers
This commit is contained in:
@@ -8,6 +8,50 @@ function createManager() {
|
||||
return new ExecApprovalManager<PluginApprovalRequestPayload>();
|
||||
}
|
||||
|
||||
function createLogGatewayMock() {
|
||||
return { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() };
|
||||
}
|
||||
|
||||
function createApprovalContext(
|
||||
params: {
|
||||
broadcast?: ReturnType<typeof vi.fn>;
|
||||
hasExecApprovalClients?: GatewayRequestHandlerOptions["context"]["hasExecApprovalClients"];
|
||||
} = {},
|
||||
): GatewayRequestHandlerOptions["context"] {
|
||||
return {
|
||||
broadcast: params.broadcast ?? vi.fn(),
|
||||
logGateway: createLogGatewayMock(),
|
||||
hasExecApprovalClients: params.hasExecApprovalClients ?? (() => true),
|
||||
} as unknown as GatewayRequestHandlerOptions["context"];
|
||||
}
|
||||
|
||||
function createClient(
|
||||
params: {
|
||||
connId?: string;
|
||||
clientId?: string;
|
||||
displayName?: string;
|
||||
deviceId?: string;
|
||||
scopes?: string[];
|
||||
} = {},
|
||||
): GatewayRequestHandlerOptions["client"] {
|
||||
const connect: Record<string, unknown> = {
|
||||
client: {
|
||||
id: params.clientId ?? "test-client",
|
||||
displayName: params.displayName ?? "Test Client",
|
||||
},
|
||||
};
|
||||
if (params.deviceId) {
|
||||
connect.device = { id: params.deviceId };
|
||||
}
|
||||
if (params.scopes) {
|
||||
connect.scopes = params.scopes;
|
||||
}
|
||||
return {
|
||||
connId: params.connId ?? "conn-test-client",
|
||||
connect,
|
||||
} as unknown as GatewayRequestHandlerOptions["client"];
|
||||
}
|
||||
|
||||
function createMockOptions(
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
@@ -16,29 +60,16 @@ function createMockOptions(
|
||||
return {
|
||||
req: { method, params, id: "req-1" },
|
||||
params,
|
||||
client: {
|
||||
connId: "conn-test-client",
|
||||
connect: {
|
||||
client: { id: "test-client", displayName: "Test Client" },
|
||||
},
|
||||
},
|
||||
client: createClient(),
|
||||
isWebchatConnect: () => false,
|
||||
respond: vi.fn(),
|
||||
context: {
|
||||
broadcast: vi.fn(),
|
||||
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
||||
hasExecApprovalClients: () => true,
|
||||
},
|
||||
context: createApprovalContext(),
|
||||
...overrides,
|
||||
} as unknown as GatewayRequestHandlerOptions;
|
||||
}
|
||||
|
||||
function createNoExecApprovalContext(): GatewayRequestHandlerOptions["context"] {
|
||||
return {
|
||||
broadcast: vi.fn(),
|
||||
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
||||
hasExecApprovalClients: () => false,
|
||||
} as unknown as GatewayRequestHandlerOptions["context"];
|
||||
return createApprovalContext({ hasExecApprovalClients: () => false });
|
||||
}
|
||||
|
||||
type MockCallSource = {
|
||||
@@ -59,15 +90,15 @@ function requireArray(value: unknown, label: string): unknown[] {
|
||||
return value as unknown[];
|
||||
}
|
||||
|
||||
function mockCall(source: MockCallSource, index: number, label: string) {
|
||||
const call = source.mock.calls[index];
|
||||
function mockCall(source: unknown, index: number, label: string) {
|
||||
const call = (source as MockCallSource).mock.calls[index];
|
||||
if (!call) {
|
||||
throw new Error(`Expected ${label}`);
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
function responseCall(source: MockCallSource, index = 0) {
|
||||
function responseCall(source: unknown, index = 0) {
|
||||
const call = mockCall(source, index, `response call ${index}`);
|
||||
return {
|
||||
ok: call[0],
|
||||
@@ -76,16 +107,17 @@ function responseCall(source: MockCallSource, index = 0) {
|
||||
};
|
||||
}
|
||||
|
||||
function responseResult(source: MockCallSource, index = 0) {
|
||||
function responseResult(source: unknown, index = 0) {
|
||||
return requireRecord(responseCall(source, index).result, `response result ${index}`);
|
||||
}
|
||||
|
||||
function responseError(source: MockCallSource, index = 0) {
|
||||
function responseError(source: unknown, index = 0) {
|
||||
return requireRecord(responseCall(source, index).error, `response error ${index}`);
|
||||
}
|
||||
|
||||
function acceptedResult(source: MockCallSource) {
|
||||
const call = Array.from(source.mock.calls).find((candidate) => {
|
||||
function acceptedResult(source: unknown) {
|
||||
const callSource = source as MockCallSource;
|
||||
const call = Array.from(callSource.mock.calls).find((candidate) => {
|
||||
const result = candidate[1];
|
||||
return typeof result === "object" && result !== null && "status" in result
|
||||
? (result as Record<string, unknown>).status === "accepted"
|
||||
@@ -97,12 +129,74 @@ function acceptedResult(source: MockCallSource) {
|
||||
return requireRecord(call[1], "accepted response result");
|
||||
}
|
||||
|
||||
function acceptedApprovalId(source: MockCallSource) {
|
||||
function acceptedApprovalId(source: unknown) {
|
||||
const id = acceptedResult(source).id;
|
||||
expect(id, "accepted approval id").toBeTypeOf("string");
|
||||
return id as string;
|
||||
}
|
||||
|
||||
function expectResponseOk(source: unknown, index = 0) {
|
||||
const call = responseCall(source, index);
|
||||
expect(call.ok).toBe(true);
|
||||
expect(call.error).toBeUndefined();
|
||||
return requireRecord(call.result, `response result ${index}`);
|
||||
}
|
||||
|
||||
function expectResponseRejected(source: unknown, index = 0) {
|
||||
expect(responseCall(source, index).ok).toBe(false);
|
||||
return responseError(source, index);
|
||||
}
|
||||
|
||||
async function waitForAcceptedApproval(respond: unknown) {
|
||||
await vi.waitFor(() => {
|
||||
const accepted = acceptedResult(respond);
|
||||
expect(accepted.status).toBe("accepted");
|
||||
expect(accepted.id).toBeTypeOf("string");
|
||||
});
|
||||
return acceptedApprovalId(respond);
|
||||
}
|
||||
|
||||
function createOwnedClient(owner: "owner" | "other" = "owner") {
|
||||
return createClient({
|
||||
connId: `conn-${owner}`,
|
||||
clientId: `client-${owner}`,
|
||||
deviceId: `device-${owner}`,
|
||||
});
|
||||
}
|
||||
|
||||
function registerApproval(
|
||||
approvalManager: ExecApprovalManager<PluginApprovalRequestPayload>,
|
||||
params: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
id?: string;
|
||||
allowedDecisions?: PluginApprovalRequestPayload["allowedDecisions"];
|
||||
} = {},
|
||||
) {
|
||||
const request = {
|
||||
title: params.title ?? "T",
|
||||
description: params.description ?? "D",
|
||||
...(params.allowedDecisions ? { allowedDecisions: params.allowedDecisions } : {}),
|
||||
};
|
||||
const record = params.id
|
||||
? approvalManager.create(request, 60_000, params.id)
|
||||
: approvalManager.create(request, 60_000);
|
||||
void approvalManager.register(record, 60_000);
|
||||
return record;
|
||||
}
|
||||
|
||||
function registerOwnedApproval(
|
||||
approvalManager: ExecApprovalManager<PluginApprovalRequestPayload>,
|
||||
params: { title: string; id?: string; owner?: "owner" | "other" },
|
||||
) {
|
||||
const record = registerApproval(approvalManager, { title: params.title, id: params.id });
|
||||
const owner = params.owner ?? "owner";
|
||||
record.requestedByDeviceId = `device-${owner}`;
|
||||
record.requestedByConnId = `conn-${owner}`;
|
||||
record.requestedByClientId = `client-${owner}`;
|
||||
return record;
|
||||
}
|
||||
|
||||
function expectPluginApprovalId(value: unknown, label: string): string {
|
||||
expect(value, label).toBeTypeOf("string");
|
||||
if (typeof value !== "string") {
|
||||
@@ -120,11 +214,7 @@ function expectPluginApprovalId(value: unknown, label: string): string {
|
||||
}
|
||||
|
||||
function broadcastCall(opts: GatewayRequestHandlerOptions, index = 0) {
|
||||
const call = mockCall(
|
||||
opts.context.broadcast as unknown as MockCallSource,
|
||||
index,
|
||||
"broadcast call",
|
||||
);
|
||||
const call = mockCall(opts.context.broadcast, index, "broadcast call");
|
||||
return {
|
||||
event: call?.[0],
|
||||
payload: requireRecord(call?.[1], "broadcast payload"),
|
||||
@@ -137,6 +227,25 @@ const invalidParamMethodCases = [
|
||||
{ method: "plugin.approval.resolve" },
|
||||
] as const;
|
||||
|
||||
const invalidRequestCases = [
|
||||
{
|
||||
name: "invalid severity value",
|
||||
params: { title: "T", description: "D", severity: "extreme" },
|
||||
},
|
||||
{
|
||||
name: "title exceeding max length",
|
||||
params: { title: "x".repeat(81), description: "D" },
|
||||
},
|
||||
{
|
||||
name: "description exceeding max length",
|
||||
params: { title: "T", description: "x".repeat(257) },
|
||||
},
|
||||
{
|
||||
name: "timeoutMs exceeding max",
|
||||
params: { title: "T", description: "D", timeoutMs: 700_000 },
|
||||
},
|
||||
] as const;
|
||||
|
||||
describe("createPluginApprovalHandlers", () => {
|
||||
let manager: ExecApprovalManager<PluginApprovalRequestPayload>;
|
||||
|
||||
@@ -163,9 +272,8 @@ describe("createPluginApprovalHandlers", () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const opts = createMockOptions(method, {});
|
||||
await handlers[method](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).result).toBeUndefined();
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
|
||||
expect(responseCall(opts.respond).result).toBeUndefined();
|
||||
expect(expectResponseRejected(opts.respond).code).toBeTypeOf("string");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,12 +296,7 @@ describe("createPluginApprovalHandlers", () => {
|
||||
// Instead, let it run and resolve the approval after the accepted response.
|
||||
const handlerPromise = handlers["plugin.approval.request"](opts);
|
||||
|
||||
// Wait for the twoPhase "accepted" response
|
||||
await vi.waitFor(() => {
|
||||
const accepted = acceptedResult(respond as unknown as MockCallSource);
|
||||
expect(accepted.status).toBe("accepted");
|
||||
expect(accepted.id).toBeTypeOf("string");
|
||||
});
|
||||
const approvalId = await waitForAcceptedApproval(respond);
|
||||
|
||||
const requestedBroadcast = broadcastCall(opts);
|
||||
expect(requestedBroadcast.event).toBe("plugin.approval.requested");
|
||||
@@ -201,18 +304,15 @@ describe("createPluginApprovalHandlers", () => {
|
||||
expect(requestedBroadcast.options).toEqual({ dropIfSlow: true });
|
||||
|
||||
// Resolve the approval so the handler can complete
|
||||
const approvalId = acceptedApprovalId(respond as unknown as MockCallSource);
|
||||
expect(manager.getSnapshot(approvalId)?.requestedByClientId).toBe("test-client");
|
||||
manager.resolve(approvalId, "allow-once");
|
||||
|
||||
await handlerPromise;
|
||||
|
||||
// Final response with decision
|
||||
const finalResult = responseResult(respond as unknown as MockCallSource, 1);
|
||||
expect(responseCall(respond as unknown as MockCallSource, 1).ok).toBe(true);
|
||||
const finalResult = expectResponseOk(respond, 1);
|
||||
expect(finalResult.id).toBe(approvalId);
|
||||
expect(finalResult.decision).toBe("allow-once");
|
||||
expect(responseCall(respond as unknown as MockCallSource, 1).error).toBeUndefined();
|
||||
});
|
||||
|
||||
it("expires immediately when no approval route", async () => {
|
||||
@@ -228,9 +328,7 @@ describe("createPluginApprovalHandlers", () => {
|
||||
},
|
||||
);
|
||||
await handlers["plugin.approval.request"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(true);
|
||||
expect(responseResult(opts.respond as unknown as MockCallSource).decision).toBeNull();
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).error).toBeUndefined();
|
||||
expect(expectResponseOk(opts.respond).decision).toBeNull();
|
||||
});
|
||||
|
||||
it("passes caller connId to hasExecApprovalClients to exclude self", async () => {
|
||||
@@ -240,15 +338,12 @@ describe("createPluginApprovalHandlers", () => {
|
||||
"plugin.approval.request",
|
||||
{ title: "T", description: "D" },
|
||||
{
|
||||
client: {
|
||||
client: createClient({
|
||||
connId: "backend-conn-42",
|
||||
connect: { client: { id: "test", displayName: "Test" } },
|
||||
} as unknown as GatewayRequestHandlerOptions["client"],
|
||||
context: {
|
||||
broadcast: vi.fn(),
|
||||
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
||||
hasExecApprovalClients,
|
||||
} as unknown as GatewayRequestHandlerOptions["context"],
|
||||
clientId: "test",
|
||||
displayName: "Test",
|
||||
}),
|
||||
context: createApprovalContext({ hasExecApprovalClients }),
|
||||
},
|
||||
);
|
||||
await handlers["plugin.approval.request"](opts);
|
||||
@@ -271,23 +366,12 @@ describe("createPluginApprovalHandlers", () => {
|
||||
},
|
||||
{
|
||||
respond,
|
||||
context: {
|
||||
broadcast: vi.fn(),
|
||||
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
||||
hasExecApprovalClients: () => false,
|
||||
} as unknown as GatewayRequestHandlerOptions["context"],
|
||||
context: createApprovalContext({ hasExecApprovalClients: () => false }),
|
||||
},
|
||||
);
|
||||
|
||||
const requestPromise = handlers["plugin.approval.request"](opts);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const accepted = acceptedResult(respond as unknown as MockCallSource);
|
||||
expect(accepted.status).toBe("accepted");
|
||||
expect(accepted.id).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
const approvalId = acceptedApprovalId(respond as unknown as MockCallSource);
|
||||
const approvalId = await waitForAcceptedApproval(respond);
|
||||
manager.resolve(approvalId, "allow-once");
|
||||
|
||||
await requestPromise;
|
||||
@@ -296,50 +380,11 @@ describe("createPluginApprovalHandlers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects invalid severity value", async () => {
|
||||
it.each(invalidRequestCases)("rejects $name", async ({ params }) => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const opts = createMockOptions("plugin.approval.request", {
|
||||
title: "T",
|
||||
description: "D",
|
||||
severity: "extreme",
|
||||
});
|
||||
const opts = createMockOptions("plugin.approval.request", params);
|
||||
await handlers["plugin.approval.request"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
it("rejects title exceeding max length", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const opts = createMockOptions("plugin.approval.request", {
|
||||
title: "x".repeat(81),
|
||||
description: "D",
|
||||
});
|
||||
await handlers["plugin.approval.request"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
it("rejects description exceeding max length", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const opts = createMockOptions("plugin.approval.request", {
|
||||
title: "T",
|
||||
description: "x".repeat(257),
|
||||
});
|
||||
await handlers["plugin.approval.request"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
it("rejects timeoutMs exceeding max", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const opts = createMockOptions("plugin.approval.request", {
|
||||
title: "T",
|
||||
description: "D",
|
||||
timeoutMs: 700_000,
|
||||
});
|
||||
await handlers["plugin.approval.request"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
|
||||
expect(expectResponseRejected(opts.respond).code).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
it("generates plugin-prefixed IDs", async () => {
|
||||
@@ -350,15 +395,11 @@ describe("createPluginApprovalHandlers", () => {
|
||||
{ title: "T", description: "D" },
|
||||
{
|
||||
respond,
|
||||
context: {
|
||||
broadcast: vi.fn(),
|
||||
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
||||
hasExecApprovalClients: () => false,
|
||||
} as unknown as GatewayRequestHandlerOptions["context"],
|
||||
context: createApprovalContext({ hasExecApprovalClients: () => false }),
|
||||
},
|
||||
);
|
||||
await handlers["plugin.approval.request"](opts);
|
||||
const result = responseResult(respond as unknown as MockCallSource);
|
||||
const result = responseResult(respond);
|
||||
expectPluginApprovalId(result?.id, "generated plugin approval id");
|
||||
});
|
||||
|
||||
@@ -390,10 +431,8 @@ describe("createPluginApprovalHandlers", () => {
|
||||
description: "D",
|
||||
});
|
||||
await handlers["plugin.approval.request"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).message).toContain(
|
||||
"unexpected property",
|
||||
);
|
||||
expect(responseCall(opts.respond).ok).toBe(false);
|
||||
expect(responseError(opts.respond).message).toContain("unexpected property");
|
||||
});
|
||||
|
||||
it("stores scoped allowed decisions on plugin approval requests", async () => {
|
||||
@@ -411,13 +450,7 @@ describe("createPluginApprovalHandlers", () => {
|
||||
);
|
||||
|
||||
const handlerPromise = handlers["plugin.approval.request"](opts);
|
||||
await vi.waitFor(() => {
|
||||
const accepted = acceptedResult(respond as unknown as MockCallSource);
|
||||
expect(accepted.status).toBe("accepted");
|
||||
expect(accepted.id).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
const approvalId = acceptedApprovalId(respond as unknown as MockCallSource);
|
||||
const approvalId = await waitForAcceptedApproval(respond);
|
||||
expect(manager.getSnapshot(approvalId)?.request.allowedDecisions).toEqual([
|
||||
"allow-once",
|
||||
"deny",
|
||||
@@ -442,30 +475,23 @@ describe("createPluginApprovalHandlers", () => {
|
||||
);
|
||||
|
||||
const handlerPromise = handlers["plugin.approval.request"](requestOpts);
|
||||
await vi.waitFor(() => {
|
||||
const accepted = acceptedResult(respond as unknown as MockCallSource);
|
||||
expect(accepted.status).toBe("accepted");
|
||||
expect(accepted.id).toBeTypeOf("string");
|
||||
});
|
||||
const approvalId = await waitForAcceptedApproval(respond);
|
||||
|
||||
const listRespond = vi.fn();
|
||||
await handlers["plugin.approval.list"](
|
||||
createMockOptions("plugin.approval.list", {}, { respond: listRespond }),
|
||||
);
|
||||
expect(responseCall(listRespond as unknown as MockCallSource).ok).toBe(true);
|
||||
const approvals = requireArray(
|
||||
responseCall(listRespond as unknown as MockCallSource).result,
|
||||
"approval list",
|
||||
);
|
||||
const listCall = responseCall(listRespond);
|
||||
expect(listCall.ok).toBe(true);
|
||||
expect(listCall.error).toBeUndefined();
|
||||
const approvals = requireArray(listCall.result, "approval list");
|
||||
expect(approvals).toHaveLength(1);
|
||||
const approval = requireRecord(approvals[0], "approval");
|
||||
const listedApprovalId = expectPluginApprovalId(approval.id, "listed approval id");
|
||||
const request = requireRecord(approval.request, "approval request");
|
||||
expect(request.title).toBe("Sensitive action");
|
||||
expect(request.description).toBe("Desc");
|
||||
expect(responseCall(listRespond as unknown as MockCallSource).error).toBeUndefined();
|
||||
|
||||
const approvalId = acceptedApprovalId(respond as unknown as MockCallSource);
|
||||
expect(listedApprovalId).toBe(approvalId);
|
||||
manager.resolve(approvalId, "allow-once");
|
||||
await handlerPromise;
|
||||
@@ -473,21 +499,12 @@ describe("createPluginApprovalHandlers", () => {
|
||||
|
||||
it("lists only plugin approvals owned by the caller", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const visible = manager.create(
|
||||
{ title: "Visible", description: "D" },
|
||||
60_000,
|
||||
"plugin:visible",
|
||||
);
|
||||
visible.requestedByDeviceId = "device-owner";
|
||||
visible.requestedByConnId = "conn-owner";
|
||||
visible.requestedByClientId = "client-owner";
|
||||
void manager.register(visible, 60_000);
|
||||
|
||||
const hidden = manager.create({ title: "Hidden", description: "D" }, 60_000, "plugin:hidden");
|
||||
hidden.requestedByDeviceId = "device-other";
|
||||
hidden.requestedByConnId = "conn-other";
|
||||
hidden.requestedByClientId = "client-other";
|
||||
void manager.register(hidden, 60_000);
|
||||
registerOwnedApproval(manager, { title: "Visible", id: "plugin:visible" });
|
||||
registerOwnedApproval(manager, {
|
||||
title: "Hidden",
|
||||
id: "plugin:hidden",
|
||||
owner: "other",
|
||||
});
|
||||
|
||||
const listRespond = vi.fn();
|
||||
await handlers["plugin.approval.list"](
|
||||
@@ -496,22 +513,14 @@ describe("createPluginApprovalHandlers", () => {
|
||||
{},
|
||||
{
|
||||
respond: listRespond,
|
||||
client: {
|
||||
connId: "conn-owner",
|
||||
connect: {
|
||||
client: { id: "client-owner" },
|
||||
device: { id: "device-owner" },
|
||||
},
|
||||
} as unknown as GatewayRequestHandlerOptions["client"],
|
||||
client: createOwnedClient(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(responseCall(listRespond as unknown as MockCallSource).ok).toBe(true);
|
||||
const approvals = requireArray(
|
||||
responseCall(listRespond as unknown as MockCallSource).result,
|
||||
"approval list",
|
||||
);
|
||||
const listCall = responseCall(listRespond);
|
||||
expect(listCall.ok).toBe(true);
|
||||
const approvals = requireArray(listCall.result, "approval list");
|
||||
expect(approvals.map((entry) => requireRecord(entry, "approval").id)).toEqual([
|
||||
"plugin:visible",
|
||||
]);
|
||||
@@ -523,89 +532,67 @@ describe("createPluginApprovalHandlers", () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const opts = createMockOptions("plugin.approval.waitDecision", {});
|
||||
await handlers["plugin.approval.waitDecision"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).message).toContain(
|
||||
"id is required",
|
||||
);
|
||||
expect(expectResponseRejected(opts.respond).message).toContain("id is required");
|
||||
});
|
||||
|
||||
it("returns not found for unknown id", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const opts = createMockOptions("plugin.approval.waitDecision", { id: "unknown" });
|
||||
await handlers["plugin.approval.waitDecision"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).message).toContain(
|
||||
"expired or not found",
|
||||
);
|
||||
expect(expectResponseRejected(opts.respond).message).toContain("expired or not found");
|
||||
});
|
||||
|
||||
it("returns not found for approvals hidden from the caller", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const record = manager.create({ title: "T", description: "D" }, 60_000);
|
||||
record.requestedByDeviceId = "device-owner";
|
||||
record.requestedByConnId = "conn-owner";
|
||||
record.requestedByClientId = "client-owner";
|
||||
void manager.register(record, 60_000);
|
||||
const record = registerOwnedApproval(manager, { title: "T" });
|
||||
manager.resolve(record.id, "allow-once");
|
||||
|
||||
const opts = createMockOptions(
|
||||
"plugin.approval.waitDecision",
|
||||
{ id: record.id },
|
||||
{
|
||||
client: {
|
||||
client: createClient({
|
||||
connId: "conn-other",
|
||||
connect: {
|
||||
client: { id: "client-other" },
|
||||
device: { id: "device-other" },
|
||||
scopes: ["operator.approvals"],
|
||||
},
|
||||
} as unknown as GatewayRequestHandlerOptions["client"],
|
||||
clientId: "client-other",
|
||||
deviceId: "device-other",
|
||||
scopes: ["operator.approvals"],
|
||||
}),
|
||||
},
|
||||
);
|
||||
await handlers["plugin.approval.waitDecision"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).message).toContain(
|
||||
"expired or not found",
|
||||
);
|
||||
expect(expectResponseRejected(opts.respond).message).toContain("expired or not found");
|
||||
});
|
||||
|
||||
it("returns decision when resolved", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const record = manager.create({ title: "T", description: "D" }, 60_000);
|
||||
void manager.register(record, 60_000);
|
||||
const record = registerApproval(manager);
|
||||
|
||||
// Resolve before waiting
|
||||
manager.resolve(record.id, "allow-once");
|
||||
|
||||
const opts = createMockOptions("plugin.approval.waitDecision", { id: record.id });
|
||||
await handlers["plugin.approval.waitDecision"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(true);
|
||||
expect(responseResult(opts.respond as unknown as MockCallSource).id).toBe(record.id);
|
||||
expect(responseResult(opts.respond as unknown as MockCallSource).decision).toBe("allow-once");
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).error).toBeUndefined();
|
||||
const result = expectResponseOk(opts.respond);
|
||||
expect(result.id).toBe(record.id);
|
||||
expect(result.decision).toBe("allow-once");
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin.approval.resolve", () => {
|
||||
it("rejects invalid decision", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const record = manager.create({ title: "T", description: "D" }, 60_000);
|
||||
void manager.register(record, 60_000);
|
||||
const record = registerApproval(manager);
|
||||
const opts = createMockOptions("plugin.approval.resolve", {
|
||||
id: record.id,
|
||||
decision: "invalid",
|
||||
});
|
||||
await handlers["plugin.approval.resolve"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
expect(responseError(opts.respond as unknown as MockCallSource).message).toBe(
|
||||
"invalid decision",
|
||||
);
|
||||
expect(expectResponseRejected(opts.respond).message).toBe("invalid decision");
|
||||
});
|
||||
|
||||
it("resolves a pending approval", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const record = manager.create({ title: "T", description: "D" }, 60_000);
|
||||
void manager.register(record, 60_000);
|
||||
const record = registerApproval(manager);
|
||||
|
||||
const opts = createMockOptions("plugin.approval.resolve", {
|
||||
id: record.id,
|
||||
@@ -622,33 +609,17 @@ describe("createPluginApprovalHandlers", () => {
|
||||
|
||||
it("resolves only plugin approvals owned by the caller", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const visible = manager.create(
|
||||
{ title: "Visible", description: "D" },
|
||||
60_000,
|
||||
"plugin:abcd-visible",
|
||||
);
|
||||
visible.requestedByDeviceId = "device-owner";
|
||||
visible.requestedByConnId = "conn-owner";
|
||||
visible.requestedByClientId = "client-owner";
|
||||
void manager.register(visible, 60_000);
|
||||
const visible = registerOwnedApproval(manager, {
|
||||
title: "Visible",
|
||||
id: "plugin:abcd-visible",
|
||||
});
|
||||
const hidden = registerOwnedApproval(manager, {
|
||||
title: "Hidden",
|
||||
id: "plugin:abcd-hidden",
|
||||
owner: "other",
|
||||
});
|
||||
|
||||
const hidden = manager.create(
|
||||
{ title: "Hidden", description: "D" },
|
||||
60_000,
|
||||
"plugin:abcd-hidden",
|
||||
);
|
||||
hidden.requestedByDeviceId = "device-other";
|
||||
hidden.requestedByConnId = "conn-other";
|
||||
hidden.requestedByClientId = "client-other";
|
||||
void manager.register(hidden, 60_000);
|
||||
|
||||
const ownerClient = {
|
||||
connId: "conn-owner",
|
||||
connect: {
|
||||
client: { id: "client-owner" },
|
||||
device: { id: "device-owner" },
|
||||
},
|
||||
} as unknown as GatewayRequestHandlerOptions["client"];
|
||||
const ownerClient = createOwnedClient();
|
||||
const resolveRespond = vi.fn();
|
||||
await handlers["plugin.approval.resolve"](
|
||||
createMockOptions(
|
||||
@@ -681,8 +652,7 @@ describe("createPluginApprovalHandlers", () => {
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(responseCall(hiddenRespond as unknown as MockCallSource).ok).toBe(false);
|
||||
const error = responseError(hiddenRespond as unknown as MockCallSource);
|
||||
const error = expectResponseRejected(hiddenRespond);
|
||||
expect(error.code).toBe("INVALID_REQUEST");
|
||||
expect(error.message).toBe("unknown or expired approval id");
|
||||
expect(manager.getSnapshot(hidden.id)?.decision).toBeUndefined();
|
||||
@@ -690,23 +660,16 @@ describe("createPluginApprovalHandlers", () => {
|
||||
|
||||
it("rejects decisions outside plugin approval allowed decisions", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const record = manager.create(
|
||||
{
|
||||
title: "T",
|
||||
description: "D",
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
void manager.register(record, 60_000);
|
||||
const record = registerApproval(manager, {
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
});
|
||||
|
||||
const opts = createMockOptions("plugin.approval.resolve", {
|
||||
id: record.id,
|
||||
decision: "allow-always",
|
||||
});
|
||||
await handlers["plugin.approval.resolve"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
const error = responseError(opts.respond as unknown as MockCallSource);
|
||||
const error = expectResponseRejected(opts.respond);
|
||||
expect(error.code).toBe("INVALID_REQUEST");
|
||||
expect(error.message).toBe("allow-always is unavailable for this plugin approval");
|
||||
expect(error.details).toEqual({ allowedDecisions: ["allow-once", "deny"] });
|
||||
@@ -720,8 +683,7 @@ describe("createPluginApprovalHandlers", () => {
|
||||
decision: "allow-once",
|
||||
});
|
||||
await handlers["plugin.approval.resolve"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
const error = responseError(opts.respond as unknown as MockCallSource);
|
||||
const error = expectResponseRejected(opts.respond);
|
||||
expect(error.code).toBe("INVALID_REQUEST");
|
||||
expect(error.message).toContain("unknown or expired");
|
||||
expect(requireRecord(error.details, "error details").reason).toBe("APPROVAL_NOT_FOUND");
|
||||
@@ -729,8 +691,7 @@ describe("createPluginApprovalHandlers", () => {
|
||||
|
||||
it("accepts unique short id prefixes", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const record = manager.create({ title: "T", description: "D" }, 60_000, "abcdef-1234");
|
||||
void manager.register(record, 60_000);
|
||||
const record = registerApproval(manager, { id: "abcdef-1234" });
|
||||
|
||||
const opts = createMockOptions("plugin.approval.resolve", {
|
||||
id: "abcdef",
|
||||
@@ -743,18 +704,15 @@ describe("createPluginApprovalHandlers", () => {
|
||||
|
||||
it("does not leak candidate ids when prefixes are ambiguous", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const recordA = manager.create({ title: "A", description: "D" }, 60_000, "plugin:abc-1111");
|
||||
const recordB = manager.create({ title: "B", description: "D" }, 60_000, "plugin:abc-2222");
|
||||
void manager.register(recordA, 60_000);
|
||||
void manager.register(recordB, 60_000);
|
||||
registerApproval(manager, { title: "A", id: "plugin:abc-1111" });
|
||||
registerApproval(manager, { title: "B", id: "plugin:abc-2222" });
|
||||
|
||||
const opts = createMockOptions("plugin.approval.resolve", {
|
||||
id: "plugin:abc",
|
||||
decision: "deny",
|
||||
});
|
||||
await handlers["plugin.approval.resolve"](opts);
|
||||
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
|
||||
const error = responseError(opts.respond as unknown as MockCallSource);
|
||||
const error = expectResponseRejected(opts.respond);
|
||||
expect(error.code).toBe("INVALID_REQUEST");
|
||||
expect(error.message).toBe("unknown or expired approval id");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user