Initial project import
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
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}) {}
|
||||
}
|
||||
Reference in New Issue
Block a user