import 'dart:async'; import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/services.dart' show MethodCall, MethodChannel; import 'package:hepan_everyday/services/app_update_service.dart'; class TestUpdateTransport implements UpdateTransport { int calls = 0; Uri? endpoint; Completer? pending; UpdateHttpResponse response = UpdateHttpResponse( 200, utf8.encode( jsonEncode({ 'schema_version': 1, 'application_id': 'com.tpschedule.app', 'latest': { 'version': '0.8.0', 'build_number': 25, 'notes': '新增更新检查\n修复导入', 'download_url': 'https://app.glowtps.cn/downloads/TPSchedule.apk', }, }), ), ); @override Future get(Uri endpoint) async { calls++; this.endpoint = endpoint; return pending == null ? response : pending!.future; } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('browser channel receives only validated HTTPS links', () async { final calls = []; const channel = MethodChannel('com.tpschedule.app/updates'); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (call) async { calls.add(call); return true; }); addTearDown( () => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, null), ); expect( await openUpdateInBrowser(Uri.parse('javascript:alert(1)')), isFalse, ); expect( await openUpdateInBrowser( Uri.parse('https://name:password@example.com/app.apk'), ), isFalse, ); expect(calls, isEmpty); expect( await openUpdateInBrowser(Uri.parse('https://example.com/app.apk')), isTrue, ); expect(calls.single.method, 'openDownload'); expect(calls.single.arguments, {'url': 'https://example.com/app.apk'}); }); test( 'build number determines updates and displayed notes and URL survive', () async { final transport = TestUpdateTransport(); final client = AppUpdateService(transport: transport, currentBuild: 24); final release = await client.check(); expect(release!.version, '0.8.0'); expect(release.notes, '新增更新检查\n修复导入'); expect( release.downloadUri.toString(), 'https://app.glowtps.cn/downloads/TPSchedule.apk', ); expect( transport.endpoint.toString(), 'https://app.glowtps.cn/v1/updates/android', ); expect( await AppUpdateService(transport: transport, currentBuild: 25).check(), isNull, ); expect( await AppUpdateService(transport: transport, currentBuild: 26).check(), isNull, ); }, ); test( 'simultaneous checks share a request and a later check refreshes', () async { final transport = TestUpdateTransport()..pending = Completer(); final service = AppUpdateService(transport: transport, currentBuild: 24); final a = service.check(), b = service.check(); await Future.value(); expect(transport.calls, 1); transport.pending!.complete(transport.response); expect((await a)!.buildNumber, 25); expect((await b)!.buildNumber, 25); transport.pending = null; await service.check(); expect(transport.calls, 2); }, ); test( 'unpublished server and equal versions are an ordinary no-update result', () async { final transport = TestUpdateTransport() ..response = UpdateHttpResponse( 200, utf8.encode( '{"schema_version":1,"application_id":"com.tpschedule.app","latest":null}', ), ); expect(await AppUpdateService(transport: transport).check(), isNull); }, ); test('bad identity, HTML response, unsafe URL and oversized responses fail honestly', () async { final transport = TestUpdateTransport(); final good = utf8.decode(transport.response.body); for (final body in [ good.replaceFirst('com.tpschedule.app', 'another.app'), 'WAF challenge', good.replaceFirst( 'https://app.glowtps.cn/downloads/TPSchedule.apk', 'javascript:alert(1)', ), good.replaceFirst( 'https://app.glowtps.cn/downloads/TPSchedule.apk', 'https://user:secret@example.com/app.apk', ), good.replaceFirst('"build_number":25', '"build_number":0'), good.replaceFirst('"notes":"', '"notes":123,"other":"'), 'x' * (64 * 1024 + 1), ]) { transport.response = UpdateHttpResponse(200, utf8.encode(body)); await expectLater( AppUpdateService(transport: transport).check(), throwsA(isA()), ); } transport.response = UpdateHttpResponse(503, utf8.encode('{}')); await expectLater( AppUpdateService(transport: transport).check(), throwsA(isA()), ); }); }