mirror of https://github.com/openclaw/openclaw.git
refactor: share mattermost interaction test helpers
This commit is contained in:
parent
e731974da1
commit
2dd180472f
|
|
@ -496,6 +496,104 @@ describe("createMattermostInteractionHandler", () => {
|
|||
return res as unknown as ServerResponse & { headers: Record<string, string>; body: string };
|
||||
}
|
||||
|
||||
function createActionContext(actionId = "approve", channelId = "chan-1") {
|
||||
const context = { action_id: actionId, __openclaw_channel_id: channelId };
|
||||
return { context, token: generateInteractionToken(context, "acct") };
|
||||
}
|
||||
|
||||
function createInteractionBody(params: {
|
||||
context: Record<string, unknown>;
|
||||
token: string;
|
||||
channelId?: string;
|
||||
postId?: string;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
}) {
|
||||
return {
|
||||
user_id: params.userId ?? "user-1",
|
||||
...(params.userName ? { user_name: params.userName } : {}),
|
||||
channel_id: params.channelId ?? "chan-1",
|
||||
post_id: params.postId ?? "post-1",
|
||||
context: { ...params.context, _token: params.token },
|
||||
};
|
||||
}
|
||||
|
||||
async function runHandler(
|
||||
handler: ReturnType<typeof createMattermostInteractionHandler>,
|
||||
params: {
|
||||
body: unknown;
|
||||
remoteAddress?: string;
|
||||
headers?: Record<string, string>;
|
||||
},
|
||||
) {
|
||||
const req = createReq({
|
||||
remoteAddress: params.remoteAddress,
|
||||
headers: params.headers,
|
||||
body: params.body,
|
||||
});
|
||||
const res = createRes();
|
||||
await handler(req, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
function expectForbiddenResponse(
|
||||
res: ServerResponse & { body: string },
|
||||
expectedMessage: string,
|
||||
) {
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(res.body).toContain(expectedMessage);
|
||||
}
|
||||
|
||||
function expectSuccessfulApprovalUpdate(
|
||||
res: ServerResponse & { body: string },
|
||||
requestLog?: Array<{ path: string; method?: string }>,
|
||||
) {
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toBe("{}");
|
||||
if (requestLog) {
|
||||
expect(requestLog).toEqual([
|
||||
{ path: "/posts/post-1", method: undefined },
|
||||
{ path: "/posts/post-1", method: "PUT" },
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function createActionPost(params?: {
|
||||
actionId?: string;
|
||||
actionName?: string;
|
||||
channelId?: string;
|
||||
rootId?: string;
|
||||
}): MattermostPost {
|
||||
return {
|
||||
id: "post-1",
|
||||
channel_id: params?.channelId ?? "chan-1",
|
||||
...(params?.rootId ? { root_id: params.rootId } : {}),
|
||||
message: "Choose",
|
||||
props: {
|
||||
attachments: [
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
id: params?.actionId ?? "approve",
|
||||
name: params?.actionName ?? "Approve",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createUnusedInteractionHandler() {
|
||||
return createMattermostInteractionHandler({
|
||||
client: {
|
||||
request: async () => ({ message: "unused" }),
|
||||
} as unknown as MattermostClient,
|
||||
botUserId: "bot",
|
||||
accountId: "acct",
|
||||
});
|
||||
}
|
||||
|
||||
async function runApproveInteraction(params?: {
|
||||
actionName?: string;
|
||||
allowedSourceIps?: string[];
|
||||
|
|
@ -503,8 +601,7 @@ describe("createMattermostInteractionHandler", () => {
|
|||
remoteAddress?: string;
|
||||
headers?: Record<string, string>;
|
||||
}) {
|
||||
const context = { action_id: "approve", __openclaw_channel_id: "chan-1" };
|
||||
const token = generateInteractionToken(context, "acct");
|
||||
const { context, token } = createActionContext();
|
||||
const requestLog: Array<{ path: string; method?: string }> = [];
|
||||
const handler = createMattermostInteractionHandler({
|
||||
client: {
|
||||
|
|
@ -513,15 +610,7 @@ describe("createMattermostInteractionHandler", () => {
|
|||
if (init?.method === "PUT") {
|
||||
return { id: "post-1" };
|
||||
}
|
||||
return {
|
||||
channel_id: "chan-1",
|
||||
message: "Choose",
|
||||
props: {
|
||||
attachments: [
|
||||
{ actions: [{ id: "approve", name: params?.actionName ?? "Approve" }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
return createActionPost({ actionName: params?.actionName });
|
||||
},
|
||||
} as unknown as MattermostClient,
|
||||
botUserId: "bot",
|
||||
|
|
@ -530,50 +619,27 @@ describe("createMattermostInteractionHandler", () => {
|
|||
trustedProxies: params?.trustedProxies,
|
||||
});
|
||||
|
||||
const req = createReq({
|
||||
const res = await runHandler(handler, {
|
||||
remoteAddress: params?.remoteAddress,
|
||||
headers: params?.headers,
|
||||
body: {
|
||||
user_id: "user-1",
|
||||
user_name: "alice",
|
||||
channel_id: "chan-1",
|
||||
post_id: "post-1",
|
||||
context: { ...context, _token: token },
|
||||
},
|
||||
body: createInteractionBody({ context, token, userName: "alice" }),
|
||||
});
|
||||
const res = createRes();
|
||||
await handler(req, res);
|
||||
return { res, requestLog };
|
||||
}
|
||||
|
||||
async function runInvalidActionRequest(actionId: string) {
|
||||
const context = { action_id: "approve", __openclaw_channel_id: "chan-1" };
|
||||
const token = generateInteractionToken(context, "acct");
|
||||
const { context, token } = createActionContext();
|
||||
const handler = createMattermostInteractionHandler({
|
||||
client: {
|
||||
request: async () => ({
|
||||
channel_id: "chan-1",
|
||||
message: "Choose",
|
||||
props: {
|
||||
attachments: [{ actions: [{ id: actionId, name: actionId }] }],
|
||||
},
|
||||
}),
|
||||
request: async () => createActionPost({ actionId, actionName: actionId }),
|
||||
} as unknown as MattermostClient,
|
||||
botUserId: "bot",
|
||||
accountId: "acct",
|
||||
});
|
||||
|
||||
const req = createReq({
|
||||
body: {
|
||||
user_id: "user-1",
|
||||
channel_id: "chan-1",
|
||||
post_id: "post-1",
|
||||
context: { ...context, _token: token },
|
||||
},
|
||||
return await runHandler(handler, {
|
||||
body: createInteractionBody({ context, token }),
|
||||
});
|
||||
const res = createRes();
|
||||
await handler(req, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
it("accepts callback requests from an allowlisted source IP", async () => {
|
||||
|
|
@ -582,12 +648,7 @@ describe("createMattermostInteractionHandler", () => {
|
|||
remoteAddress: "198.51.100.8",
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toBe("{}");
|
||||
expect(requestLog).toEqual([
|
||||
{ path: "/posts/post-1", method: undefined },
|
||||
{ path: "/posts/post-1", method: "PUT" },
|
||||
]);
|
||||
expectSuccessfulApprovalUpdate(res, requestLog);
|
||||
});
|
||||
|
||||
it("accepts forwarded Mattermost source IPs from a trusted proxy", async () => {
|
||||
|
|
@ -603,8 +664,7 @@ describe("createMattermostInteractionHandler", () => {
|
|||
});
|
||||
|
||||
it("rejects callback requests from non-allowlisted source IPs", async () => {
|
||||
const context = { action_id: "approve", __openclaw_channel_id: "chan-1" };
|
||||
const token = generateInteractionToken(context, "acct");
|
||||
const { context, token } = createActionContext();
|
||||
const handler = createMattermostInteractionHandler({
|
||||
client: {
|
||||
request: async () => {
|
||||
|
|
@ -616,33 +676,17 @@ describe("createMattermostInteractionHandler", () => {
|
|||
allowedSourceIps: ["127.0.0.1"],
|
||||
});
|
||||
|
||||
const req = createReq({
|
||||
const res = await runHandler(handler, {
|
||||
remoteAddress: "198.51.100.8",
|
||||
body: {
|
||||
user_id: "user-1",
|
||||
channel_id: "chan-1",
|
||||
post_id: "post-1",
|
||||
context: { ...context, _token: token },
|
||||
},
|
||||
body: createInteractionBody({ context, token }),
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(res.body).toContain("Forbidden origin");
|
||||
expectForbiddenResponse(res, "Forbidden origin");
|
||||
});
|
||||
|
||||
it("rejects requests with an invalid interaction token", async () => {
|
||||
const handler = createMattermostInteractionHandler({
|
||||
client: {
|
||||
request: async () => ({ message: "unused" }),
|
||||
} as unknown as MattermostClient,
|
||||
botUserId: "bot",
|
||||
accountId: "acct",
|
||||
});
|
||||
const handler = createUnusedInteractionHandler();
|
||||
|
||||
const req = createReq({
|
||||
const res = await runHandler(handler, {
|
||||
body: {
|
||||
user_id: "user-1",
|
||||
channel_id: "chan-1",
|
||||
|
|
@ -650,72 +694,33 @@ describe("createMattermostInteractionHandler", () => {
|
|||
context: { action_id: "approve", _token: "deadbeef" },
|
||||
},
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(res.body).toContain("Invalid token");
|
||||
expectForbiddenResponse(res, "Invalid token");
|
||||
});
|
||||
|
||||
it("rejects requests when the signed channel does not match the callback payload", async () => {
|
||||
const context = { action_id: "approve", __openclaw_channel_id: "chan-1" };
|
||||
const token = generateInteractionToken(context, "acct");
|
||||
const handler = createMattermostInteractionHandler({
|
||||
client: {
|
||||
request: async () => ({ message: "unused" }),
|
||||
} as unknown as MattermostClient,
|
||||
botUserId: "bot",
|
||||
accountId: "acct",
|
||||
const { context, token } = createActionContext();
|
||||
const handler = createUnusedInteractionHandler();
|
||||
|
||||
const res = await runHandler(handler, {
|
||||
body: createInteractionBody({ context, token, channelId: "chan-2" }),
|
||||
});
|
||||
|
||||
const req = createReq({
|
||||
body: {
|
||||
user_id: "user-1",
|
||||
channel_id: "chan-2",
|
||||
post_id: "post-1",
|
||||
context: { ...context, _token: token },
|
||||
},
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(res.body).toContain("Channel mismatch");
|
||||
expectForbiddenResponse(res, "Channel mismatch");
|
||||
});
|
||||
|
||||
it("rejects requests when the fetched post does not belong to the callback channel", async () => {
|
||||
const context = { action_id: "approve", __openclaw_channel_id: "chan-1" };
|
||||
const token = generateInteractionToken(context, "acct");
|
||||
const { context, token } = createActionContext();
|
||||
const handler = createMattermostInteractionHandler({
|
||||
client: {
|
||||
request: async () => ({
|
||||
channel_id: "chan-9",
|
||||
message: "Choose",
|
||||
props: {
|
||||
attachments: [{ actions: [{ id: "approve", name: "Approve" }] }],
|
||||
},
|
||||
}),
|
||||
request: async () => createActionPost({ channelId: "chan-9" }),
|
||||
} as unknown as MattermostClient,
|
||||
botUserId: "bot",
|
||||
accountId: "acct",
|
||||
});
|
||||
|
||||
const req = createReq({
|
||||
body: {
|
||||
user_id: "user-1",
|
||||
channel_id: "chan-1",
|
||||
post_id: "post-1",
|
||||
context: { ...context, _token: token },
|
||||
},
|
||||
const res = await runHandler(handler, {
|
||||
body: createInteractionBody({ context, token }),
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(res.body).toContain("Post/channel mismatch");
|
||||
expectForbiddenResponse(res, "Post/channel mismatch");
|
||||
});
|
||||
|
||||
it("rejects requests when the action is not present on the fetched post", async () => {
|
||||
|
|
@ -730,12 +735,7 @@ describe("createMattermostInteractionHandler", () => {
|
|||
actionName: "approve",
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toBe("{}");
|
||||
expect(requestLog).toEqual([
|
||||
{ path: "/posts/post-1", method: undefined },
|
||||
{ path: "/posts/post-1", method: "PUT" },
|
||||
]);
|
||||
expectSuccessfulApprovalUpdate(res, requestLog);
|
||||
});
|
||||
|
||||
it("forwards fetched post threading metadata to session and button callbacks", async () => {
|
||||
|
|
@ -745,19 +745,10 @@ describe("createMattermostInteractionHandler", () => {
|
|||
enqueueSystemEvent,
|
||||
},
|
||||
} as unknown as Parameters<typeof setMattermostRuntime>[0]);
|
||||
const context = { action_id: "approve", __openclaw_channel_id: "chan-1" };
|
||||
const token = generateInteractionToken(context, "acct");
|
||||
const { context, token } = createActionContext();
|
||||
const resolveSessionKey = vi.fn().mockResolvedValue("session:thread:root-9");
|
||||
const dispatchButtonClick = vi.fn();
|
||||
const fetchedPost: MattermostPost = {
|
||||
id: "post-1",
|
||||
channel_id: "chan-1",
|
||||
root_id: "root-9",
|
||||
message: "Choose",
|
||||
props: {
|
||||
attachments: [{ actions: [{ id: "approve", name: "Approve" }] }],
|
||||
},
|
||||
};
|
||||
const fetchedPost = createActionPost({ rootId: "root-9" });
|
||||
const handler = createMattermostInteractionHandler({
|
||||
client: {
|
||||
request: async (_path: string, init?: { method?: string }) =>
|
||||
|
|
@ -769,19 +760,9 @@ describe("createMattermostInteractionHandler", () => {
|
|||
dispatchButtonClick,
|
||||
});
|
||||
|
||||
const req = createReq({
|
||||
body: {
|
||||
user_id: "user-1",
|
||||
user_name: "alice",
|
||||
channel_id: "chan-1",
|
||||
post_id: "post-1",
|
||||
context: { ...context, _token: token },
|
||||
},
|
||||
const res = await runHandler(handler, {
|
||||
body: createInteractionBody({ context, token, userName: "alice" }),
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resolveSessionKey).toHaveBeenCalledWith({
|
||||
channelId: "chan-1",
|
||||
|
|
@ -803,8 +784,7 @@ describe("createMattermostInteractionHandler", () => {
|
|||
});
|
||||
|
||||
it("lets a custom interaction handler short-circuit generic completion updates", async () => {
|
||||
const context = { action_id: "mdlprov", __openclaw_channel_id: "chan-1" };
|
||||
const token = generateInteractionToken(context, "acct");
|
||||
const { context, token } = createActionContext("mdlprov");
|
||||
const requestLog: Array<{ path: string; method?: string }> = [];
|
||||
const handleInteraction = vi.fn().mockResolvedValue({
|
||||
ephemeral_text: "Only the original requester can use this picker.",
|
||||
|
|
@ -814,14 +794,10 @@ describe("createMattermostInteractionHandler", () => {
|
|||
client: {
|
||||
request: async (path: string, init?: { method?: string }) => {
|
||||
requestLog.push({ path, method: init?.method });
|
||||
return {
|
||||
id: "post-1",
|
||||
channel_id: "chan-1",
|
||||
message: "Choose",
|
||||
props: {
|
||||
attachments: [{ actions: [{ id: "mdlprov", name: "Browse providers" }] }],
|
||||
},
|
||||
};
|
||||
return createActionPost({
|
||||
actionId: "mdlprov",
|
||||
actionName: "Browse providers",
|
||||
});
|
||||
},
|
||||
} as unknown as MattermostClient,
|
||||
botUserId: "bot",
|
||||
|
|
@ -830,18 +806,14 @@ describe("createMattermostInteractionHandler", () => {
|
|||
dispatchButtonClick,
|
||||
});
|
||||
|
||||
const req = createReq({
|
||||
body: {
|
||||
user_id: "user-2",
|
||||
user_name: "alice",
|
||||
channel_id: "chan-1",
|
||||
post_id: "post-1",
|
||||
context: { ...context, _token: token },
|
||||
},
|
||||
const res = await runHandler(handler, {
|
||||
body: createInteractionBody({
|
||||
context,
|
||||
token,
|
||||
userId: "user-2",
|
||||
userName: "alice",
|
||||
}),
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toBe(
|
||||
|
|
|
|||
Loading…
Reference in New Issue