Files
IKAR/test/account_registration_test.dart
T

269 lines
8.4 KiB
Dart

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<String?> getApplicationSupportPath() async => directory;
}
class _Api extends Api {
final requests = <(int, Map<dynamic, dynamic>)>[];
final responses = <int, Map<dynamic, dynamic>>{};
@override
SessionState get state => SessionState.online;
@override
Stream<SessionState> get stateStream => const Stream.empty();
@override
Future<Packet> sendRequest(
int opcode,
Map<dynamic, dynamic> 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] ?? <dynamic, dynamic>{},
);
}
}
const _profile = {
'contact': {
'id': 42,
'names': <dynamic>[
{'type': 'ONEME', 'firstName': 'New', 'lastName': 'User'},
],
},
};
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const secureChannel = MethodChannel(
'plugins.it_nomads.com/flutter_secure_storage',
);
final secureValues = <String, String>{};
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);
});
}