270 lines
8.0 KiB
Dart
270 lines
8.0 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
|
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
|
|
import 'package:miaoji_zhang/shared/update/update_api.dart';
|
|
import 'package:miaoji_zhang/shared/update/update_coordinator.dart';
|
|
import 'package:miaoji_zhang/shared/update/update_downloader.dart';
|
|
import 'package:miaoji_zhang/shared/update/update_models.dart';
|
|
import 'package:miaoji_zhang/shared/update/update_prompt.dart';
|
|
import 'package:miaoji_zhang/shared/version.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
|
|
void main() {
|
|
const release = AppRelease(
|
|
id: 'release-7',
|
|
versionName: '1.2.2',
|
|
buildNumber: 7,
|
|
downloadUrl: 'https://downloads.example.com/jizhi-7.apk',
|
|
releaseNotes: '## 本次更新\n\n- 修复识别问题\n- 优化更新体验',
|
|
sha256: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
fileSize: 1024,
|
|
publishedAt: null,
|
|
);
|
|
|
|
test('使用原生 versionName 和 buildNumber 展示版本', () async {
|
|
PackageInfo.setMockInitialValues(
|
|
appName: '记之',
|
|
packageName: 'com.nx.miaoji.internal',
|
|
version: '1.2.1-internal',
|
|
buildNumber: '6',
|
|
buildSignature: '',
|
|
);
|
|
await AppVersion.initialize();
|
|
|
|
expect(AppVersion.versionName, '1.2.1-internal');
|
|
expect(AppVersion.buildNumber, 6);
|
|
expect(AppVersion.display, 'v1.2.1-internal (6)');
|
|
});
|
|
test('更新响应保留发布信息并且只按 buildNumber 判断新旧', () {
|
|
final response = UpdateCheckResponse.fromJson({
|
|
'hasUpdate': true,
|
|
'forceUpdate': false,
|
|
'release': {
|
|
'id': 'release-7',
|
|
'versionName': '0.1.0',
|
|
'buildNumber': 7,
|
|
'downloadUrl': 'https://downloads.example.com/jizhi-7.apk',
|
|
'releaseNotes': '测试版本',
|
|
'sha256': release.sha256,
|
|
'fileSize': 1024,
|
|
'publishedAt': '2026-07-21T12:00:00Z',
|
|
},
|
|
});
|
|
|
|
expect(UpdatePolicy.availableRelease(response, currentBuild: 6), isNotNull);
|
|
expect(UpdatePolicy.availableRelease(response, currentBuild: 7), isNull);
|
|
expect(response.release?.versionName, '0.1.0');
|
|
});
|
|
|
|
test('忽略普通版本但不影响手动检查和强制更新', () {
|
|
expect(
|
|
UpdatePolicy.shouldPresent(
|
|
release: release,
|
|
forced: false,
|
|
manual: false,
|
|
ignoredReleaseId: release.id,
|
|
),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
UpdatePolicy.shouldPresent(
|
|
release: release,
|
|
forced: false,
|
|
manual: true,
|
|
ignoredReleaseId: release.id,
|
|
),
|
|
isTrue,
|
|
);
|
|
expect(
|
|
UpdatePolicy.shouldPresent(
|
|
release: release,
|
|
forced: true,
|
|
manual: false,
|
|
ignoredReleaseId: release.id,
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('UpdateApi 发送公开参数且不附带登录请求配置', () async {
|
|
final adapter = _FakeAdapter(
|
|
statusCode: 200,
|
|
body: jsonEncode({
|
|
'hasUpdate': true,
|
|
'forceUpdate': false,
|
|
'release': {
|
|
'id': release.id,
|
|
'versionName': release.versionName,
|
|
'buildNumber': release.buildNumber,
|
|
'downloadUrl': release.downloadUrl,
|
|
'releaseNotes': release.releaseNotes,
|
|
'sha256': release.sha256,
|
|
'fileSize': release.fileSize,
|
|
'publishedAt': '2026-07-21T12:00:00Z',
|
|
},
|
|
}),
|
|
);
|
|
final dio = Dio(BaseOptions(baseUrl: 'https://version.test'))
|
|
..httpClientAdapter = adapter;
|
|
|
|
final result = await UpdateApi(
|
|
dio: dio,
|
|
).check(platform: 'android', channel: 'beta', currentBuild: 6);
|
|
|
|
expect(result.release?.buildNumber, 7);
|
|
expect(adapter.request?.path, '/api/client/v1/update');
|
|
expect(adapter.request?.queryParameters['platform'], 'android');
|
|
expect(adapter.request?.queryParameters['channel'], 'beta');
|
|
expect(adapter.request?.queryParameters['currentBuild'], 6);
|
|
expect(adapter.request?.headers['Authorization'], isNull);
|
|
});
|
|
|
|
test('协调器合并并发检查请求', () async {
|
|
final adapter = _FakeAdapter(
|
|
statusCode: 200,
|
|
body: '{"hasUpdate":false,"forceUpdate":false,"release":null}',
|
|
delay: const Duration(milliseconds: 20),
|
|
);
|
|
final dio = Dio(BaseOptions(baseUrl: 'https://version.test'))
|
|
..httpClientAdapter = adapter;
|
|
final coordinator = UpdateCoordinator(api: UpdateApi(dio: dio));
|
|
|
|
await Future.wait([
|
|
coordinator.debugCheck(platform: 'android'),
|
|
coordinator.debugCheck(platform: 'android'),
|
|
]);
|
|
|
|
expect(adapter.callCount, 1);
|
|
});
|
|
|
|
test('Internal 更新缺少 SHA-256 时在下载前拒绝', () async {
|
|
const unsafeRelease = AppRelease(
|
|
id: 'unsafe',
|
|
versionName: '1.2.2',
|
|
buildNumber: 7,
|
|
downloadUrl: 'https://downloads.example.com/jizhi-7.apk',
|
|
releaseNotes: '',
|
|
sha256: null,
|
|
fileSize: null,
|
|
publishedAt: null,
|
|
);
|
|
|
|
await expectLater(
|
|
UpdateDownloader().downloadAndVerify(unsafeRelease, onProgress: (_) {}),
|
|
throwsA(
|
|
isA<UpdateDownloadException>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
contains('SHA-256'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
test('UpdateApi 将 404 和 429 转换为稳定错误', () async {
|
|
for (final entry in {
|
|
404: UpdateFailureKind.unavailable,
|
|
429: UpdateFailureKind.rateLimited,
|
|
}.entries) {
|
|
final dio = Dio(BaseOptions(baseUrl: 'https://version.test'))
|
|
..httpClientAdapter = _FakeAdapter(statusCode: entry.key, body: '{}');
|
|
await expectLater(
|
|
UpdateApi(
|
|
dio: dio,
|
|
).check(platform: 'ios', channel: 'stable', currentBuild: 6),
|
|
throwsA(
|
|
isA<UpdateCheckException>().having(
|
|
(error) => error.kind,
|
|
'kind',
|
|
entry.value,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
testWidgets('普通更新面板展示说明并允许忽略', (tester) async {
|
|
var ignored = false;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.light,
|
|
home: Scaffold(
|
|
body: UpdatePromptSheet(
|
|
release: release,
|
|
forced: false,
|
|
onIgnore: () async => ignored = true,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('发现新版本'), findsOneWidget);
|
|
expect(find.textContaining('修复识别问题'), findsOneWidget);
|
|
expect(find.text('本次更新'), findsOneWidget);
|
|
expect(find.byType(MarkdownBody), findsOneWidget);
|
|
expect(find.text('忽略此版本'), findsOneWidget);
|
|
await tester.tap(find.text('忽略此版本'));
|
|
await tester.pump();
|
|
expect(ignored, isTrue);
|
|
});
|
|
|
|
testWidgets('强制更新面板没有忽略入口并适配深色主题', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.dark,
|
|
home: Scaffold(
|
|
body: UpdatePromptSheet(
|
|
release: release,
|
|
forced: true,
|
|
onIgnore: () async {},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('必须更新后继续使用'), findsOneWidget);
|
|
expect(find.text('忽略此版本'), findsNothing);
|
|
expect(find.text('立即更新'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
|
|
class _FakeAdapter implements HttpClientAdapter {
|
|
final int statusCode;
|
|
final String body;
|
|
RequestOptions? request;
|
|
int callCount = 0;
|
|
final Duration delay;
|
|
|
|
_FakeAdapter({
|
|
required this.statusCode,
|
|
required this.body,
|
|
this.delay = Duration.zero,
|
|
});
|
|
|
|
@override
|
|
Future<ResponseBody> fetch(
|
|
RequestOptions options,
|
|
Stream<Uint8List>? requestStream,
|
|
Future<void>? cancelFuture,
|
|
) async {
|
|
request = options;
|
|
callCount++;
|
|
if (delay > Duration.zero) await Future<void>.delayed(delay);
|
|
return ResponseBody.fromString(
|
|
body,
|
|
statusCode,
|
|
headers: {
|
|
Headers.contentTypeHeader: ['application/json'],
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void close({bool force = false}) {}
|
|
}
|