import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:ikar/backend/api.dart'; import 'package:ikar/backend/modules/account.dart'; import 'package:ikar/core/protocol/opcode_map.dart'; import 'package:ikar/core/protocol/packet.dart'; import 'package:ikar/core/storage/app_database.dart'; import 'package:ikar/core/storage/token_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; class _Paths extends PathProviderPlatform with MockPlatformInterfaceMixin { _Paths(this.directory); final String directory; @override Future getApplicationSupportPath() async => directory; } class _Api extends Api { final requests = <(int, Map)>[]; final responses = >{}; @override SessionState get state => SessionState.online; @override Stream get stateStream => const Stream.empty(); @override Future sendRequest( int opcode, Map payload, { void Function()? beforeSend, bool silent = false, }) async { beforeSend?.call(); requests.add((opcode, Map.of(payload))); return Packet( cmd: CmdType.ok, opcode: opcode, payload: responses[opcode] ?? {}, ); } } const _profile = { 'contact': { 'id': 42, 'names': [ {'type': 'ONEME', 'firstName': 'New', 'lastName': 'User'}, ], }, }; void main() { TestWidgetsFlutterBinding.ensureInitialized(); const secureChannel = MethodChannel( 'plugins.it_nomads.com/flutter_secure_storage', ); final secureValues = {}; late _Api api; late AccountModule account; setUp(() async { SharedPreferences.setMockInitialValues({}); secureValues.clear(); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(secureChannel, (call) async { final args = call.arguments as Map; final key = args['key'] as String; switch (call.method) { case 'write': secureValues[key] = args['value'] as String; return null; case 'read': return secureValues[key]; case 'delete': secureValues.remove(key); return null; } throw StateError('Unexpected storage method: ${call.method}'); }); final directory = Directory.systemTemp.createTempSync( 'qlyra_registration_test_', ); final originalPaths = PathProviderPlatform.instance; PathProviderPlatform.instance = _Paths(directory.path); addTearDown(() async { await AppDatabase.close(); PathProviderPlatform.instance = originalPaths; directory.deleteSync(recursive: true); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(secureChannel, null); }); await AppDatabase.init(); api = _Api(); addTearDown(api.dispose); account = AccountModule(api); }); test( 'new account: OTP, registration, first login and restored login', () async { api.responses[Opcode.auth] = { 'tokenAttrs': { 'REGISTER': {'token': 'temporary-registration-token'}, }, }; final verified = await account.verifyCode('123456', 'sms-token'); expect(verified.isRegistration, isTrue); expect(secureValues, isEmpty); expect(await TokenStorage.getActiveAccountId(), isNull); api.responses[Opcode.authConfirm] = { 'userToken': 42, 'profile': _profile, 'tokenType': 'LOGIN', 'token': 'registered-login-token', }; final id = await account.completeRegistration( token: verified.registerToken!, firstName: 'New', lastName: 'User', photoId: 7, ); expect(id, 42); expect(api.requests.last.$1, Opcode.authConfirm); expect(api.requests.last.$2, { 'token': 'temporary-registration-token', 'tokenType': 'REGISTER', 'firstName': 'New', 'lastName': 'User', 'photoId': 7, 'avatarType': 'PRESET_AVATAR', }); expect(await TokenStorage.readActiveToken(), 'registered-login-token'); expect((await AppDatabase.loadProfile(id))?.firstName, 'New'); api.responses[Opcode.login] = { 'profile': _profile, 'token': 'rotated-login-token', 'time': 1700000000000, }; final loggedIn = await account.login(accountId: id); expect(loggedIn.profile.id, id); expect(account.isLoggedIn, isTrue); expect( api.requests.firstWhere((r) => r.$1 == Opcode.login).$2['token'], 'registered-login-token', ); expect(await TokenStorage.readActiveToken(), 'rotated-login-token'); final restored = AccountModule(api); await restored.login(); expect( api.requests.lastWhere((r) => r.$1 == Opcode.login).$2['token'], 'rotated-login-token', ); expect(restored.isLoggedIn, isTrue); }, ); test( 'REGISTER token with account ID does not become an active session', () async { api.responses[Opcode.auth] = { 'profile': _profile, 'tokenAttrs': { 'REGISTER': {'token': 'registration-only'}, }, }; final result = await account.verifyCode('123456', 'sms-token'); expect(result.isRegistration, isTrue); expect(secureValues, isEmpty); expect(await TokenStorage.getActiveAccountId(), isNull); expect(await AppDatabase.loadAllProfiles(), isEmpty); }, ); test('existing account still persists LOGIN token after OTP', () async { api.responses[Opcode.auth] = { 'profile': _profile, 'tokenAttrs': { 'LOGIN': {'token': 'existing-login-token'}, }, }; final result = await account.verifyCode('123456', 'sms-token'); expect(result.isRegistration, isFalse); expect(await TokenStorage.readActiveToken(), 'existing-login-token'); expect((await AppDatabase.loadProfile(42))?.firstName, 'New'); }); for (final invalidToken in [null, '', ' ', 123]) { test('registration rejects invalid login token: $invalidToken', () async { api.responses[Opcode.authConfirm] = { 'profile': _profile, 'tokenType': 'LOGIN', 'token': invalidToken, }; await expectLater( account.completeRegistration(token: 'register', firstName: 'New'), throwsFormatException, ); expect(secureValues, isEmpty); expect(await TokenStorage.getActiveAccountId(), isNull); expect(await AppDatabase.loadAllProfiles(), isEmpty); }); } test( 'registration rejects a REGISTER token in the confirmation response', () async { api.responses[Opcode.authConfirm] = { 'profile': _profile, 'tokenType': 'REGISTER', 'token': 'temporary', }; await expectLater( account.completeRegistration(token: 'register', firstName: 'New'), throwsFormatException, ); expect(secureValues, isEmpty); expect(await TokenStorage.getActiveAccountId(), isNull); }, ); test( 'login rejects empty explicit and stored tokens before network request', () async { await expectLater( account.login(accountId: 42, token: ''), throwsStateError, ); await TokenStorage.saveToken(' ', 42); await expectLater(account.login(accountId: 42), throwsStateError); expect(api.requests, isEmpty); }, ); test('resend requests a new OTP token from the server', () async { api.responses[Opcode.authRequest] = {'token': 'fresh-sms-token'}; final result = await account.resendCode('+7 (999) 000-00-00'); expect(result.token, 'fresh-sms-token'); expect(api.requests.single.$1, Opcode.authRequest); expect(api.requests.single.$2, { 'phone': '+79990000000', 'type': 'RESEND', 'language': 'ru', }); }); test('empty LOGIN token does not hide a valid registration token', () { final result = VerifyCodeResult( payload: { 'tokenAttrs': { 'LOGIN': {'token': ''}, 'REGISTER': {'token': 'registration-only'}, }, }, ); expect(result.loginToken, isNull); expect(result.isRegistration, isTrue); }); }