mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 22:38:50 +05:00
Integration Testing Infrastructure: - Add real magnet link test using Sintel (Creative Commons licensed film) - Create comprehensive torrent integration tests that work in GitHub Actions - Add CI environment detection and validation tests - Enable integration test execution in GitHub Actions workflow Sintel Integration Test (test/integration/torrent_integration_test.dart): - Uses official Sintel magnet link from Blender Foundation - Tests real magnet link parsing and validation - Covers all torrent operations: add, pause, resume, remove - Tests file priority management and video file detection - Includes performance tests and timeout handling - Validates torrent hash extraction and state management - Works with mock platform channel (no real downloads) CI Environment Test (test/integration/ci_environment_test.dart): - Detects GitHub Actions and CI environments - Validates Dart/Flutter environment in CI - Tests network connectivity gracefully - Verifies test infrastructure availability GitHub Actions Integration: - Add integration test step to test.yml workflow - Set CI and GITHUB_ACTIONS environment variables - Use --reporter=expanded for detailed test output - Run after unit tests but before coverage upload Key Features: - Mock platform channel prevents real downloads - Works on any platform (Linux/macOS/Windows) - Fast execution suitable for CI pipelines - Uses only open source, legally free content - Comprehensive error handling and timeouts - Environment-aware test configuration Documentation: - Detailed README for integration tests - Troubleshooting guide for CI issues - Explanation of mock vs real testing approach - Security and licensing considerations This enables thorough testing of torrent functionality in GitHub Actions while respecting copyright and maintaining fast CI execution times.
346 lines
14 KiB
Dart
346 lines
14 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:neomovies_mobile/data/models/torrent_info.dart';
|
||
import 'package:neomovies_mobile/data/services/torrent_platform_service.dart';
|
||
|
||
void main() {
|
||
group('Torrent Integration Tests', () {
|
||
late TorrentPlatformService service;
|
||
late List<MethodCall> methodCalls;
|
||
|
||
// Sintel - открытый короткометражный фильм от Blender Foundation
|
||
// Официально доступен под Creative Commons лицензией
|
||
const sintelMagnetLink = 'magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10'
|
||
'&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969'
|
||
'&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969'
|
||
'&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337'
|
||
'&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969'
|
||
'&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337'
|
||
'&tr=wss%3A%2F%2Ftracker.btorrent.xyz'
|
||
'&tr=wss%3A%2F%2Ftracker.fastcast.nz'
|
||
'&tr=wss%3A%2F%2Ftracker.openwebtorrent.com'
|
||
'&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F'
|
||
'&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent';
|
||
|
||
const expectedTorrentHash = '08ada5a7a6183aae1e09d831df6748d566095a10';
|
||
|
||
setUp(() {
|
||
service = TorrentPlatformService();
|
||
methodCalls = [];
|
||
|
||
// Mock platform channel для симуляции Android ответов
|
||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||
.setMockMethodCallHandler(
|
||
const MethodChannel('com.neo.neomovies_mobile/torrent'),
|
||
(MethodCall methodCall) async {
|
||
methodCalls.add(methodCall);
|
||
return _handleSintelMethodCall(methodCall);
|
||
},
|
||
);
|
||
});
|
||
|
||
tearDown(() {
|
||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||
.setMockMethodCallHandler(
|
||
const MethodChannel('com.neo.neomovies_mobile/torrent'),
|
||
null,
|
||
);
|
||
});
|
||
|
||
group('Real Magnet Link Tests', () {
|
||
test('should parse Sintel magnet link correctly', () {
|
||
// Проверяем, что магнет ссылка содержит правильные компоненты
|
||
expect(sintelMagnetLink, contains('urn:btih:$expectedTorrentHash'));
|
||
expect(sintelMagnetLink, contains('Sintel'));
|
||
expect(sintelMagnetLink, contains('tracker.opentrackr.org'));
|
||
|
||
// Проверяем, что это действительно magnet ссылка
|
||
expect(sintelMagnetLink, startsWith('magnet:?xt=urn:btih:'));
|
||
|
||
// Извлекаем hash из магнет ссылки
|
||
final hashMatch = RegExp(r'urn:btih:([a-fA-F0-9]{40})').firstMatch(sintelMagnetLink);
|
||
expect(hashMatch, isNotNull);
|
||
expect(hashMatch!.group(1)?.toLowerCase(), expectedTorrentHash);
|
||
});
|
||
|
||
test('should add Sintel torrent successfully', () async {
|
||
const downloadPath = '/storage/emulated/0/Download/Torrents';
|
||
|
||
final result = await service.addTorrent(sintelMagnetLink, downloadPath);
|
||
|
||
// Проверяем, что метод был вызван с правильными параметрами
|
||
expect(methodCalls.length, 1);
|
||
expect(methodCalls.first.method, 'addTorrent');
|
||
expect(methodCalls.first.arguments['magnetUri'], sintelMagnetLink);
|
||
expect(methodCalls.first.arguments['downloadPath'], downloadPath);
|
||
|
||
// Проверяем результат
|
||
expect(result, isA<Map<String, dynamic>>());
|
||
expect(result['success'], isTrue);
|
||
expect(result['torrentHash'], expectedTorrentHash);
|
||
});
|
||
|
||
test('should retrieve Sintel torrent info', () async {
|
||
// Добавляем торрент
|
||
await service.addTorrent(sintelMagnetLink, '/storage/emulated/0/Download/Torrents');
|
||
methodCalls.clear(); // Очищаем предыдущие вызовы
|
||
|
||
// Получаем информацию о торренте
|
||
final torrentInfo = await service.getTorrentInfo(expectedTorrentHash);
|
||
|
||
expect(methodCalls.length, 1);
|
||
expect(methodCalls.first.method, 'getTorrentInfo');
|
||
expect(methodCalls.first.arguments['torrentHash'], expectedTorrentHash);
|
||
|
||
expect(torrentInfo, isNotNull);
|
||
expect(torrentInfo!.infoHash, expectedTorrentHash);
|
||
expect(torrentInfo.name, contains('Sintel'));
|
||
|
||
// Проверяем, что обнаружены видео файлы
|
||
final videoFiles = torrentInfo.videoFiles;
|
||
expect(videoFiles.isNotEmpty, isTrue);
|
||
|
||
final mainFile = torrentInfo.mainVideoFile;
|
||
expect(mainFile, isNotNull);
|
||
expect(mainFile!.name.toLowerCase(), anyOf(
|
||
contains('.mp4'),
|
||
contains('.mkv'),
|
||
contains('.avi'),
|
||
contains('.webm'),
|
||
));
|
||
});
|
||
|
||
test('should handle torrent operations on Sintel', () async {
|
||
// Добавляем торрент
|
||
await service.addTorrent(sintelMagnetLink, '/storage/emulated/0/Download/Torrents');
|
||
|
||
// Тестируем все операции
|
||
await service.pauseTorrent(expectedTorrentHash);
|
||
await service.resumeTorrent(expectedTorrentHash);
|
||
|
||
// Проверяем приоритеты файлов
|
||
final priorities = await service.getFilePriorities(expectedTorrentHash);
|
||
expect(priorities, isA<List<FilePriority>>());
|
||
expect(priorities.isNotEmpty, isTrue);
|
||
|
||
// Устанавливаем высокий приоритет для первого файла
|
||
await service.setFilePriority(expectedTorrentHash, 0, FilePriority.high);
|
||
|
||
// Получаем список всех торрентов
|
||
final allTorrents = await service.getAllTorrents();
|
||
expect(allTorrents.any((t) => t.infoHash == expectedTorrentHash), isTrue);
|
||
|
||
// Удаляем торрент
|
||
await service.removeTorrent(expectedTorrentHash);
|
||
|
||
// Проверяем все вызовы методов
|
||
final expectedMethods = ['addTorrent', 'pauseTorrent', 'resumeTorrent',
|
||
'getFilePriorities', 'setFilePriority', 'getAllTorrents', 'removeTorrent'];
|
||
final actualMethods = methodCalls.map((call) => call.method).toList();
|
||
|
||
for (final method in expectedMethods) {
|
||
expect(actualMethods, contains(method));
|
||
}
|
||
});
|
||
});
|
||
|
||
group('Network and Environment Tests', () {
|
||
test('should work in GitHub Actions environment', () async {
|
||
// Проверяем переменные окружения GitHub Actions
|
||
final isGitHubActions = Platform.environment['GITHUB_ACTIONS'] == 'true';
|
||
final isCI = Platform.environment['CI'] == 'true';
|
||
|
||
if (isGitHubActions || isCI) {
|
||
print('Running in CI/GitHub Actions environment');
|
||
|
||
// В CI окружении используем более короткие таймауты
|
||
// и дополнительные проверки
|
||
expect(Platform.environment['RUNNER_OS'], isNotNull);
|
||
}
|
||
|
||
// Тест должен работать в любом окружении
|
||
final result = await service.addTorrent(sintelMagnetLink, '/tmp/test');
|
||
expect(result['success'], isTrue);
|
||
});
|
||
|
||
test('should handle network timeouts gracefully', () async {
|
||
// Симулируем медленную сеть
|
||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||
.setMockMethodCallHandler(
|
||
const MethodChannel('com.neo.neomovies_mobile/torrent'),
|
||
(MethodCall methodCall) async {
|
||
if (methodCall.method == 'addTorrent') {
|
||
// Симулируем задержку сети
|
||
await Future.delayed(const Duration(milliseconds: 100));
|
||
return _handleSintelMethodCall(methodCall);
|
||
}
|
||
return _handleSintelMethodCall(methodCall);
|
||
},
|
||
);
|
||
|
||
final stopwatch = Stopwatch()..start();
|
||
final result = await service.addTorrent(sintelMagnetLink, '/tmp/test');
|
||
stopwatch.stop();
|
||
|
||
expect(result['success'], isTrue);
|
||
expect(stopwatch.elapsedMilliseconds, lessThan(5000)); // Максимум 5 секунд
|
||
});
|
||
|
||
test('should validate magnet link format', () {
|
||
// Проверяем различные форматы магнет ссылок
|
||
const validMagnets = [
|
||
sintelMagnetLink,
|
||
'magnet:?xt=urn:btih:1234567890abcdef1234567890abcdef12345678&dn=test',
|
||
];
|
||
|
||
const invalidMagnets = [
|
||
'not-a-magnet-link',
|
||
'http://example.com/torrent',
|
||
'magnet:invalid',
|
||
'',
|
||
];
|
||
|
||
for (final magnet in validMagnets) {
|
||
expect(_isValidMagnetLink(magnet), isTrue, reason: 'Should accept valid magnet: $magnet');
|
||
}
|
||
|
||
for (final magnet in invalidMagnets) {
|
||
expect(_isValidMagnetLink(magnet), isFalse, reason: 'Should reject invalid magnet: $magnet');
|
||
}
|
||
});
|
||
});
|
||
|
||
group('Performance Tests', () {
|
||
test('should handle multiple concurrent operations', () async {
|
||
// Тестируем параллельные операции
|
||
final futures = <Future>[];
|
||
|
||
// Параллельно выполняем несколько операций
|
||
futures.add(service.addTorrent(sintelMagnetLink, '/tmp/test1'));
|
||
futures.add(service.getAllTorrents());
|
||
futures.add(service.getTorrentInfo(expectedTorrentHash));
|
||
|
||
final results = await Future.wait(futures);
|
||
|
||
expect(results.length, 3);
|
||
expect(results[0], isA<Map<String, dynamic>>()); // addTorrent result
|
||
expect(results[1], isA<List<TorrentInfo>>()); // getAllTorrents result
|
||
expect(results[2], anyOf(isA<TorrentInfo>(), isNull)); // getTorrentInfo result
|
||
});
|
||
|
||
test('should complete operations within reasonable time', () async {
|
||
final stopwatch = Stopwatch()..start();
|
||
|
||
await service.addTorrent(sintelMagnetLink, '/tmp/test');
|
||
await service.getAllTorrents();
|
||
await service.removeTorrent(expectedTorrentHash);
|
||
|
||
stopwatch.stop();
|
||
|
||
// Все операции должны завершиться быстро (меньше 1 секунды в тестах)
|
||
expect(stopwatch.elapsedMilliseconds, lessThan(1000));
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/// Проверяет, является ли строка валидной магнет ссылкой
|
||
bool _isValidMagnetLink(String link) {
|
||
if (!link.startsWith('magnet:?')) return false;
|
||
|
||
// Проверяем наличие xt параметра с BitTorrent hash
|
||
final btihPattern = RegExp(r'xt=urn:btih:[a-fA-F0-9]{40}');
|
||
return btihPattern.hasMatch(link);
|
||
}
|
||
|
||
/// Mock обработчик для Sintel торрента
|
||
dynamic _handleSintelMethodCall(MethodCall methodCall) {
|
||
switch (methodCall.method) {
|
||
case 'addTorrent':
|
||
final magnetUri = methodCall.arguments['magnetUri'] as String;
|
||
if (magnetUri.contains('08ada5a7a6183aae1e09d831df6748d566095a10')) {
|
||
return {
|
||
'success': true,
|
||
'torrentHash': '08ada5a7a6183aae1e09d831df6748d566095a10',
|
||
};
|
||
}
|
||
return {'success': false, 'error': 'Invalid magnet link'};
|
||
|
||
case 'getTorrentInfo':
|
||
final hash = methodCall.arguments['torrentHash'] as String;
|
||
if (hash == '08ada5a7a6183aae1e09d831df6748d566095a10') {
|
||
return _getSintelTorrentData();
|
||
}
|
||
return null;
|
||
|
||
case 'getAllTorrents':
|
||
return [_getSintelTorrentData()];
|
||
|
||
case 'pauseTorrent':
|
||
case 'resumeTorrent':
|
||
case 'removeTorrent':
|
||
return {'success': true};
|
||
|
||
case 'setFilePriority':
|
||
return {'success': true};
|
||
|
||
case 'getFilePriorities':
|
||
return [
|
||
FilePriority.high.value,
|
||
FilePriority.normal.value,
|
||
FilePriority.low.value,
|
||
];
|
||
|
||
default:
|
||
throw PlatformException(
|
||
code: 'UNIMPLEMENTED',
|
||
message: 'Method ${methodCall.method} not implemented in mock',
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Возвращает mock данные для Sintel торрента
|
||
Map<String, dynamic> _getSintelTorrentData() {
|
||
return {
|
||
'name': 'Sintel (2010) [1080p]',
|
||
'infoHash': '08ada5a7a6183aae1e09d831df6748d566095a10',
|
||
'state': 'downloading',
|
||
'progress': 0.15, // 15% загружено
|
||
'downloadSpeed': 1500000, // 1.5 MB/s
|
||
'uploadSpeed': 200000, // 200 KB/s
|
||
'totalSize': 734003200, // ~700 MB
|
||
'downloadedSize': 110100480, // ~105 MB
|
||
'seeders': 45,
|
||
'leechers': 12,
|
||
'ratio': 0.8,
|
||
'addedTime': DateTime.now().subtract(const Duration(minutes: 30)).millisecondsSinceEpoch,
|
||
'files': [
|
||
{
|
||
'name': 'Sintel.2010.1080p.mkv',
|
||
'size': 734003200,
|
||
'path': '/storage/emulated/0/Download/Torrents/Sintel/Sintel.2010.1080p.mkv',
|
||
'priority': FilePriority.high.value,
|
||
},
|
||
{
|
||
'name': 'Sintel.2010.720p.mp4',
|
||
'size': 367001600, // ~350 MB
|
||
'path': '/storage/emulated/0/Download/Torrents/Sintel/Sintel.2010.720p.mp4',
|
||
'priority': FilePriority.normal.value,
|
||
},
|
||
{
|
||
'name': 'subtitles/Sintel.srt',
|
||
'size': 52428, // ~51 KB
|
||
'path': '/storage/emulated/0/Download/Torrents/Sintel/subtitles/Sintel.srt',
|
||
'priority': FilePriority.normal.value,
|
||
},
|
||
{
|
||
'name': 'README.txt',
|
||
'size': 2048,
|
||
'path': '/storage/emulated/0/Download/Torrents/Sintel/README.txt',
|
||
'priority': FilePriority.low.value,
|
||
},
|
||
],
|
||
};
|
||
} |