mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 06:28:27 +02:00
186 lines
6.0 KiB
Dart
186 lines
6.0 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:spotiflac_android/models/track.dart';
|
|
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
|
import 'package:spotiflac_android/providers/playback_provider.dart';
|
|
import 'package:spotiflac_android/services/history_database.dart';
|
|
import 'package:spotiflac_android/services/library_database.dart';
|
|
|
|
class _Library extends Fake implements LibraryDatabase {
|
|
@override
|
|
Future<List<Map<String, dynamic>?>> findExistingBatch(
|
|
List<LocalLibraryBatchLookupRequest> requests,
|
|
) async => List.filled(requests.length, null);
|
|
}
|
|
|
|
class _HistoryDatabase extends Fake implements HistoryDatabase {
|
|
final Map<String, String> paths = {};
|
|
final List<List<String>> deletes = [];
|
|
bool failDelete = false;
|
|
|
|
@override
|
|
Future<List<Map<String, dynamic>?>> findExistingTracks(
|
|
List<HistoryLookupRequest> requests,
|
|
) async => [
|
|
for (final request in requests)
|
|
{'id': request.spotifyId, 'filePath': paths[request.spotifyId]},
|
|
];
|
|
|
|
@override
|
|
Future<int> deleteByIds(List<String> ids) async {
|
|
deletes.add(List.of(ids));
|
|
if (failDelete) throw StateError('write failed');
|
|
var deleted = 0;
|
|
for (final id in ids) {
|
|
if (paths.remove(id) != null) deleted++;
|
|
}
|
|
return deleted;
|
|
}
|
|
}
|
|
|
|
class _History extends DownloadHistoryNotifier {
|
|
_History(this.database) : super(database: database);
|
|
final _HistoryDatabase database;
|
|
|
|
@override
|
|
DownloadHistoryState build() {
|
|
final items = [
|
|
for (final entry in database.paths.entries)
|
|
DownloadHistoryItem(
|
|
id: entry.key,
|
|
trackName: 'Track ${entry.key}',
|
|
artistName: 'Artist',
|
|
albumName: 'Album',
|
|
filePath: entry.value,
|
|
service: 'example',
|
|
downloadedAt: DateTime(2026),
|
|
),
|
|
];
|
|
return DownloadHistoryState(
|
|
items: items,
|
|
lookupItems: items,
|
|
totalCount: items.length,
|
|
);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
const channel = MethodChannel('com.zarz.spotiflac/backend');
|
|
final messenger =
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
|
tearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
|
|
|
List<Track> tracks(int count) => [
|
|
for (var index = 0; index < count; index++)
|
|
Track(
|
|
id: '$index',
|
|
name: 'Track $index',
|
|
artistName: 'Artist',
|
|
albumName: 'Album',
|
|
duration: 180,
|
|
),
|
|
];
|
|
|
|
ProviderContainer containerFor(_HistoryDatabase history) {
|
|
final container = ProviderContainer(
|
|
overrides: [
|
|
playbackProvider.overrideWith(
|
|
() => PlaybackController(library: _Library(), history: history),
|
|
),
|
|
downloadHistoryProvider.overrideWith(() => _History(history)),
|
|
],
|
|
);
|
|
addTearDown(container.dispose);
|
|
return container;
|
|
}
|
|
|
|
test('large SAF queues use bounded batches and deduplicate paths', () async {
|
|
final database = _HistoryDatabase();
|
|
for (var index = 0; index < 131; index++) {
|
|
database.paths['$index'] = 'content://music/${index % 130}';
|
|
}
|
|
final batches = <List<String>>[];
|
|
messenger.setMockMethodCallHandler(channel, (call) async {
|
|
expect(call.method, 'safExistsBatch');
|
|
final uris = List<String>.from(
|
|
jsonDecode((call.arguments as Map)['uris_json'] as String) as List,
|
|
);
|
|
batches.add(uris);
|
|
return jsonEncode({for (final uri in uris) uri: 'found'});
|
|
});
|
|
final container = containerFor(database);
|
|
final paths = await container
|
|
.read(playbackProvider.notifier)
|
|
.resolveTrackFilePaths(tracks(131));
|
|
expect(paths, database.paths.values);
|
|
expect(batches.map((batch) => batch.length), [64, 64, 2]);
|
|
expect(database.deletes, isEmpty);
|
|
});
|
|
|
|
test(
|
|
'confirmed missing rows delete once while unknown SAF rows survive',
|
|
() async {
|
|
final database = _HistoryDatabase()
|
|
..paths.addAll({
|
|
'0': 'content://music/missing',
|
|
'1': 'content://music/found',
|
|
'2': 'content://music/unknown',
|
|
});
|
|
final calls = <String>[];
|
|
messenger.setMockMethodCallHandler(channel, (call) async {
|
|
calls.add(call.method);
|
|
if (call.method == 'safExists') return false;
|
|
return jsonEncode({
|
|
'content://music/missing': 'missing',
|
|
'content://music/found': 'found',
|
|
});
|
|
});
|
|
final container = containerFor(database);
|
|
var revisions = 0;
|
|
container.listen(
|
|
downloadHistoryProvider.select((state) => state.loadedIndexVersion),
|
|
(_, _) => revisions++,
|
|
);
|
|
final paths = await container
|
|
.read(playbackProvider.notifier)
|
|
.resolveTrackFilePaths(tracks(3));
|
|
expect(paths, [null, 'content://music/found', null]);
|
|
expect(calls, ['safExistsBatch', 'safExists']);
|
|
expect(database.deletes, [
|
|
['0'],
|
|
]);
|
|
expect(revisions, 1);
|
|
final state = container.read(downloadHistoryProvider);
|
|
expect(state.items.map((item) => item.id), ['1', '2']);
|
|
expect(state.totalCount, 2);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'batch deletion deduplicates IDs and a failed write keeps the snapshot',
|
|
() async {
|
|
final database = _HistoryDatabase()..paths['0'] = '/music/track.flac';
|
|
final container = containerFor(database);
|
|
final before = container.read(downloadHistoryProvider);
|
|
database.failDelete = true;
|
|
await container
|
|
.read(downloadHistoryProvider.notifier)
|
|
.removeManyFromHistory(['0', '0']);
|
|
expect(database.deletes, [
|
|
['0'],
|
|
]);
|
|
expect(container.read(downloadHistoryProvider), same(before));
|
|
database.failDelete = false;
|
|
await container
|
|
.read(downloadHistoryProvider.notifier)
|
|
.removeManyFromHistory(['0', '0']);
|
|
expect(container.read(downloadHistoryProvider).items, isEmpty);
|
|
expect(container.read(downloadHistoryProvider).loadedIndexVersion, 1);
|
|
},
|
|
);
|
|
}
|