161 lines
6.1 KiB
Dart
161 lines
6.1 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:immich_mobile/services/app_update.service.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
const id = 'be34e6a7-5970-41ab-af01-71789e844f73';
|
|
final bytes = utf8.encode('APK test fixture');
|
|
Map<String, dynamic> manifest() => {
|
|
'slug': 'immich-local',
|
|
'release': {
|
|
'id': id,
|
|
'version': '3.3.3-local.2',
|
|
'androidVersionCode': 3030401,
|
|
'channel': 'stable',
|
|
'platform': 'android',
|
|
'packageKind': 'apk',
|
|
'downloadPath': '/api/apps/immich-local/releases/$id/download',
|
|
'packageSizeBytes': bytes.length,
|
|
'sha256': sha256.convert(bytes).toString(),
|
|
'notes': 'Changes',
|
|
},
|
|
};
|
|
|
|
test('compares Android build numbers; same and older releases cannot be installed', () {
|
|
final release = AppRelease.fromManifest(manifest())!;
|
|
expect(release.isNewerThan('3030400'), isTrue);
|
|
expect(release.isNewerThan('3030401'), isFalse);
|
|
expect(release.isNewerThan('3030402'), isFalse);
|
|
expect(() => release.isNewerThan('unknown'), throwsFormatException);
|
|
expect(release.downloadUri.host, 'argus.kusoft.xyz');
|
|
});
|
|
|
|
test('a catalog with no release is valid', () {
|
|
expect(AppRelease.fromManifest({'slug': 'immich-local', 'release': null}), isNull);
|
|
});
|
|
|
|
test('rejects another application catalog', () {
|
|
expect(() => AppRelease.fromManifest({...manifest(), 'slug': 'another-app'}), throwsFormatException);
|
|
});
|
|
|
|
for (final entry in <String, Object?>{
|
|
'id': '../invalid',
|
|
'platform': 'ios',
|
|
'channel': 'beta',
|
|
'packageKind': 'zip',
|
|
'androidVersionCode': '3030401',
|
|
'version': '',
|
|
'packageSizeBytes': 0,
|
|
'sha256': 'invalid',
|
|
'downloadPath': 'https://example.com/app.apk',
|
|
'notes': 42,
|
|
}.entries) {
|
|
test('rejects invalid ${entry.key}', () {
|
|
final data = manifest();
|
|
(data['release'] as Map<String, dynamic>)[entry.key] = entry.value;
|
|
expect(() => AppRelease.fromManifest(data), throwsFormatException);
|
|
});
|
|
}
|
|
|
|
test('queries only stable Android releases without cache or redirects', () async {
|
|
final service = AppUpdateService(
|
|
clientFactory: () => MockClient((request) async {
|
|
expect(
|
|
request.url.toString(),
|
|
'https://argus.kusoft.xyz/api/apps/immich-local/manifest?platform=android&channel=stable',
|
|
);
|
|
expect(request.followRedirects, isFalse);
|
|
expect(request.headers['Cache-Control'], 'no-cache');
|
|
return http.Response(jsonEncode(manifest()), 200);
|
|
}),
|
|
);
|
|
expect((await service.check())!.versionCode, 3030401);
|
|
service.dispose();
|
|
});
|
|
|
|
test('server errors are reported instead of saying up to date', () async {
|
|
final service = AppUpdateService(clientFactory: () => MockClient((_) async => http.Response('', 503)));
|
|
await expectLater(service.check(), throwsA(isA<HttpException>()));
|
|
service.dispose();
|
|
});
|
|
|
|
group('verified download', () {
|
|
late Directory directory;
|
|
setUp(() async => directory = await Directory.systemTemp.createTemp('immich-update-test-'));
|
|
tearDown(() async => directory.delete(recursive: true));
|
|
AppUpdateService service(List<int> body, {int status = 200}) => AppUpdateService(
|
|
temporaryDirectory: () async => directory,
|
|
clientFactory: () => MockClient((request) async {
|
|
expect(request.followRedirects, isFalse);
|
|
return http.Response.bytes(body, status);
|
|
}),
|
|
);
|
|
|
|
test('writes an APK only after its size and digest match', () async {
|
|
final updater = service(bytes);
|
|
final progress = <double>[];
|
|
final apk = await updater.download(AppRelease.fromManifest(manifest())!, progress.add);
|
|
expect(await apk.readAsBytes(), bytes);
|
|
expect(progress.last, 1);
|
|
expect(File('${apk.path}.part').existsSync(), isFalse);
|
|
updater.dispose();
|
|
});
|
|
|
|
for (final body in [
|
|
utf8.encode('wrong bytes here'),
|
|
bytes.sublist(1),
|
|
[...bytes, 0],
|
|
]) {
|
|
test('rejects corrupt or truncated content and removes the partial file (${body.length})', () async {
|
|
final updater = service(body);
|
|
await expectLater(updater.download(AppRelease.fromManifest(manifest())!, (_) {}), throwsA(isA<Exception>()));
|
|
expect(File('${directory.path}/immich-updates/update.apk').existsSync(), isFalse);
|
|
expect(File('${directory.path}/immich-updates/update.apk.part').existsSync(), isFalse);
|
|
updater.dispose();
|
|
});
|
|
}
|
|
|
|
test('refuses redirects', () async {
|
|
final updater = service(bytes, status: 302);
|
|
await expectLater(updater.download(AppRelease.fromManifest(manifest())!, (_) {}), throwsA(isA<HttpException>()));
|
|
updater.dispose();
|
|
});
|
|
|
|
test('cancellation never leaves an installable APK', () async {
|
|
final updater = service(bytes);
|
|
await expectLater(
|
|
updater.download(AppRelease.fromManifest(manifest())!, (_) => updater.cancel()),
|
|
throwsA(isA<UpdateCancelled>()),
|
|
);
|
|
expect(File('${directory.path}/immich-updates/update.apk').existsSync(), isFalse);
|
|
updater.dispose();
|
|
});
|
|
});
|
|
|
|
test('uses the native installer and reports missing install permission', () async {
|
|
final calls = <MethodCall>[];
|
|
final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
|
messenger.setMockMethodCallHandler(AppUpdateService.channel, (call) async {
|
|
calls.add(call);
|
|
return call.method == 'install' ? 'permissionRequired' : null;
|
|
});
|
|
addTearDown(() => messenger.setMockMethodCallHandler(AppUpdateService.channel, null));
|
|
final updater = AppUpdateService();
|
|
expect(
|
|
await updater.install(File('/cache/immich-updates/update.apk'), AppRelease.fromManifest(manifest())!),
|
|
isFalse,
|
|
);
|
|
await updater.openInstallPermission();
|
|
expect(calls.map((call) => call.method), ['install', 'openInstallPermission']);
|
|
expect(calls.first.arguments['versionCode'], 3030401);
|
|
updater.dispose();
|
|
});
|
|
}
|