UNPKG

@zerospacegg/vynthra

Version:
488 lines (409 loc) 16.5 kB
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)); }); }); });