import 'dart:io'; import 'dart:math'; import 'package:test/test.dart'; import 'package:path/path.dart' as path; import 'package:mumbullet/src/audio/downloader.dart'; import 'package:mumbullet/src/config/config.dart'; import 'package:mumbullet/src/storage/database.dart'; void main() { group('YoutubeDownloader Integration Tests', () { late YoutubeDownloader downloader; late DatabaseManager database; late BotConfig config; late Directory tempDir; late String tempDbPath; 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 tests: yt-dlp not found in PATH'); return; } }); setUp(() async { // Create temporary directory for cache tempDir = await Directory.systemTemp.createTemp('mumbullet_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.001, // 1MB for testing ); // Initialize database database = DatabaseManager(tempDbPath); // Create downloader downloader = YoutubeDownloader(config, database); }); tearDown(() async { // Clean up database.close(); if (tempDir.existsSync()) { await tempDir.delete(recursive: true); } }); group('Valid YouTube URL Downloads', () { test('should download a valid YouTube video', () async { // Using a short, stable YouTube video for testing const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; // "Me at the zoo" - first YouTube video final song = await downloader.download(testUrl); // Verify song object expect(song.url, equals(testUrl)); expect(song.title, isNotEmpty); expect(song.duration, greaterThan(0)); expect(song.filePath, isNotEmpty); expect(song.id, greaterThan(0)); // Verify file exists (check all .wav files in cache directory) final cacheDir = Directory(tempDir.path); final wavFiles = cacheDir.listSync().where((f) => f.path.endsWith('.wav')).toList(); expect(wavFiles.length, greaterThan(0)); // Find the actual downloaded file final actualFile = wavFiles.firstWhere((f) => File(f.path).existsSync()); expect(File(actualFile.path).lengthSync(), greaterThan(0)); // Verify file is in cache directory expect(song.filePath, startsWith(tempDir.path)); expect(song.filePath, endsWith('.wav')); }, timeout: Timeout(Duration(minutes: 2))); test('should use cached version on second download', () async { const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; // First download final stopwatch1 = Stopwatch()..start(); final song1 = await downloader.download(testUrl); stopwatch1.stop(); // Second download (should use cache) final stopwatch2 = Stopwatch()..start(); final song2 = await downloader.download(testUrl); stopwatch2.stop(); // Verify same song returned expect(song1.id, equals(song2.id)); expect(song1.filePath, equals(song2.filePath)); expect(song1.title, equals(song2.title)); // Second call should be much faster (cached) expect(stopwatch2.elapsedMilliseconds, lessThan(stopwatch1.elapsedMilliseconds ~/ 2)); // File should still exist final file = File(song2.filePath); expect(file.existsSync(), isTrue); }, timeout: Timeout(Duration(minutes: 3))); test('should handle different YouTube URL formats', () async { final testUrls = [ 'https://www.youtube.com/watch?v=jNQXAC9IVRw', 'https://youtube.com/watch?v=jNQXAC9IVRw', 'https://youtu.be/jNQXAC9IVRw', 'https://m.youtube.com/watch?v=jNQXAC9IVRw', ]; final songs = []; for (final url in testUrls) { final song = await downloader.download(url); songs.add(song); expect(song.title, isNotEmpty); expect(song.duration, greaterThan(0)); final file = File(song.filePath); expect(file.existsSync(), isTrue); } // All should have the same title (same video) final firstTitle = songs.first.title; for (final song in songs) { expect(song.title, equals(firstTitle)); } }, timeout: Timeout(Duration(minutes: 5))); }); group('Invalid URL Handling', () { test('should throw exception for malformed URLs', () async { final invalidUrls = [ 'not-a-url', 'https://invalid-domain.com/video', 'https://youtube.com/invalid-path', 'https://www.youtube.com/watch?v=', '', ]; for (final url in invalidUrls) { expect( () => downloader.download(url), throwsException, reason: 'Should throw exception for invalid URL: $url', ); } }); test('should throw exception for non-existent YouTube video', () async { // Using a clearly non-existent video ID const invalidUrl = 'https://www.youtube.com/watch?v=ThisVideoDoesNotExist123456789'; expect( () => downloader.download(invalidUrl), throwsException, ); }); test('should throw exception for private/deleted videos', () async { // This video ID is known to be private/deleted const privateUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ_invalid'; expect( () => downloader.download(privateUrl), throwsException, ); }); test('should handle network timeouts gracefully', () async { // Using a URL that will likely timeout or fail const slowUrl = 'https://httpstat.us/200?sleep=30000'; // 30 second delay expect( () => downloader.download(slowUrl), throwsException, ); }, timeout: Timeout(Duration(seconds: 10))); }); group('Cache Management', () { test('should enforce cache size limits', () async { // Set very small cache limit final smallConfig = BotConfig( commandPrefix: '!', defaultPermissionLevel: 1, maxQueueSize: 10, cacheDirectory: tempDir.path, maxCacheSizeGb: 0.000001, // ~1KB - very small ); final smallCacheDownloader = YoutubeDownloader(smallConfig, database); // Download a video (will exceed tiny cache limit) const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; final song = await smallCacheDownloader.download(testUrl); // Verify file was created expect(File(song.filePath).existsSync(), isTrue); // Cache enforcement should have run (no exception thrown) final stats = await smallCacheDownloader.getCacheStats(); expect(stats['songCount'], greaterThanOrEqualTo(0)); }, timeout: Timeout(Duration(minutes: 2))); test('should clear cache completely', () async { const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; // Download a video final song = await downloader.download(testUrl); expect(File(song.filePath).existsSync(), isTrue); // Verify cache has content final statsBefore = await downloader.getCacheStats(); expect(statsBefore['songCount'], greaterThan(0)); // Clear cache await downloader.clearCache(); // Verify cache is empty final statsAfter = await downloader.getCacheStats(); expect(statsAfter['songCount'], equals(0)); expect(statsAfter['totalSizeBytes'], equals(0)); // Verify file was deleted expect(File(song.filePath).existsSync(), isFalse); }, timeout: Timeout(Duration(minutes: 2))); test('should provide accurate cache statistics', () async { // Initially empty final initialStats = await downloader.getCacheStats(); expect(initialStats['songCount'], equals(0)); expect(initialStats['totalSizeBytes'], equals(0)); // Download a video const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; final song = await downloader.download(testUrl); // Check updated stats final updatedStats = await downloader.getCacheStats(); expect(updatedStats['songCount'], equals(1)); expect(updatedStats['totalSizeBytes'], greaterThan(0)); expect(updatedStats['totalSizeMb'], greaterThanOrEqualTo(0)); // Verify file size matches final file = File(song.filePath); expect(updatedStats['totalSizeBytes'], equals(file.lengthSync())); }, timeout: Timeout(Duration(minutes: 2))); test('should handle missing cached files gracefully', () async { const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; // Download a video final song1 = await downloader.download(testUrl); expect(File(song1.filePath).existsSync(), isTrue); // Manually delete the file (simulating external deletion) await File(song1.filePath).delete(); // Try to download again - should re-download final song2 = await downloader.download(testUrl); // Should create new file expect(File(song2.filePath).existsSync(), isTrue); expect(song2.title, equals(song1.title)); // May have different file path due to random naming expect(song2.filePath, isNotEmpty); }, timeout: Timeout(Duration(minutes: 3))); }); group('Error Recovery', () { test('should handle file system permission errors', () async { // Create a read-only cache directory final readOnlyDir = Directory(path.join(tempDir.path, 'readonly')); await readOnlyDir.create(); // Make directory read-only (Unix-like systems) if (!Platform.isWindows) { await Process.run('chmod', ['444', readOnlyDir.path]); } final restrictedConfig = BotConfig( commandPrefix: '!', defaultPermissionLevel: 1, maxQueueSize: 10, cacheDirectory: readOnlyDir.path, maxCacheSizeGb: 1.0, ); final restrictedDownloader = YoutubeDownloader(restrictedConfig, database); const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; // Should handle permission error gracefully if (!Platform.isWindows) { expect( () => restrictedDownloader.download(testUrl), throwsException, ); } // Restore permissions for cleanup if (!Platform.isWindows) { await Process.run('chmod', ['755', readOnlyDir.path]); } }, timeout: Timeout(Duration(minutes: 2))); }); group('Metadata Extraction', () { test('should extract correct metadata from YouTube video', () async { const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; final song = await downloader.download(testUrl); // Verify metadata expect(song.title, isNotEmpty); expect(song.title, isNot(equals('Unknown Title'))); expect(song.duration, greaterThan(0)); expect(song.url, equals(testUrl)); expect(song.addedAt, isNotNull); // Title should be reasonable (not just the URL) expect(song.title.length, greaterThan(5)); expect(song.title, isNot(contains('http'))); }, timeout: Timeout(Duration(minutes: 2))); }); }, skip: Platform.environment['SKIP_INTEGRATION_TESTS'] == 'true'); }