perf(nas): process listings, artwork caches, and upload recovery natively

Move WebDAV/HTML parsing, content-addressed artwork caching, cover promotion, and streamed upload verification to native workers. Preserve proxy authentication, cancellation, cache limits, and deep XML fallback behavior.
This commit is contained in:
zarzet committed 2026-10-06 23:27:07 +07:00
1 parent 867371cb67
commit 927fd033cd
15 files changed
+1349 -98

No files matched your search

+98
View File
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/services/network_download_staging.dart';
import 'package:spotiflac_android/services/network_storage_service.dart';
@@ -97,6 +99,10 @@ class _Dav {
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
// These integration tests use an authenticated in-process WebDAV server.
HttpOverrides.global = null;
const channel = MethodChannel('com.zarz.spotiflac/backend');
late _Dav dav;
late NetworkStorageService storage;
late NetworkDownloadStaging staging;
@@ -126,6 +132,8 @@ void main() {
);
});
tearDown(() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
await storage.dispose();
await dav.server.close(force: true);
await temp.delete(recursive: true);
@@ -327,6 +335,96 @@ void main() {
},
);
test(
'native recovery receives only the authenticated proxy and local path',
() async {
final file = await prepare(dir: '');
dav.loseMoveResponse = true;
await expectLater(publish(), throwsA(anything));
var nativeHashes = 0;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
final args = Map<String, dynamic>.from(call.arguments as Map);
final request = jsonDecode(args['request_json'] as String) as Map;
if (request['operation'] != 'matches_network_upload') {
throw MissingPluginException();
}
final uri = Uri.parse(request['url'] as String);
expect(uri.scheme, 'http');
expect(uri.host, '127.0.0.1');
expect(uri.hasPort, true);
expect(uri.path, matches(r'^/[a-f0-9]{48}$'));
expect(jsonEncode(request), isNot(contains('secret')));
expect(request['length'], await file.length());
expect(
await File(request['path'] as String).readAsBytes(),
await file.readAsBytes(),
);
expect(request['idle_timeout_ms'], 20000);
nativeHashes++;
return {'matches': true};
});
await publish();
expect(nativeHashes, 1);
expect(dav.puts, 1);
},
);
test(
'native recovery cancellation keeps staging until the worker completes',
() async {
final file = await prepare(dir: '');
dav.loseMoveResponse = true;
await expectLater(publish(), throwsA(anything));
final started = Completer<void>();
final cancelled = Completer<void>();
final worker = Completer<Map<String, dynamic>>();
String? requestId;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
final args = Map<String, dynamic>.from(call.arguments as Map);
if (call.method == 'cancelNativeDataJob') {
expect(args['request_id'], requestId);
cancelled.complete();
return null;
}
final request = jsonDecode(args['request_json'] as String) as Map;
if (request['operation'] != 'matches_network_upload') {
throw MissingPluginException();
}
requestId = args['request_id'] as String;
started.complete();
return worker.future;
});
var stop = false;
var completed = false;
final originalError = StateError('user cancelled verification');
final result = publish(
checkpoint: () {
if (stop) throw originalError;
},
);
final expectation = expectLater(result, throwsA(same(originalError)));
unawaited(
result.then(
(_) => completed = true,
onError: (Object _) {
completed = true;
},
),
);
await started.future;
stop = true;
await cancelled.future;
expect(completed, false);
expect(await file.exists(), true);
worker.completeError(PlatformException(code: 'cancelled'));
await expectation;
expect(dav.puts, 1);
expect(await file.exists(), true);
},
);
test(
'same-length conflicting file is never overwritten or adopted',
() async {