@zerospacegg/vynthra
Version:
Discord bot for ZeroSpace.gg data
488 lines (409 loc) • 16.5 kB
text/typescript
import { test } from 'node:test';
import assert from 'node:assert';
import { createSubcommand, createQuerySubcommand, validateSubcommand, validateSubcommands } from '../../src/bot/utils.js';
import type { BotSubcommand } from '../../src/bot/types.js';
import { createMockInteraction } from '../setup-node.js';
import { SlashCommandSubcommandBuilder } from 'discord.js';
import { CommandInteractionOptionResolver } from 'discord.js';
test('Bot Utilities', async (t) => {
await t.test('createSubcommand', async (t) => {
await t.test('should create a valid subcommand with basic options', () => {
const execute = async () => {};
const subcommand = createSubcommand(
'test',
'Test command',
(builder) => builder.addStringOption(option =>
option.setName('input').setDescription('Test input').setRequired(true)
),
execute
);
assert.strictEqual(subcommand.name, 'test');
assert.strictEqual(subcommand.description, 'Test command');
assert.strictEqual(typeof subcommand.builder, 'function');
assert.strictEqual(subcommand.execute, execute);
});
await t.test('should create a builder function that sets name and description', () => {
const mockBuilder = {
setName: () => mockBuilder,
setDescription: () => mockBuilder,
addStringOption: () => mockBuilder,
};
const customBuilder = (builder: any) => {
builder.setName('test');
builder.setDescription('Test description');
return builder;
};
const subcommand = createSubcommand(
'test',
'Test description',
customBuilder,
async () => {}
);
// Call the builder function to test it
const result = subcommand.builder(mockBuilder as any);
assert.strictEqual(result, mockBuilder);
});
await t.test('should preserve custom builder modifications', () => {
const subcommand = createSubcommand(
'complex',
'Complex command',
(builder) => builder
.addStringOption(option => option.setName('first').setDescription('First param'))
.addStringOption(option => option.setName('second').setDescription('Second param')),
async (interaction) => {
await interaction.reply('Complex response');
}
);
assert.strictEqual(subcommand.name, 'complex');
assert.strictEqual(subcommand.description, 'Complex command');
assert.strictEqual(typeof subcommand.builder, 'function');
});
await t.test('should create executable subcommand', async () => {
const mockInteraction = createMockInteraction();
let executeCalled = false;
const execute = async () => {
executeCalled = true;
};
const subcommand = createSubcommand(
'executable',
'Executable command',
(builder) => builder,
execute
);
await subcommand.execute(mockInteraction);
assert.ok(executeCalled);
});
});
await t.test('createQuerySubcommand', async (t) => {
await t.test('should create a query-based subcommand', () => {
const execute = async () => {};
const subcommand = createQuerySubcommand(
'search',
'Search command',
'What to search for',
execute
);
assert.strictEqual(subcommand.name, 'search');
assert.strictEqual(subcommand.description, 'Search command');
assert.strictEqual(typeof subcommand.builder, 'function');
assert.strictEqual(subcommand.execute, execute);
});
await t.test('should create a builder with query string option', () => {
const mockBuilder = {
setName: () => mockBuilder,
setDescription: () => mockBuilder,
addStringOption: (callback: any) => {
const mockStringOption = {
setName: () => mockStringOption,
setDescription: () => mockStringOption,
setRequired: () => mockStringOption,
};
callback(mockStringOption);
return mockBuilder;
},
};
const subcommand = createQuerySubcommand(
'search',
'Search something',
'Search query description',
async () => {}
);
// Call the builder to test the string option setup
const result = subcommand.builder(mockBuilder as any);
assert.strictEqual(result, mockBuilder);
});
await t.test('should execute with query parameter', async () => {
const mockInteraction = createMockInteraction();
mockInteraction.options.getSubcommand = () => 'search';
mockInteraction.options.getString = ((name: string, required?: boolean) => {
if (name === 'query') return 'test search query';
if (required) throw new Error(`Required option ${name} not provided`);
return null;
}) as {
(name: string, required: true): string;
(name: string, required?: boolean): string | null;
};
let executeCalled = false;
const execute = async () => {
executeCalled = true;
};
const subcommand = createQuerySubcommand(
'search',
'Search command',
'Search for something',
execute
);
await subcommand.execute(mockInteraction);
assert.ok(executeCalled);
});
await t.test('should handle different query descriptions', () => {
const subcommand1 = createQuerySubcommand(
'find',
'Find items',
'Item to find',
async () => {}
);
const subcommand2 = createQuerySubcommand(
'lookup',
'Lookup data',
'Data to lookup',
async () => {}
);
assert.strictEqual(subcommand1.name, 'find');
assert.strictEqual(subcommand1.description, 'Find items');
assert.strictEqual(subcommand2.name, 'lookup');
assert.strictEqual(subcommand2.description, 'Lookup data');
});
});
await t.test('validateSubcommand', async (t) => {
await t.test('should validate a correct subcommand', () => {
const validSubcommand: BotSubcommand = {
name: 'valid',
description: 'Valid subcommand',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
assert.doesNotThrow(() => validateSubcommand(validSubcommand));
});
await t.test('should throw for missing name', () => {
const invalidSubcommand = {
name: '',
description: 'Valid description',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
assert.throws(() => validateSubcommand(invalidSubcommand), /valid name/);
});
await t.test('should throw for non-string name', () => {
const invalidSubcommand = {
name: 123 as any,
description: 'Valid description',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
assert.throws(() => validateSubcommand(invalidSubcommand), /valid name/);
});
await t.test('should throw for missing description', () => {
const invalidSubcommand = {
name: 'valid',
description: '',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
assert.throws(() => validateSubcommand(invalidSubcommand), /valid description/);
});
await t.test('should throw for non-string description', () => {
const invalidSubcommand = {
name: 'valid',
description: 123 as any,
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
assert.throws(() => validateSubcommand(invalidSubcommand), /valid description/);
});
await t.test('should throw for missing builder', () => {
const invalidSubcommand = {
name: 'valid',
description: 'Valid description',
builder: null,
execute: async () => {},
};
assert.throws(() => validateSubcommand(invalidSubcommand as any), /builder function/);
});
await t.test('should throw for non-function builder', () => {
const invalidSubcommand = {
name: 'valid',
description: 'Valid description',
builder: 'not a function',
execute: async () => {},
};
assert.throws(() => validateSubcommand(invalidSubcommand as any), /builder function/);
});
await t.test('should throw for missing execute', () => {
const invalidSubcommand = {
name: 'valid',
description: 'Valid description',
builder: () => new SlashCommandSubcommandBuilder(),
execute: null,
};
assert.throws(() => validateSubcommand(invalidSubcommand as any), /execute function/);
});
await t.test('should throw for non-function execute', () => {
const invalidSubcommand = {
name: 'valid',
description: 'Valid description',
builder: () => new SlashCommandSubcommandBuilder(),
execute: 'not a function',
};
assert.throws(() => validateSubcommand(invalidSubcommand as any), /execute function/);
});
await t.test('should validate command name format', () => {
const validNames = ['test', 'test-command', 'test_command', 'test123', 'a', 'z'.repeat(32)];
for (const name of validNames) {
const subcommand = {
name,
description: 'Valid description',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
assert.doesNotThrow(() => validateSubcommand(subcommand), `Name "${name}" should be valid`);
}
});
await t.test('should reject invalid command name formats', () => {
const invalidNames = [
'Test', // Uppercase
'test command', // Space
'test!', // Special character
'test@command', // Special character
'test.command', // Period
'test/command', // Slash
'1test', // Starting with number
'a'.repeat(33), // Too long (33 chars)
];
for (const name of invalidNames) {
const subcommand = {
name,
description: 'Valid description',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
assert.throws(() => validateSubcommand(subcommand), `Name "${name}" should be invalid`);
}
});
await t.test('should validate description length', () => {
const validDescription = 'a'.repeat(100); // Exactly 100 chars
const invalidDescription = 'a'.repeat(101); // 101 chars
const validSubcommand = {
name: 'valid',
description: validDescription,
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
const invalidSubcommand = {
name: 'valid',
description: invalidDescription,
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
assert.doesNotThrow(() => validateSubcommand(validSubcommand));
assert.throws(() => validateSubcommand(invalidSubcommand), /100 characters/);
});
});
await t.test('validateSubcommands', async (t) => {
await t.test('should validate empty array', () => {
assert.doesNotThrow(() => validateSubcommands([]));
});
await t.test('should validate array with single valid subcommand', () => {
const subcommands = [
createSubcommand('test', 'Test command', (sc) => sc, async () => {}),
];
assert.doesNotThrow(() => validateSubcommands(subcommands));
});
await t.test('should validate array with multiple valid subcommands', () => {
const subcommands = [
createSubcommand('test1', 'Test command 1', (sc) => sc, async () => {}),
createSubcommand('test2', 'Test command 2', (sc) => sc, async () => {}),
createSubcommand('test3', 'Test command 3', (sc) => sc, async () => {}),
];
assert.doesNotThrow(() => validateSubcommands(subcommands));
});
await t.test('should reject array with duplicate names', () => {
const subcommands = [
createSubcommand('test', 'Test command 1', (sc) => sc, async () => {}),
createSubcommand('different', 'Different command', (sc) => sc, async () => {}),
createSubcommand('test', 'Test command 2', (sc) => sc, async () => {}),
];
assert.throws(() => validateSubcommands(subcommands), /Duplicate subcommand name: test/);
});
await t.test('should reject array with invalid subcommand', () => {
const validSubcommand = createSubcommand('valid', 'Valid command', (sc) => sc, async () => {});
const invalidSubcommand = {
name: '',
description: 'Invalid command',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
const subcommands = [validSubcommand, invalidSubcommand as any];
assert.throws(() => validateSubcommands(subcommands));
});
await t.test('should catch all validation errors in order', () => {
const invalidSubcommand1 = {
name: '',
description: 'First invalid',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
const invalidSubcommand2 = {
name: 'valid',
description: '',
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
// Should throw on first invalid subcommand
assert.throws(() => validateSubcommands([invalidSubcommand1 as any, invalidSubcommand2 as any]), /valid name/);
});
await t.test('should handle case-sensitive duplicate detection', () => {
const subcommands = [
createSubcommand('test', 'Test command 1', (sc) => sc, async () => {}),
createSubcommand('Test', 'Test command 2', (sc) => sc, async () => {}), // Different case, but Test is invalid anyway
];
// Should fail on invalid name format before duplicate check
assert.throws(() => validateSubcommands(subcommands));
});
await t.test('should validate each subcommand independently', () => {
const subcommand1 = createSubcommand('valid1', 'Valid command 1', (sc) => sc, async () => {});
const subcommand2 = createSubcommand('valid2', 'Valid command 2', (sc) => sc, async () => {});
const subcommand3 = {
name: 'valid3',
description: 'a'.repeat(101), // Too long description
builder: () => new SlashCommandSubcommandBuilder(),
execute: async () => {},
};
const subcommands = [subcommand1, subcommand2, subcommand3 as any];
assert.throws(() => validateSubcommands(subcommands), /100 characters/);
});
});
await t.test('Integration Tests', async (t) => {
await t.test('should create and validate subcommand from createSubcommand', () => {
const subcommand = createSubcommand(
'integration',
'Integration test command',
(builder) => builder.addStringOption(option =>
option.setName('param').setDescription('A parameter')
),
async (interaction) => {
await interaction.reply('Integration test response');
}
);
assert.doesNotThrow(() => validateSubcommand(subcommand));
});
await t.test('should create and validate subcommand from createQuerySubcommand', () => {
const subcommand = createQuerySubcommand(
'query-integration',
'Query integration test',
'Query parameter description',
async (interaction) => {
const query = interaction.options.getString('query', true);
await interaction.reply(`Query: ${query}`);
}
);
assert.doesNotThrow(() => validateSubcommand(subcommand));
});
await t.test('should validate array of mixed subcommand types', () => {
const regularSubcommand = createSubcommand(
'regular',
'Regular command',
(sc) => sc,
async () => {}
);
const querySubcommand = createQuerySubcommand(
'query',
'Query command',
'Query description',
async () => {}
);
const subcommands = [regularSubcommand, querySubcommand];
assert.doesNotThrow(() => validateSubcommands(subcommands));
});
});
});