mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 04:42:02 +02:00
314 lines
10 KiB
Dart
314 lines
10 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:spotiflac_android/services/update_checker.dart';
|
|
import 'package:spotiflac_android/l10n/l10n.dart';
|
|
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
|
|
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
|
import 'package:spotiflac_android/utils/logger.dart';
|
|
import 'package:spotiflac_android/widgets/update_dialog.dart';
|
|
|
|
Map<String, Object?> release(String version) => {
|
|
'tag_name': 'v$version',
|
|
'prerelease': false,
|
|
'draft': false,
|
|
'body': '- Release notes',
|
|
'published_at': '2026-09-26T06:32:33Z',
|
|
'html_url':
|
|
'https://github.com/spotiflacapp/SpotiFLAC-Mobile/releases/tag/v$version',
|
|
'assets': [
|
|
for (final abi in ['arm32', 'arm64'])
|
|
{
|
|
'name': 'SpotiFLAC-v$version-$abi.apk',
|
|
'browser_download_url':
|
|
'https://example.test/SpotiFLAC-v$version-$abi.apk',
|
|
'digest': 'sha256:${List.filled(64, 'a').join()}',
|
|
},
|
|
],
|
|
};
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
LogBuffer().clear();
|
|
LogBuffer.loggingEnabled = true;
|
|
});
|
|
tearDown(() => LogBuffer.loggingEnabled = false);
|
|
|
|
for (final (abi, asset) in [
|
|
('arm64-v8a', 'arm64'),
|
|
('armeabi-v7a', 'arm32'),
|
|
]) {
|
|
test('5.0.0 discovers 5.0.5 despite a recent cache ($abi)', () async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'update_checker_releases_json': jsonEncode([release('5.0.0')]),
|
|
'update_checker_releases_fetched_at':
|
|
DateTime.now().millisecondsSinceEpoch,
|
|
'update_checker_releases_etag': '"old-release-list"',
|
|
});
|
|
var requests = 0;
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.0',
|
|
isAndroid: true,
|
|
supportedAbis: [abi],
|
|
client: MockClient((request) async {
|
|
requests++;
|
|
expect(request.headers['If-None-Match'], '"old-release-list"');
|
|
return http.Response(
|
|
jsonEncode([release('5.0.5'), release('5.0.0')]),
|
|
200,
|
|
headers: {'etag': '"new-release-list"'},
|
|
);
|
|
}),
|
|
);
|
|
|
|
final update = await checker.checkForUpdate();
|
|
expect(requests, 1);
|
|
expect(update?.version, '5.0.5');
|
|
expect(update?.apkDownloadUrl, endsWith('v5.0.5-$asset.apk'));
|
|
expect(update?.releasesBehind, 1);
|
|
expect(update?.apkSha256, List.filled(64, 'a').join());
|
|
final prefs = await SharedPreferences.getInstance();
|
|
expect(
|
|
prefs.getString('update_checker_releases_etag'),
|
|
'"new-release-list"',
|
|
);
|
|
expect(
|
|
prefs.getString('update_checker_releases_json'),
|
|
contains('v5.0.5'),
|
|
);
|
|
});
|
|
}
|
|
|
|
test(
|
|
'checks again immediately and reuses the body only after GitHub 304',
|
|
() async {
|
|
var requests = 0;
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.0',
|
|
isAndroid: true,
|
|
supportedAbis: ['arm64-v8a'],
|
|
client: MockClient((request) async {
|
|
requests++;
|
|
if (requests == 1) {
|
|
expect(request.headers['If-None-Match'], isNull);
|
|
return http.Response(
|
|
jsonEncode([release('5.0.5')]),
|
|
200,
|
|
headers: {'etag': '"fresh"'},
|
|
);
|
|
}
|
|
expect(request.headers['If-None-Match'], '"fresh"');
|
|
return http.Response('', 304);
|
|
}),
|
|
);
|
|
expect((await checker.checkForUpdate())?.version, '5.0.5');
|
|
expect((await checker.checkForUpdate())?.version, '5.0.5');
|
|
expect(requests, 2);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'replacing a body without an ETag removes the previous validator',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'update_checker_releases_json': jsonEncode([release('5.0.0')]),
|
|
'update_checker_releases_etag': '"old"',
|
|
});
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.0',
|
|
isAndroid: true,
|
|
supportedAbis: ['arm64-v8a'],
|
|
client: MockClient(
|
|
(_) async => http.Response(jsonEncode([release('5.0.5')]), 200),
|
|
),
|
|
);
|
|
expect((await checker.checkForUpdate())?.version, '5.0.5');
|
|
expect(
|
|
(await SharedPreferences.getInstance()).getString(
|
|
'update_checker_releases_etag',
|
|
),
|
|
isNull,
|
|
);
|
|
},
|
|
);
|
|
|
|
for (final failure in ['offline', 'rate limit', 'invalid JSON']) {
|
|
test('$failure does not claim cached 5.0.0 is the latest online', () async {
|
|
final cachedBody = jsonEncode([release('5.0.0')]);
|
|
SharedPreferences.setMockInitialValues({
|
|
'update_checker_releases_json': cachedBody,
|
|
'update_checker_releases_etag': '"cached"',
|
|
});
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.0',
|
|
isAndroid: true,
|
|
supportedAbis: ['arm64-v8a'],
|
|
client: MockClient((_) async {
|
|
if (failure == 'offline') throw http.ClientException('Offline');
|
|
if (failure == 'rate limit') return http.Response('Limited', 403);
|
|
return http.Response('{"message":"Invalid response"}', 200);
|
|
}),
|
|
);
|
|
expect(await checker.checkForUpdate(), isNull);
|
|
final messages = LogBuffer().entries
|
|
.map((entry) => entry.message)
|
|
.join('\n');
|
|
expect(messages, contains('could not be verified'));
|
|
expect(messages, isNot(contains('No update available')));
|
|
final prefs = await SharedPreferences.getInstance();
|
|
expect(prefs.getString('update_checker_releases_json'), cachedBody);
|
|
expect(prefs.getString('update_checker_releases_etag'), '"cached"');
|
|
});
|
|
}
|
|
|
|
test(
|
|
'an offline check can still offer a previously discovered update',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'update_checker_releases_json': jsonEncode([release('5.0.5')]),
|
|
});
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.0',
|
|
isAndroid: true,
|
|
supportedAbis: ['armeabi-v7a'],
|
|
client: MockClient((_) async => throw http.ClientException('Offline')),
|
|
);
|
|
expect((await checker.checkForUpdate())?.version, '5.0.5');
|
|
},
|
|
);
|
|
|
|
test(
|
|
'invalid stored JSON does not send its ETag or block fresh releases',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'update_checker_releases_json': '{broken',
|
|
'update_checker_releases_etag': '"broken"',
|
|
});
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.0',
|
|
isAndroid: true,
|
|
supportedAbis: ['arm64-v8a'],
|
|
client: MockClient((request) async {
|
|
expect(request.headers['If-None-Match'], isNull);
|
|
return http.Response(jsonEncode([release('5.0.5')]), 200);
|
|
}),
|
|
);
|
|
expect((await checker.checkForUpdate())?.version, '5.0.5');
|
|
},
|
|
);
|
|
|
|
test('stable skips previews and counts only newer stable releases', () async {
|
|
final checker = UpdateChecker(
|
|
installedVersion: '4.9.5',
|
|
isAndroid: true,
|
|
supportedAbis: ['arm64-v8a'],
|
|
client: MockClient(
|
|
(_) async => http.Response(
|
|
jsonEncode([
|
|
{...release('5.0.6-preview'), 'prerelease': true},
|
|
release('5.0.5'),
|
|
release('5.0.0'),
|
|
release('4.9.6'),
|
|
release('4.9.5'),
|
|
]),
|
|
200,
|
|
),
|
|
),
|
|
);
|
|
final stable = await checker.checkForUpdate();
|
|
expect(stable?.version, '5.0.5');
|
|
expect(stable?.releasesBehind, UpdateChecker.forceUpdateThreshold);
|
|
expect(
|
|
(await checker.checkForUpdate(channel: 'preview'))?.version,
|
|
'5.0.6-preview',
|
|
);
|
|
});
|
|
|
|
test(
|
|
'an installed 5.0.5 does not prompt to reinstall the same version',
|
|
() async {
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.5',
|
|
isAndroid: true,
|
|
client: MockClient(
|
|
(_) async => http.Response(jsonEncode([release('5.0.5')]), 200),
|
|
),
|
|
);
|
|
expect(await checker.checkForUpdate(), isNull);
|
|
},
|
|
);
|
|
|
|
test('non-Android platforms do not request or offer an APK', () async {
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.0',
|
|
isAndroid: false,
|
|
client: MockClient(
|
|
(_) async => throw StateError('Unexpected network request'),
|
|
),
|
|
);
|
|
expect(await checker.checkForUpdate(), isNull);
|
|
});
|
|
|
|
for (final mornye in [false, true]) {
|
|
testWidgets(
|
|
'freshly discovered release opens the update dialog ($mornye)',
|
|
(tester) async {
|
|
tester.view.physicalSize = const Size(390, 844);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
final checker = UpdateChecker(
|
|
installedVersion: '5.0.0',
|
|
isAndroid: true,
|
|
supportedAbis: ['arm64-v8a'],
|
|
client: MockClient(
|
|
(_) async => http.Response(jsonEncode([release('5.0.5')]), 200),
|
|
),
|
|
);
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [lowEndDeviceProvider.overrideWithValue(false)],
|
|
child: MaterialApp(
|
|
theme: mornye ? MornyeTheme.build(Brightness.dark) : null,
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (context) => TextButton(
|
|
onPressed: () async {
|
|
final update = await checker.checkForUpdate();
|
|
if (context.mounted && update != null) {
|
|
await showUpdateDialog(
|
|
context,
|
|
updateInfo: update,
|
|
onDisableUpdates: () {},
|
|
);
|
|
}
|
|
},
|
|
child: const Text('Check now'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('Check now'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(UpdateDialog), findsOneWidget);
|
|
expect(find.text('v5.0.5'), findsOneWidget);
|
|
expect(find.textContaining('Release notes'), findsOneWidget);
|
|
expect(find.text('Download & Install').hitTestable(), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
}
|
|
}
|