Initial commit
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
const axios = require('axios');
|
||||
const { createMockInteraction } = require('../utils/testUtils');
|
||||
|
||||
// Mock axios
|
||||
jest.mock('axios');
|
||||
|
||||
// Mock the discord.js module
|
||||
jest.mock('discord.js', () => ({
|
||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
addStringOption: jest.fn().mockImplementation(callback => {
|
||||
const option = {
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
setRequired: jest.fn().mockReturnThis(),
|
||||
};
|
||||
callback(option);
|
||||
return {
|
||||
addBooleanOption: jest.fn().mockImplementation(boolCallback => {
|
||||
const boolOption = {
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
setRequired: jest.fn().mockReturnThis(),
|
||||
};
|
||||
boolCallback(boolOption);
|
||||
return {
|
||||
toJSON: jest.fn().mockReturnValue({
|
||||
name: 'ask',
|
||||
description: 'Ask a question to the AI',
|
||||
options: [
|
||||
{
|
||||
name: 'prompt',
|
||||
description: 'Your question or prompt',
|
||||
type: 3,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'websearch',
|
||||
description:
|
||||
'Enable web search for more up-to-date information',
|
||||
type: 5,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
toJSON: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const askCommand = require('../../commands/ask');
|
||||
|
||||
describe('Ask Command', () => {
|
||||
describe('Command Structure', () => {
|
||||
it('should have correct name and description', () => {
|
||||
const commandData = askCommand.data.toJSON();
|
||||
expect(commandData.name).toBe('ask');
|
||||
expect(commandData.description).toBe('Ask a question to the AI');
|
||||
});
|
||||
|
||||
it('should have required command properties', () => {
|
||||
expect(askCommand).toHaveProperty('data');
|
||||
expect(askCommand).toHaveProperty('execute');
|
||||
expect(typeof askCommand.execute).toBe('function');
|
||||
});
|
||||
|
||||
it('should have correct option configuration', () => {
|
||||
const commandData = askCommand.data.toJSON();
|
||||
const [promptOption, websearchOption] = commandData.options;
|
||||
|
||||
expect(promptOption.name).toBe('prompt');
|
||||
expect(promptOption.description).toBe('Your question or prompt');
|
||||
expect(promptOption.required).toBe(true);
|
||||
|
||||
expect(websearchOption.name).toBe('websearch');
|
||||
expect(websearchOption.description).toBe(
|
||||
'Enable web search for more up-to-date information'
|
||||
);
|
||||
expect(websearchOption.required).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command Execution', () => {
|
||||
let interaction;
|
||||
const mockPrompt = 'What is the meaning of life?';
|
||||
const mockApiResponse = {
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: 'The meaning of life is 42.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.OPENROUTER_API_KEY = 'test-api-key';
|
||||
axios.post.mockReset();
|
||||
jest.clearAllMocks();
|
||||
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle successful API response with websearch disabled', async () => {
|
||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
expect.objectContaining({
|
||||
model: 'google/gemini-2.0-flash-exp:free',
|
||||
plugins: undefined,
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
content: expect.not.stringContaining('Web search enabled'),
|
||||
split: false,
|
||||
allowedMentions: { parse: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle successful API response with websearch enabled', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: true,
|
||||
},
|
||||
});
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
expect.objectContaining({
|
||||
model: 'google/gemini-2.0-flash-exp:free:online',
|
||||
plugins: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'web',
|
||||
max_results: 3,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
content: expect.stringContaining('Web search enabled'),
|
||||
split: false,
|
||||
allowedMentions: { parse: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default websearch value when not provided', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
// Not providing websearch option
|
||||
});
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
expect.objectContaining({
|
||||
model: 'google/gemini-2.0-flash-exp:free',
|
||||
plugins: undefined,
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle long responses with proper chunking and websearch enabled', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: true,
|
||||
},
|
||||
});
|
||||
|
||||
const longResponse = 'A'.repeat(2500);
|
||||
axios.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: longResponse,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledTimes(2);
|
||||
expect(interaction.followUp.mock.calls[0][0].content).toContain(
|
||||
'Web search enabled'
|
||||
);
|
||||
expect(interaction.followUp.mock.calls[1][0].content).toContain(
|
||||
'(continued)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle code blocks in chunked responses with websearch', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: true,
|
||||
},
|
||||
});
|
||||
|
||||
const responseWithCodeBlock =
|
||||
"Here's a code example:\n```python\nprint('hello')\n```".repeat(20);
|
||||
axios.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: responseWithCodeBlock,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
|
||||
// Verify that code blocks are not split in the middle
|
||||
interaction.followUp.mock.calls.forEach(call => {
|
||||
const { content } = call[0];
|
||||
const openBlocks = (content.match(/```/g) || []).length;
|
||||
expect(openBlocks % 2).toBe(0); // Should always be even
|
||||
});
|
||||
|
||||
// First chunk should contain websearch indicator
|
||||
expect(interaction.followUp.mock.calls[0][0].content).toContain(
|
||||
'Web search enabled'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle API errors with websearch enabled', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: true,
|
||||
},
|
||||
});
|
||||
|
||||
const error = new Error('API Error');
|
||||
error.response = { data: 'API Error Details' };
|
||||
axios.post.mockRejectedValueOnce(error);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
content: 'Sorry, there was an error processing your request.',
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should send API request with correct headers regardless of websearch', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: true,
|
||||
},
|
||||
});
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(Object),
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer test-api-key',
|
||||
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle defer reply failure with websearch', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: true,
|
||||
},
|
||||
deferFails: true,
|
||||
});
|
||||
|
||||
await expect(askCommand.execute(interaction)).rejects.toThrow(
|
||||
'Failed to defer reply'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle follow up failure with websearch', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: true,
|
||||
},
|
||||
followUpFails: true,
|
||||
});
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
await expect(askCommand.execute(interaction)).rejects.toThrow(
|
||||
'Failed to follow up'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
const { createMockInteraction } = require('../utils/testUtils');
|
||||
|
||||
// Mock the discord.js module
|
||||
jest.mock('discord.js', () => ({
|
||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
toJSON: jest.fn().mockReturnValue({
|
||||
name: 'help',
|
||||
description: 'Lists all available commands',
|
||||
}),
|
||||
}),
|
||||
EmbedBuilder: jest.fn().mockReturnValue({
|
||||
setColor: jest.fn().mockReturnThis(),
|
||||
setTitle: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
setTimestamp: jest.fn().mockReturnThis(),
|
||||
addFields: jest.fn().mockReturnThis(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const helpCommand = require('../../commands/help');
|
||||
|
||||
describe('Help Command', () => {
|
||||
describe('Command Structure', () => {
|
||||
it('should have correct name and description', () => {
|
||||
const commandData = helpCommand.data.toJSON();
|
||||
expect(commandData.name).toBe('help');
|
||||
expect(commandData.description).toBe('Lists all available commands');
|
||||
});
|
||||
|
||||
it('should have required command properties', () => {
|
||||
expect(helpCommand).toHaveProperty('data');
|
||||
expect(helpCommand).toHaveProperty('execute');
|
||||
expect(typeof helpCommand.execute).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command Execution', () => {
|
||||
let interaction;
|
||||
const mockCommands = new Map([
|
||||
['ping', { data: { name: 'ping', description: 'Replies with Pong!' } }],
|
||||
[
|
||||
'help',
|
||||
{ data: { name: 'help', description: 'Lists all available commands' } },
|
||||
],
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'help',
|
||||
});
|
||||
interaction.client.commands = mockCommands;
|
||||
});
|
||||
|
||||
it('should create an embed with all commands', async () => {
|
||||
await helpCommand.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
embeds: expect.arrayContaining([expect.any(Object)]),
|
||||
ephemeral: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty commands collection', async () => {
|
||||
interaction.client.commands = new Map();
|
||||
await helpCommand.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
embeds: expect.arrayContaining([expect.any(Object)]),
|
||||
ephemeral: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle interaction reply failure', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'help',
|
||||
replyFails: true,
|
||||
});
|
||||
interaction.client.commands = mockCommands;
|
||||
|
||||
await expect(helpCommand.execute(interaction)).rejects.toThrow(
|
||||
'Failed to reply',
|
||||
);
|
||||
});
|
||||
|
||||
it('should create embed with correct structure', async () => {
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
await helpCommand.execute(interaction);
|
||||
|
||||
const embedInstance = EmbedBuilder.mock.results[0].value;
|
||||
|
||||
expect(embedInstance.setColor).toHaveBeenCalledWith('#5dc67b');
|
||||
expect(embedInstance.setTitle).toHaveBeenCalledWith('Available Commands');
|
||||
expect(embedInstance.setDescription).toHaveBeenCalledWith(
|
||||
'Here are all my commands:',
|
||||
);
|
||||
expect(embedInstance.setTimestamp).toHaveBeenCalled();
|
||||
expect(embedInstance.addFields).toHaveBeenCalledTimes(mockCommands.size);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
const { createMockInteraction } = require('../utils/testUtils');
|
||||
|
||||
// Mock the discord.js module
|
||||
jest.mock('discord.js', () => ({
|
||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
addUserOption: jest.fn().mockImplementation(callback => {
|
||||
const option = {
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
setRequired: jest.fn().mockReturnThis(),
|
||||
};
|
||||
callback(option);
|
||||
return {
|
||||
addStringOption: jest.fn().mockImplementation(callback => {
|
||||
const option = {
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
};
|
||||
callback(option);
|
||||
return {
|
||||
setDefaultMemberPermissions: jest.fn().mockReturnThis(),
|
||||
toJSON: jest.fn().mockReturnValue({
|
||||
name: 'kick',
|
||||
description: 'Kick a user from the server',
|
||||
options: [
|
||||
{
|
||||
name: 'target',
|
||||
description: 'The user to kick',
|
||||
type: 6,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: 'Reason for kicking',
|
||||
type: 3,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
toJSON: jest.fn(),
|
||||
}),
|
||||
PermissionFlagsBits: {
|
||||
KickMembers: 0x2n,
|
||||
},
|
||||
}));
|
||||
|
||||
const kickCommand = require('../../commands/kick');
|
||||
|
||||
describe('Kick Command', () => {
|
||||
describe('Command Structure', () => {
|
||||
it('should have correct name and description', () => {
|
||||
const commandData = kickCommand.data.toJSON();
|
||||
expect(commandData.name).toBe('kick');
|
||||
expect(commandData.description).toBe('Kick a user from the server');
|
||||
});
|
||||
|
||||
it('should have required command properties', () => {
|
||||
expect(kickCommand).toHaveProperty('data');
|
||||
expect(kickCommand).toHaveProperty('execute');
|
||||
expect(typeof kickCommand.execute).toBe('function');
|
||||
});
|
||||
|
||||
it('should have correct option configuration', () => {
|
||||
const commandData = kickCommand.data.toJSON();
|
||||
const [targetOption, reasonOption] = commandData.options;
|
||||
|
||||
expect(targetOption.name).toBe('target');
|
||||
expect(targetOption.description).toBe('The user to kick');
|
||||
expect(targetOption.required).toBe(true);
|
||||
|
||||
expect(reasonOption.name).toBe('reason');
|
||||
expect(reasonOption.description).toBe('Reason for kicking');
|
||||
expect(reasonOption.required).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command Execution', () => {
|
||||
let interaction;
|
||||
const mockKick = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockKick.mockReset();
|
||||
|
||||
const mockTarget = {
|
||||
kickable: true,
|
||||
kick: mockKick,
|
||||
user: {
|
||||
tag: 'TestUser#1234',
|
||||
},
|
||||
};
|
||||
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'kick',
|
||||
memberOptions: {
|
||||
target: mockTarget,
|
||||
},
|
||||
stringOptions: {
|
||||
reason: 'Test reason',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully kick a user with reason', async () => {
|
||||
await kickCommand.execute(interaction);
|
||||
|
||||
expect(mockKick).toHaveBeenCalledWith('Test reason');
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
content: expect.stringContaining('Successfully kicked TestUser#1234'),
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default reason when none provided', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'kick',
|
||||
memberOptions: {
|
||||
target: {
|
||||
kickable: true,
|
||||
kick: mockKick,
|
||||
user: { tag: 'TestUser#1234' },
|
||||
},
|
||||
},
|
||||
stringOptions: {
|
||||
reason: null,
|
||||
},
|
||||
});
|
||||
|
||||
await kickCommand.execute(interaction);
|
||||
|
||||
expect(mockKick).toHaveBeenCalledWith('No reason provided');
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
content: expect.stringContaining('No reason provided'),
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-existent target', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'kick',
|
||||
memberOptions: {
|
||||
target: null,
|
||||
},
|
||||
});
|
||||
|
||||
await kickCommand.execute(interaction);
|
||||
|
||||
expect(mockKick).not.toHaveBeenCalled();
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
content: 'That user is not in this server!',
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-kickable target', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'kick',
|
||||
memberOptions: {
|
||||
target: {
|
||||
kickable: false,
|
||||
user: { tag: 'Admin#1234' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await kickCommand.execute(interaction);
|
||||
|
||||
expect(mockKick).not.toHaveBeenCalled();
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
content:
|
||||
'I cannot kick this user! They may have higher permissions than me.',
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle kick failure', async () => {
|
||||
const mockTarget = {
|
||||
kickable: true,
|
||||
kick: jest.fn().mockRejectedValueOnce(new Error('Failed to kick user')),
|
||||
user: { tag: 'TestUser#1234' },
|
||||
};
|
||||
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'kick',
|
||||
memberOptions: {
|
||||
target: mockTarget,
|
||||
},
|
||||
});
|
||||
|
||||
await kickCommand.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
content: 'There was an error trying to kick this user!',
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle interaction reply failure', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'kick',
|
||||
memberOptions: {
|
||||
target: {
|
||||
kickable: true,
|
||||
kick: mockKick,
|
||||
user: { tag: 'TestUser#1234' },
|
||||
},
|
||||
},
|
||||
replyFails: true,
|
||||
});
|
||||
|
||||
await expect(kickCommand.execute(interaction)).rejects.toThrow(
|
||||
'Failed to reply',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
const { createMockInteraction } = require('../utils/testUtils');
|
||||
|
||||
// Mock the discord.js module
|
||||
jest.mock('discord.js', () => ({
|
||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
toJSON: jest.fn().mockReturnValue({
|
||||
name: 'ping',
|
||||
description: 'Replies with Pong!',
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
const pingCommand = require('../../commands/ping');
|
||||
|
||||
describe('Ping Command', () => {
|
||||
describe('Command Structure', () => {
|
||||
it('should have correct name and description', () => {
|
||||
const commandData = pingCommand.data.toJSON();
|
||||
expect(commandData.name).toBe('ping');
|
||||
expect(commandData.description).toBe('Replies with Pong!');
|
||||
});
|
||||
|
||||
it('should have required command properties', () => {
|
||||
expect(pingCommand).toHaveProperty('data');
|
||||
expect(pingCommand).toHaveProperty('execute');
|
||||
expect(typeof pingCommand.execute).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command Execution', () => {
|
||||
let interaction;
|
||||
|
||||
beforeEach(() => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ping',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reply with "Pong! 🏓"', async () => {
|
||||
await pingCommand.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toHaveBeenCalledTimes(1);
|
||||
expect(interaction.reply).toHaveBeenCalledWith('Pong! 🏓');
|
||||
});
|
||||
|
||||
it('should handle interaction reply failure', async () => {
|
||||
// Mock a failed reply
|
||||
interaction.reply.mockRejectedValueOnce(new Error('Failed to reply'));
|
||||
|
||||
await expect(pingCommand.execute(interaction)).rejects.toThrow(
|
||||
'Failed to reply',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
const { createMockInteraction } = require('../utils/testUtils');
|
||||
|
||||
// Mock the discord.js module
|
||||
jest.mock('discord.js', () => ({
|
||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
addIntegerOption: jest.fn().mockImplementation(callback => {
|
||||
const option = {
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
setMinValue: jest.fn().mockReturnThis(),
|
||||
setMaxValue: jest.fn().mockReturnThis(),
|
||||
setRequired: jest.fn().mockReturnThis(),
|
||||
};
|
||||
callback(option);
|
||||
return {
|
||||
setDefaultMemberPermissions: jest.fn().mockReturnThis(),
|
||||
toJSON: jest.fn().mockReturnValue({
|
||||
name: 'prune',
|
||||
description: 'Prune up to 99 messages.',
|
||||
options: [
|
||||
{
|
||||
name: 'amount',
|
||||
description: 'Number of messages to prune',
|
||||
type: 4,
|
||||
required: true,
|
||||
min_value: 1,
|
||||
max_value: 99,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}),
|
||||
toJSON: jest.fn(),
|
||||
}),
|
||||
PermissionFlagsBits: {
|
||||
ManageMessages: 0x2000n,
|
||||
},
|
||||
}));
|
||||
|
||||
const pruneCommand = require('../../commands/prune');
|
||||
|
||||
describe('Prune Command', () => {
|
||||
describe('Command Structure', () => {
|
||||
it('should have correct name and description', () => {
|
||||
const commandData = pruneCommand.data.toJSON();
|
||||
expect(commandData.name).toBe('prune');
|
||||
expect(commandData.description).toBe('Prune up to 99 messages.');
|
||||
});
|
||||
|
||||
it('should have required command properties', () => {
|
||||
expect(pruneCommand).toHaveProperty('data');
|
||||
expect(pruneCommand).toHaveProperty('execute');
|
||||
expect(typeof pruneCommand.execute).toBe('function');
|
||||
});
|
||||
|
||||
it('should have correct option configuration', () => {
|
||||
const commandData = pruneCommand.data.toJSON();
|
||||
const option = commandData.options[0];
|
||||
|
||||
expect(option.name).toBe('amount');
|
||||
expect(option.description).toBe('Number of messages to prune');
|
||||
expect(option.required).toBe(true);
|
||||
expect(option.min_value).toBe(1);
|
||||
expect(option.max_value).toBe(99);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command Execution', () => {
|
||||
let interaction;
|
||||
const mockBulkDelete = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockBulkDelete.mockReset();
|
||||
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'prune',
|
||||
integerOptions: {
|
||||
amount: 5,
|
||||
},
|
||||
});
|
||||
interaction.channel.bulkDelete = mockBulkDelete;
|
||||
});
|
||||
|
||||
it('should successfully delete messages', async () => {
|
||||
mockBulkDelete.mockResolvedValueOnce({ size: 5 });
|
||||
|
||||
await pruneCommand.execute(interaction);
|
||||
|
||||
expect(mockBulkDelete).toHaveBeenCalledWith(5, true);
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
content: 'Successfully deleted 5 message(s).',
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle bulk delete failure', async () => {
|
||||
mockBulkDelete.mockRejectedValueOnce(
|
||||
new Error('Failed to delete messages'),
|
||||
);
|
||||
|
||||
await pruneCommand.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
content: 'There was an error trying to prune messages in this channel!',
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle messages older than 14 days', async () => {
|
||||
mockBulkDelete.mockRejectedValueOnce(
|
||||
new Error('Messages older than 14 days cannot be deleted'),
|
||||
);
|
||||
|
||||
await pruneCommand.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
content: 'There was an error trying to prune messages in this channel!',
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle interaction reply failure', async () => {
|
||||
mockBulkDelete.mockResolvedValueOnce({ size: 5 });
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'prune',
|
||||
integerOptions: {
|
||||
amount: 5,
|
||||
},
|
||||
replyFails: true,
|
||||
});
|
||||
interaction.channel.bulkDelete = mockBulkDelete;
|
||||
|
||||
await expect(pruneCommand.execute(interaction)).rejects.toThrow(
|
||||
'Failed to reply',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
// Test utilities for Discord.js bot testing
|
||||
|
||||
/**
|
||||
* Creates a mock interaction object with common properties and methods
|
||||
* @param {Object} options - Customization options for the mock interaction
|
||||
* @returns {Object} Mock interaction object
|
||||
*/
|
||||
const createMockInteraction = (options = {}) => ({
|
||||
commandName: options.commandName || 'test-command',
|
||||
user: {
|
||||
id: options.userId || 'mock-user-id',
|
||||
username: options.username || 'MockUser',
|
||||
tag: options.userTag || 'MockUser#0000',
|
||||
},
|
||||
guild: {
|
||||
id: options.guildId || 'mock-guild-id',
|
||||
name: options.guildName || 'Mock Guild',
|
||||
},
|
||||
channel: {
|
||||
id: options.channelId || 'mock-channel-id',
|
||||
name: options.channelName || 'mock-channel',
|
||||
send: jest.fn().mockResolvedValue({ id: 'mock-message-id' }),
|
||||
bulkDelete: jest.fn().mockResolvedValue({ size: 0 }),
|
||||
},
|
||||
client: {
|
||||
commands: new Map(),
|
||||
user: {
|
||||
id: 'mock-client-id',
|
||||
username: 'MockBot',
|
||||
setActivity: jest.fn(),
|
||||
},
|
||||
},
|
||||
reply: jest.fn().mockImplementation(async _response => {
|
||||
if (options.replyFails) {
|
||||
throw new Error('Failed to reply');
|
||||
}
|
||||
return { id: 'mock-reply-id' };
|
||||
}),
|
||||
deferReply: jest.fn().mockImplementation(async () => {
|
||||
if (options.deferFails) {
|
||||
throw new Error('Failed to defer reply');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
editReply: jest.fn().mockResolvedValue({ id: 'mock-edit-id' }),
|
||||
followUp: jest.fn().mockImplementation(async _response => {
|
||||
if (options.followUpFails) {
|
||||
throw new Error('Failed to follow up');
|
||||
}
|
||||
return { id: 'mock-followup-id' };
|
||||
}),
|
||||
options: {
|
||||
getString: jest.fn(name => options.stringOptions?.[name]),
|
||||
getInteger: jest.fn(name => options.integerOptions?.[name]),
|
||||
getBoolean: jest.fn(name => options.booleanOptions?.[name]),
|
||||
getUser: jest.fn(name => options.userOptions?.[name]),
|
||||
getMember: jest.fn(name => options.memberOptions?.[name]),
|
||||
get: jest.fn(name => options.options?.[name]),
|
||||
},
|
||||
member: {
|
||||
permissions: {
|
||||
has: jest.fn().mockReturnValue(options.hasPermission ?? true),
|
||||
},
|
||||
roles: {
|
||||
cache: new Map(),
|
||||
},
|
||||
},
|
||||
isCommand: () => true,
|
||||
isChatInputCommand: () => true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a mock client object with common properties and methods
|
||||
* @param {Object} options - Customization options for the mock client
|
||||
* @returns {Object} Mock client object
|
||||
*/
|
||||
const createMockClient = (options = {}) => ({
|
||||
user: {
|
||||
id: options.clientId || 'mock-client-id',
|
||||
username: options.clientUsername || 'MockBot',
|
||||
setActivity: jest.fn(),
|
||||
},
|
||||
guilds: {
|
||||
cache: new Map(),
|
||||
},
|
||||
commands: new Map(),
|
||||
on: jest.fn(),
|
||||
once: jest.fn(),
|
||||
login: jest.fn().mockResolvedValue('token'),
|
||||
destroy: jest.fn().mockResolvedValue(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createMockInteraction,
|
||||
createMockClient,
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
const { createMockInteraction, createMockClient } = require('./testUtils');
|
||||
|
||||
describe('Test Utilities', () => {
|
||||
describe('createMockInteraction', () => {
|
||||
it('should create a mock interaction with default values', () => {
|
||||
const interaction = createMockInteraction();
|
||||
|
||||
expect(interaction.commandName).toBe('test-command');
|
||||
expect(interaction.user.id).toBe('mock-user-id');
|
||||
expect(interaction.guild.id).toBe('mock-guild-id');
|
||||
expect(interaction.channel.id).toBe('mock-channel-id');
|
||||
expect(typeof interaction.reply).toBe('function');
|
||||
expect(typeof interaction.deferReply).toBe('function');
|
||||
expect(typeof interaction.editReply).toBe('function');
|
||||
expect(typeof interaction.followUp).toBe('function');
|
||||
});
|
||||
|
||||
it('should create a mock interaction with custom values', () => {
|
||||
const customOptions = {
|
||||
commandName: 'custom-command',
|
||||
userId: 'custom-user-id',
|
||||
username: 'CustomUser',
|
||||
guildId: 'custom-guild-id',
|
||||
channelId: 'custom-channel-id',
|
||||
};
|
||||
|
||||
const interaction = createMockInteraction(customOptions);
|
||||
|
||||
expect(interaction.commandName).toBe('custom-command');
|
||||
expect(interaction.user.id).toBe('custom-user-id');
|
||||
expect(interaction.user.username).toBe('CustomUser');
|
||||
expect(interaction.guild.id).toBe('custom-guild-id');
|
||||
expect(interaction.channel.id).toBe('custom-channel-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMockClient', () => {
|
||||
it('should create a mock client with default values', () => {
|
||||
const client = createMockClient();
|
||||
|
||||
expect(client.user.id).toBe('mock-client-id');
|
||||
expect(client.user.username).toBe('MockBot');
|
||||
expect(typeof client.login).toBe('function');
|
||||
expect(typeof client.destroy).toBe('function');
|
||||
expect(typeof client.on).toBe('function');
|
||||
expect(typeof client.once).toBe('function');
|
||||
});
|
||||
|
||||
it('should create a mock client with custom values', () => {
|
||||
const customOptions = {
|
||||
clientId: 'custom-client-id',
|
||||
clientUsername: 'CustomBot',
|
||||
};
|
||||
|
||||
const client = createMockClient(customOptions);
|
||||
|
||||
expect(client.user.id).toBe('custom-client-id');
|
||||
expect(client.user.username).toBe('CustomBot');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user