168 lines
5.3 KiB
TypeScript
168 lines
5.3 KiB
TypeScript
import { ChannelType } from "discord-api-types/v10";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { NativeCommandSpec } from "../../auto-reply/commands-registry.js";
|
|
import * as dispatcherModule from "../../auto-reply/reply/provider-dispatcher.js";
|
|
import type { OpenClawConfig } from "../../config/config.js";
|
|
import * as pluginCommandsModule from "../../plugins/commands.js";
|
|
import { createDiscordNativeCommand } from "./native-command.js";
|
|
import {
|
|
createMockCommandInteraction,
|
|
type MockCommandInteraction,
|
|
} from "./native-command.test-helpers.js";
|
|
import { createNoopThreadBindingManager } from "./thread-bindings.js";
|
|
|
|
function createInteraction(params?: { userId?: string }): MockCommandInteraction {
|
|
return createMockCommandInteraction({
|
|
userId: params?.userId ?? "123456789012345678",
|
|
username: "discord-user",
|
|
globalName: "Discord User",
|
|
channelType: ChannelType.GuildText,
|
|
channelId: "234567890123456789",
|
|
guildId: "345678901234567890",
|
|
guildName: "Test Guild",
|
|
interactionId: "interaction-1",
|
|
});
|
|
}
|
|
|
|
function createConfig(): OpenClawConfig {
|
|
return {
|
|
commands: {
|
|
allowFrom: {
|
|
discord: ["user:123456789012345678"],
|
|
},
|
|
},
|
|
channels: {
|
|
discord: {
|
|
groupPolicy: "allowlist",
|
|
guilds: {
|
|
"345678901234567890": {
|
|
channels: {
|
|
"234567890123456789": {
|
|
allow: true,
|
|
requireMention: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
}
|
|
|
|
function createCommand(cfg: OpenClawConfig) {
|
|
const commandSpec: NativeCommandSpec = {
|
|
name: "status",
|
|
description: "Status",
|
|
acceptsArgs: false,
|
|
};
|
|
return createDiscordNativeCommand({
|
|
command: commandSpec,
|
|
cfg,
|
|
discordConfig: cfg.channels?.discord ?? {},
|
|
accountId: "default",
|
|
sessionPrefix: "discord:slash",
|
|
ephemeralDefault: true,
|
|
threadBindings: createNoopThreadBindingManager("default"),
|
|
});
|
|
}
|
|
|
|
function createDispatchSpy() {
|
|
return vi.spyOn(dispatcherModule, "dispatchReplyWithDispatcher").mockResolvedValue({
|
|
counts: {
|
|
final: 1,
|
|
block: 0,
|
|
tool: 0,
|
|
},
|
|
} as never);
|
|
}
|
|
|
|
async function runGuildSlashCommand(params?: {
|
|
userId?: string;
|
|
mutateConfig?: (cfg: OpenClawConfig) => void;
|
|
}) {
|
|
const cfg = createConfig();
|
|
params?.mutateConfig?.(cfg);
|
|
const command = createCommand(cfg);
|
|
const interaction = createInteraction({ userId: params?.userId });
|
|
vi.spyOn(pluginCommandsModule, "matchPluginCommand").mockReturnValue(null);
|
|
const dispatchSpy = createDispatchSpy();
|
|
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
|
return { dispatchSpy, interaction };
|
|
}
|
|
|
|
function expectNotUnauthorizedReply(interaction: MockCommandInteraction) {
|
|
expect(interaction.reply).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({ content: "You are not authorized to use this command." }),
|
|
);
|
|
}
|
|
|
|
function expectUnauthorizedReply(interaction: MockCommandInteraction) {
|
|
expect(interaction.reply).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
content: "You are not authorized to use this command.",
|
|
ephemeral: true,
|
|
}),
|
|
);
|
|
}
|
|
|
|
describe("Discord native slash commands with commands.allowFrom", () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("authorizes guild slash commands when commands.allowFrom.discord matches the sender", async () => {
|
|
const { dispatchSpy, interaction } = await runGuildSlashCommand();
|
|
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
|
expectNotUnauthorizedReply(interaction);
|
|
});
|
|
|
|
it("authorizes guild slash commands from the global commands.allowFrom list when provider-specific allowFrom is missing", async () => {
|
|
const { dispatchSpy, interaction } = await runGuildSlashCommand({
|
|
mutateConfig: (cfg) => {
|
|
cfg.commands = {
|
|
allowFrom: {
|
|
"*": ["user:123456789012345678"],
|
|
},
|
|
};
|
|
},
|
|
});
|
|
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
|
expectNotUnauthorizedReply(interaction);
|
|
});
|
|
|
|
it("authorizes guild slash commands when commands.useAccessGroups is false and commands.allowFrom.discord matches the sender", async () => {
|
|
const { dispatchSpy, interaction } = await runGuildSlashCommand({
|
|
mutateConfig: (cfg) => {
|
|
cfg.commands = {
|
|
...cfg.commands,
|
|
useAccessGroups: false,
|
|
};
|
|
},
|
|
});
|
|
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
|
expectNotUnauthorizedReply(interaction);
|
|
});
|
|
|
|
it("rejects guild slash commands when commands.allowFrom.discord does not match the sender", async () => {
|
|
const { dispatchSpy, interaction } = await runGuildSlashCommand({
|
|
userId: "999999999999999999",
|
|
});
|
|
expect(dispatchSpy).not.toHaveBeenCalled();
|
|
expectUnauthorizedReply(interaction);
|
|
});
|
|
|
|
it("rejects guild slash commands when commands.useAccessGroups is false and commands.allowFrom.discord does not match the sender", async () => {
|
|
const { dispatchSpy, interaction } = await runGuildSlashCommand({
|
|
userId: "999999999999999999",
|
|
mutateConfig: (cfg) => {
|
|
cfg.commands = {
|
|
...cfg.commands,
|
|
useAccessGroups: false,
|
|
};
|
|
},
|
|
});
|
|
expect(dispatchSpy).not.toHaveBeenCalled();
|
|
expectUnauthorizedReply(interaction);
|
|
});
|
|
});
|