import 'dart:io'; import 'package:test/test.dart'; import 'package:path/path.dart' as path; import 'package:mumbullet/src/audio/downloader.dart'; import 'package:mumbullet/src/command/command_handler.dart'; import 'package:mumbullet/src/command/command_parser.dart'; import 'package:mumbullet/src/config/config.dart'; import 'package:mumbullet/src/queue/music_queue.dart'; import 'package:mumbullet/src/storage/database.dart'; /// Mock command context for testing class MockCommandContext { final String args; final int permissionLevel; final List replies = []; MockCommandContext({ required this.args, this.permissionLevel = 2, }); Future reply(String message) async { replies.add(message); } } void main() { group('Command Handler with Downloader Integration Tests', () { late CommandHandler commandHandler; late YoutubeDownloader downloader; late DatabaseManager database; late BotConfig config; late Directory tempDir; late String tempDbPath; late CommandParser commandParser; late MusicQueue musicQueue; setUpAll(() async { // Check if yt-dlp is available try { final result = await Process.run('yt-dlp', ['--version']); if (result.exitCode != 0) { throw Exception('yt-dlp not available'); } } catch (e) { print('Skipping yt-dlp integration tests: yt-dlp not found in PATH'); return; } }); setUp(() async { // Create temporary directory for cache tempDir = await Directory.systemTemp.createTemp('mumbullet_cmd_test_'); // Create temporary database tempDbPath = path.join(tempDir.path, 'test.db'); // Create test configuration config = BotConfig( commandPrefix: '!', defaultPermissionLevel: 1, maxQueueSize: 10, cacheDirectory: tempDir.path, maxCacheSizeGb: 0.1, // 100MB for testing ); // Initialize components database = DatabaseManager(tempDbPath); downloader = YoutubeDownloader(config, database); commandParser = CommandParser(); musicQueue = MusicQueue(); // Create command handler commandHandler = CommandHandler( commandParser, musicQueue, downloader, config, ); }); tearDown(() async { // Clean up database.close(); if (tempDir.existsSync()) { await tempDir.delete(recursive: true); } }); group('Play Command Integration', () { test('should download and play valid YouTube URL', () async { const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; final context = MockCommandContext(args: testUrl); // Get the play command final playCommand = commandParser.getCommand('play'); expect(playCommand, isNotNull); // Execute play command await playCommand!.execute(context); // Verify responses expect(context.replies.length, equals(2)); expect(context.replies[0], contains('Downloading audio from:')); expect(context.replies[0], contains(testUrl)); expect(context.replies[1], startsWith('Now playing:')); // Verify queue state expect(musicQueue.currentSong, isNotNull); expect(musicQueue.currentSong!.url, equals(testUrl)); // Verify file was downloaded final file = File(musicQueue.currentSong!.filePath); expect(file.existsSync(), isTrue); }, timeout: Timeout(Duration(minutes: 2))); test('should handle HTML anchor tag URLs in play command', () async { const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; const htmlInput = 'YouTube Video'; final context = MockCommandContext(args: htmlInput); final playCommand = commandParser.getCommand('play'); await playCommand!.execute(context); // Should extract URL from HTML and download expect(context.replies.length, equals(2)); expect(context.replies[0], contains(testUrl)); expect(context.replies[1], startsWith('Now playing:')); expect(musicQueue.currentSong, isNotNull); expect(musicQueue.currentSong!.url, equals(testUrl)); }, timeout: Timeout(Duration(minutes: 2))); test('should handle play command with invalid URL', () async { const invalidUrl = 'https://www.youtube.com/watch?v=InvalidVideoId123'; final context = MockCommandContext(args: invalidUrl); final playCommand = commandParser.getCommand('play'); await playCommand!.execute(context); // Should show download attempt and then error expect(context.replies.length, equals(2)); expect(context.replies[0], contains('Downloading audio from:')); expect(context.replies[1], startsWith('Failed to download or play')); // Queue should be empty expect(musicQueue.currentSong, isNull); }, timeout: Timeout(Duration(minutes: 1))); }); group('Queue Command Integration', () { test('should download and queue valid YouTube URL', () async { const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; final context = MockCommandContext(args: testUrl, permissionLevel: 1); final queueCommand = commandParser.getCommand('queue'); expect(queueCommand, isNotNull); await queueCommand!.execute(context); // Verify responses expect(context.replies.length, equals(2)); expect(context.replies[0], contains('Adding to queue:')); expect(context.replies[1], startsWith('Now playing:')); // First song starts playing // Verify queue state expect(musicQueue.currentSong, isNotNull); expect(musicQueue.currentSong!.url, equals(testUrl)); }, timeout: Timeout(Duration(minutes: 2))); test('should queue multiple songs', () async { const testUrls = [ 'https://www.youtube.com/watch?v=jNQXAC9IVRw', 'https://youtu.be/jNQXAC9IVRw', // Same video, different format ]; for (int i = 0; i < testUrls.length; i++) { final context = MockCommandContext(args: testUrls[i], permissionLevel: 1); final queueCommand = commandParser.getCommand('queue'); await queueCommand!.execute(context); if (i == 0) { // First song starts playing expect(context.replies.last, startsWith('Now playing:')); } else { // Subsequent songs are queued (but may use cache) expect(context.replies.last, anyOf( startsWith('Now playing:'), // If cached, might start immediately startsWith('Added to queue at position'), )); } } // Verify queue has songs final queue = musicQueue.getQueue(); expect(queue.length, greaterThanOrEqualTo(1)); }, timeout: Timeout(Duration(minutes: 3))); test('should handle queue command with invalid URL', () async { const invalidUrl = 'not-a-valid-url'; final context = MockCommandContext(args: invalidUrl, permissionLevel: 1); final queueCommand = commandParser.getCommand('queue'); await queueCommand!.execute(context); // Should show error expect(context.replies.length, equals(2)); expect(context.replies[0], contains('Adding to queue:')); expect(context.replies[1], startsWith('Failed to download or queue')); // Queue should be empty expect(musicQueue.getQueue(), isEmpty); }, timeout: Timeout(Duration(minutes: 1))); }); group('Shuffle Command Integration', () { test('should shuffle cached songs', () async { // First, download a few songs to cache const testUrls = [ 'https://www.youtube.com/watch?v=jNQXAC9IVRw', ]; // Download songs to cache for (final url in testUrls) { await downloader.download(url); } // Now test shuffle command final context = MockCommandContext(args: '', permissionLevel: 2); final shuffleCommand = commandParser.getCommand('shuffle'); await shuffleCommand!.execute(context); // Should have shuffled and started playing expect(context.replies.length, equals(1)); expect(context.replies[0], contains('Added')); expect(context.replies[0], contains('shuffled songs')); expect(context.replies[0], contains('Now playing:')); // Queue should have songs final queue = musicQueue.getQueue(); expect(queue.length, greaterThan(0)); expect(musicQueue.currentSong, isNotNull); }, timeout: Timeout(Duration(minutes: 2))); test('should handle shuffle with no cached songs', () async { // Clear any existing cache await downloader.clearCache(); final context = MockCommandContext(args: '', permissionLevel: 2); final shuffleCommand = commandParser.getCommand('shuffle'); await shuffleCommand!.execute(context); // Should indicate no cached songs expect(context.replies.length, equals(1)); expect(context.replies[0], contains('No cached songs found')); }); }); group('URL Format Handling', () { test('should handle various YouTube URL formats in commands', () async { final testCases = [ 'https://www.youtube.com/watch?v=jNQXAC9IVRw', 'https://youtube.com/watch?v=jNQXAC9IVRw', 'https://youtu.be/jNQXAC9IVRw', 'https://m.youtube.com/watch?v=jNQXAC9IVRw', 'Video Link', ]; for (final testUrl in testCases) { // Clear queue for each test musicQueue.clear(); final context = MockCommandContext(args: testUrl); final playCommand = commandParser.getCommand('play'); await playCommand!.execute(context); // Should successfully download and play expect(context.replies.length, equals(2)); expect(context.replies[1], startsWith('Now playing:')); expect(musicQueue.currentSong, isNotNull); // All should result in the same video title (same video) expect(musicQueue.currentSong!.title, isNotEmpty); } }, timeout: Timeout(Duration(minutes: 5))); }); group('Permission Handling', () { test('should respect permission levels for commands', () async { const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; // Test play command with insufficient permissions final lowPermContext = MockCommandContext(args: testUrl, permissionLevel: 1); final playCommand = commandParser.getCommand('play'); // Play command requires level 2, user has level 1 expect(playCommand!.requiredPermissionLevel, equals(2)); // This would normally be handled by the command parser's permission check // but we're testing the command directly, so we'll verify the requirement expect(lowPermContext.permissionLevel, lessThan(playCommand.requiredPermissionLevel)); // Test queue command with sufficient permissions final queueContext = MockCommandContext(args: testUrl, permissionLevel: 1); final queueCommand = commandParser.getCommand('queue'); expect(queueCommand!.requiredPermissionLevel, equals(1)); expect(queueContext.permissionLevel, greaterThanOrEqualTo(queueCommand.requiredPermissionLevel)); await queueCommand.execute(queueContext); expect(queueContext.replies.length, equals(2)); expect(queueContext.replies[1], startsWith('Now playing:')); }, timeout: Timeout(Duration(minutes: 2))); }); }, skip: Platform.environment['SKIP_INTEGRATION_TESTS'] == 'true'); }