343 lines
12 KiB
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');
|
|
}
|