mumbullet/test/audio/downloader_test.dart

343 lines
12 KiB
Dart

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 = <Song>[];
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');
}