


























@@ -0,0 +1,206 @@
1+import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { registerPlatformAdapter, type PlatformAdapter } from "../adapter/index.js";
4+import type { InteractionEvent } from "../types.js";
5+import { createInteractionHandler } from "./interaction-handler.js";
6+import type { GatewayAccount, GatewayPluginRuntime } from "./types.js";
7+8+const acknowledgeInteractionMock = vi.hoisted(() => vi.fn(async () => undefined));
9+10+vi.mock("../messaging/sender.js", () => ({
11+accountToCreds: (account: GatewayAccount) => ({
12+appId: account.appId,
13+clientSecret: account.clientSecret,
14+}),
15+acknowledgeInteraction: acknowledgeInteractionMock,
16+}));
17+18+const resolveApprovalMock = vi.fn(async () => true);
19+20+const account: GatewayAccount = {
21+accountId: "default",
22+appId: "app",
23+clientSecret: "secret",
24+markdownSupport: false,
25+config: {},
26+};
27+28+const runtime = {} as GatewayPluginRuntime;
29+30+function makeRestrictedCfg(approvers: string[]): OpenClawConfig {
31+return {
32+channels: {
33+qqbot: {
34+appId: "app",
35+clientSecret: "secret",
36+execApprovals: {
37+enabled: true,
38+ approvers,
39+},
40+},
41+},
42+} as OpenClawConfig;
43+}
44+45+function makeUnrestrictedCfg(): OpenClawConfig {
46+return {
47+channels: {
48+qqbot: {
49+appId: "app",
50+clientSecret: "secret",
51+},
52+},
53+} as OpenClawConfig;
54+}
55+56+function makeApprovalEvent(overrides: Partial<InteractionEvent> = {}): InteractionEvent {
57+return {
58+id: "interaction-1",
59+type: 11,
60+chat_type: 1,
61+group_openid: "group-1",
62+group_member_openid: "ATTACKER_OPENID",
63+version: 1,
64+data: {
65+type: 11,
66+resolved: {
67+button_data: "approve:exec:abc12345:allow-once",
68+user_id: "ATTACKER_USER_ID",
69+},
70+},
71+ ...overrides,
72+};
73+}
74+75+function installPlatformAdapter(): void {
76+registerPlatformAdapter({
77+validateRemoteUrl: vi.fn(async () => undefined),
78+resolveSecret: vi.fn(async (value: unknown) => (typeof value === "string" ? value : undefined)),
79+downloadFile: vi.fn(async () => "/tmp/file"),
80+fetchMedia: vi.fn(async () => {
81+throw new Error("unused");
82+}),
83+getTempDir: () => "/tmp",
84+hasConfiguredSecret: (value: unknown) => typeof value === "string" && value.length > 0,
85+normalizeSecretInputString: (value: unknown) => (typeof value === "string" ? value : undefined),
86+resolveSecretInputString: ({ value }: { value: unknown }) =>
87+typeof value === "string" ? value : undefined,
88+resolveApproval: resolveApprovalMock,
89+} as PlatformAdapter);
90+}
91+92+describe("createInteractionHandler approval buttons", () => {
93+beforeEach(() => {
94+vi.clearAllMocks();
95+installPlatformAdapter();
96+});
97+98+it("rejects approval button clicks from users outside the configured approvers", async () => {
99+const handler = createInteractionHandler(account, runtime, undefined, {
100+getActiveCfg: () => makeRestrictedCfg(["OWNER_OPENID"]),
101+});
102+103+handler(makeApprovalEvent());
104+105+await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
106+107+expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
108+{ appId: "app", clientSecret: "secret" },
109+"interaction-1",
110+0,
111+{ content: "You are not authorized to approve this request." },
112+);
113+expect(resolveApprovalMock).not.toHaveBeenCalled();
114+});
115+116+it("does not authorize from resolved user id when the actor openid is not approved", async () => {
117+const handler = createInteractionHandler(account, runtime, undefined, {
118+getActiveCfg: () => makeRestrictedCfg(["OWNER_OPENID"]),
119+});
120+121+handler(
122+makeApprovalEvent({
123+data: {
124+type: 11,
125+resolved: {
126+button_data: "approve:exec:abc12345:allow-once",
127+user_id: "OWNER_OPENID",
128+},
129+},
130+}),
131+);
132+133+await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
134+135+expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
136+{ appId: "app", clientSecret: "secret" },
137+"interaction-1",
138+0,
139+{ content: "You are not authorized to approve this request." },
140+);
141+expect(resolveApprovalMock).not.toHaveBeenCalled();
142+});
143+144+it("resolves approval button clicks from configured approvers", async () => {
145+const handler = createInteractionHandler(account, runtime, undefined, {
146+getActiveCfg: () => makeRestrictedCfg(["OWNER_OPENID"]),
147+});
148+149+handler(makeApprovalEvent({ group_member_openid: "OWNER_OPENID" }));
150+151+await vi.waitFor(() =>
152+expect(resolveApprovalMock).toHaveBeenCalledWith("exec:abc12345", "allow-once"),
153+);
154+});
155+156+it("uses the direct user openid when a group member openid is unavailable", async () => {
157+const handler = createInteractionHandler(account, runtime, undefined, {
158+getActiveCfg: () => makeRestrictedCfg(["OWNER_OPENID"]),
159+});
160+161+handler(
162+makeApprovalEvent({
163+chat_type: 2,
164+group_openid: undefined,
165+group_member_openid: undefined,
166+user_openid: "OWNER_OPENID",
167+}),
168+);
169+170+await vi.waitFor(() =>
171+expect(resolveApprovalMock).toHaveBeenCalledWith("exec:abc12345", "allow-once"),
172+);
173+});
174+175+it("allows approval button clicks when exec approvals are not configured", async () => {
176+const handler = createInteractionHandler(account, runtime, undefined, {
177+getActiveCfg: () => makeUnrestrictedCfg(),
178+});
179+180+handler(makeApprovalEvent());
181+182+await vi.waitFor(() =>
183+expect(resolveApprovalMock).toHaveBeenCalledWith("exec:abc12345", "allow-once"),
184+);
185+});
186+187+it("rejects approval button clicks when active config cannot be loaded", async () => {
188+const handler = createInteractionHandler(account, runtime, undefined, {
189+getActiveCfg: () => {
190+throw new Error("config unavailable");
191+},
192+});
193+194+handler(makeApprovalEvent());
195+196+await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
197+198+expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
199+{ appId: "app", clientSecret: "secret" },
200+"interaction-1",
201+0,
202+{ content: "Approval is unavailable." },
203+);
204+expect(resolveApprovalMock).not.toHaveBeenCalled();
205+});
206+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。