refactor: share mattermost test harnesses

This commit is contained in:
Peter Steinberger 2026-03-13 20:27:04 +00:00
parent 48853f875b
commit ba2d57d024
4 changed files with 96 additions and 118 deletions

View File

@ -16,6 +16,35 @@ const accountFixture: ResolvedMattermostAccount = {
config: {}, config: {},
}; };
function authorizeGroupCommand(senderId: string) {
return authorizeMattermostCommandInvocation({
account: {
...accountFixture,
config: {
groupPolicy: "allowlist",
allowFrom: ["trusted-user"],
},
},
cfg: {
commands: {
useAccessGroups: true,
},
},
senderId,
senderName: senderId,
channelId: "chan-1",
channelInfo: {
id: "chan-1",
type: "O",
name: "general",
display_name: "General",
},
storeAllowFrom: [],
allowTextCommands: true,
hasControlCommand: true,
});
}
describe("mattermost monitor authz", () => { describe("mattermost monitor authz", () => {
it("keeps DM allowlist merged with pairing-store entries", () => { it("keeps DM allowlist merged with pairing-store entries", () => {
const resolved = resolveMattermostEffectiveAllowFromLists({ const resolved = resolveMattermostEffectiveAllowFromLists({
@ -72,32 +101,7 @@ describe("mattermost monitor authz", () => {
}); });
it("denies group control commands when the sender is outside the allowlist", () => { it("denies group control commands when the sender is outside the allowlist", () => {
const decision = authorizeMattermostCommandInvocation({ const decision = authorizeGroupCommand("attacker");
account: {
...accountFixture,
config: {
groupPolicy: "allowlist",
allowFrom: ["trusted-user"],
},
},
cfg: {
commands: {
useAccessGroups: true,
},
},
senderId: "attacker",
senderName: "attacker",
channelId: "chan-1",
channelInfo: {
id: "chan-1",
type: "O",
name: "general",
display_name: "General",
},
storeAllowFrom: [],
allowTextCommands: true,
hasControlCommand: true,
});
expect(decision).toMatchObject({ expect(decision).toMatchObject({
ok: false, ok: false,
@ -107,32 +111,7 @@ describe("mattermost monitor authz", () => {
}); });
it("authorizes group control commands for allowlisted senders", () => { it("authorizes group control commands for allowlisted senders", () => {
const decision = authorizeMattermostCommandInvocation({ const decision = authorizeGroupCommand("trusted-user");
account: {
...accountFixture,
config: {
groupPolicy: "allowlist",
allowFrom: ["trusted-user"],
},
},
cfg: {
commands: {
useAccessGroups: true,
},
},
senderId: "trusted-user",
senderName: "trusted-user",
channelId: "chan-1",
channelInfo: {
id: "chan-1",
type: "O",
name: "general",
display_name: "General",
},
storeAllowFrom: [],
allowTextCommands: true,
hasControlCommand: true,
});
expect(decision).toMatchObject({ expect(decision).toMatchObject({
ok: true, ok: true,

View File

@ -14,6 +14,28 @@ describe("mattermost reactions", () => {
resetMattermostReactionBotUserCacheForTests(); resetMattermostReactionBotUserCacheForTests();
}); });
async function addReactionWithFetch(
fetchMock: ReturnType<typeof createMattermostReactionFetchMock>,
) {
return addMattermostReaction({
cfg: createMattermostTestConfig(),
postId: "POST1",
emojiName: "thumbsup",
fetchImpl: fetchMock as unknown as typeof fetch,
});
}
async function removeReactionWithFetch(
fetchMock: ReturnType<typeof createMattermostReactionFetchMock>,
) {
return removeMattermostReaction({
cfg: createMattermostTestConfig(),
postId: "POST1",
emojiName: "thumbsup",
fetchImpl: fetchMock as unknown as typeof fetch,
});
}
it("adds reactions by calling /users/me then POST /reactions", async () => { it("adds reactions by calling /users/me then POST /reactions", async () => {
const fetchMock = createMattermostReactionFetchMock({ const fetchMock = createMattermostReactionFetchMock({
mode: "add", mode: "add",
@ -21,12 +43,7 @@ describe("mattermost reactions", () => {
emojiName: "thumbsup", emojiName: "thumbsup",
}); });
const result = await addMattermostReaction({ const result = await addReactionWithFetch(fetchMock);
cfg: createMattermostTestConfig(),
postId: "POST1",
emojiName: "thumbsup",
fetchImpl: fetchMock as unknown as typeof fetch,
});
expect(result).toEqual({ ok: true }); expect(result).toEqual({ ok: true });
expect(fetchMock).toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalled();
@ -41,12 +58,7 @@ describe("mattermost reactions", () => {
body: { id: "err", message: "boom" }, body: { id: "err", message: "boom" },
}); });
const result = await addMattermostReaction({ const result = await addReactionWithFetch(fetchMock);
cfg: createMattermostTestConfig(),
postId: "POST1",
emojiName: "thumbsup",
fetchImpl: fetchMock as unknown as typeof fetch,
});
expect(result.ok).toBe(false); expect(result.ok).toBe(false);
if (!result.ok) { if (!result.ok) {
@ -61,12 +73,7 @@ describe("mattermost reactions", () => {
emojiName: "thumbsup", emojiName: "thumbsup",
}); });
const result = await removeMattermostReaction({ const result = await removeReactionWithFetch(fetchMock);
cfg: createMattermostTestConfig(),
postId: "POST1",
emojiName: "thumbsup",
fetchImpl: fetchMock as unknown as typeof fetch,
});
expect(result).toEqual({ ok: true }); expect(result).toEqual({ ok: true });
expect(fetchMock).toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalled();

View File

@ -10,6 +10,25 @@ import {
} from "./slash-commands.js"; } from "./slash-commands.js";
describe("slash-commands", () => { describe("slash-commands", () => {
async function registerSingleStatusCommand(
request: (path: string, init?: { method?: string }) => Promise<unknown>,
) {
const client = { request } as unknown as MattermostClient;
return registerSlashCommands({
client,
teamId: "team-1",
creatorUserId: "bot-user",
callbackUrl: "http://gateway/callback",
commands: [
{
trigger: "oc_status",
description: "status",
autoComplete: true,
},
],
});
}
it("parses application/x-www-form-urlencoded payloads", () => { it("parses application/x-www-form-urlencoded payloads", () => {
const payload = parseSlashCommandPayload( const payload = parseSlashCommandPayload(
"token=t1&team_id=team&channel_id=ch1&user_id=u1&command=%2Foc_status&text=now", "token=t1&team_id=team&channel_id=ch1&user_id=u1&command=%2Foc_status&text=now",
@ -101,21 +120,7 @@ describe("slash-commands", () => {
} }
throw new Error(`unexpected request path: ${path}`); throw new Error(`unexpected request path: ${path}`);
}); });
const client = { request } as unknown as MattermostClient; const result = await registerSingleStatusCommand(request);
const result = await registerSlashCommands({
client,
teamId: "team-1",
creatorUserId: "bot-user",
callbackUrl: "http://gateway/callback",
commands: [
{
trigger: "oc_status",
description: "status",
autoComplete: true,
},
],
});
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0]?.managed).toBe(false); expect(result[0]?.managed).toBe(false);
@ -144,21 +149,7 @@ describe("slash-commands", () => {
} }
throw new Error(`unexpected request path: ${path}`); throw new Error(`unexpected request path: ${path}`);
}); });
const client = { request } as unknown as MattermostClient; const result = await registerSingleStatusCommand(request);
const result = await registerSlashCommands({
client,
teamId: "team-1",
creatorUserId: "bot-user",
callbackUrl: "http://gateway/callback",
commands: [
{
trigger: "oc_status",
description: "status",
autoComplete: true,
},
],
});
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledTimes(1);

View File

@ -58,6 +58,23 @@ const accountFixture: ResolvedMattermostAccount = {
config: {}, config: {},
}; };
async function runSlashRequest(params: {
commandTokens: Set<string>;
body: string;
method?: string;
}) {
const handler = createSlashCommandHttpHandler({
account: accountFixture,
cfg: {} as OpenClawConfig,
runtime: {} as RuntimeEnv,
commandTokens: params.commandTokens,
});
const req = createRequest({ method: params.method, body: params.body });
const response = createResponse();
await handler(req, response.res);
return response;
}
describe("slash-http", () => { describe("slash-http", () => {
it("rejects non-POST methods", async () => { it("rejects non-POST methods", async () => {
const handler = createSlashCommandHttpHandler({ const handler = createSlashCommandHttpHandler({
@ -93,36 +110,20 @@ describe("slash-http", () => {
}); });
it("fails closed when no command tokens are registered", async () => { it("fails closed when no command tokens are registered", async () => {
const handler = createSlashCommandHttpHandler({ const response = await runSlashRequest({
account: accountFixture,
cfg: {} as OpenClawConfig,
runtime: {} as RuntimeEnv,
commandTokens: new Set<string>(), commandTokens: new Set<string>(),
});
const req = createRequest({
body: "token=tok1&team_id=t1&channel_id=c1&user_id=u1&command=%2Foc_status&text=", body: "token=tok1&team_id=t1&channel_id=c1&user_id=u1&command=%2Foc_status&text=",
}); });
const response = createResponse();
await handler(req, response.res);
expect(response.res.statusCode).toBe(401); expect(response.res.statusCode).toBe(401);
expect(response.getBody()).toContain("Unauthorized: invalid command token."); expect(response.getBody()).toContain("Unauthorized: invalid command token.");
}); });
it("rejects unknown command tokens", async () => { it("rejects unknown command tokens", async () => {
const handler = createSlashCommandHttpHandler({ const response = await runSlashRequest({
account: accountFixture,
cfg: {} as OpenClawConfig,
runtime: {} as RuntimeEnv,
commandTokens: new Set(["known-token"]), commandTokens: new Set(["known-token"]),
});
const req = createRequest({
body: "token=unknown&team_id=t1&channel_id=c1&user_id=u1&command=%2Foc_status&text=", body: "token=unknown&team_id=t1&channel_id=c1&user_id=u1&command=%2Foc_status&text=",
}); });
const response = createResponse();
await handler(req, response.res);
expect(response.res.statusCode).toBe(401); expect(response.res.statusCode).toBe(401);
expect(response.getBody()).toContain("Unauthorized: invalid command token."); expect(response.getBody()).toContain("Unauthorized: invalid command token.");