From 05efad7d89dba8db86b0bf1b4632af18b7d96d38 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Sat, 17 Aug 2024 13:27:19 +0200 Subject: [PATCH] fixup!: Signed-off-by: Nikolas Rimikis --- .../account_repository/analysis_options.yaml | 1 - .../lib/src/account_repository.dart | 38 +- .../lib/src/models/account.dart | 1 - .../lib/src/utils/authentication_client.dart | 2 +- .../packages/account_repository/pubspec.yaml | 1 + .../test/account_repository_test.dart | 357 +++++++++--------- .../test/account_storage_test.dart | 50 +-- .../test/models/credentials_test.dart | 10 +- 8 files changed, 238 insertions(+), 222 deletions(-) diff --git a/packages/neon_framework/packages/account_repository/analysis_options.yaml b/packages/neon_framework/packages/account_repository/analysis_options.yaml index f0a42286945..bff1b129f3c 100644 --- a/packages/neon_framework/packages/account_repository/analysis_options.yaml +++ b/packages/neon_framework/packages/account_repository/analysis_options.yaml @@ -3,4 +3,3 @@ include: package:neon_lints/dart.yaml custom_lint: rules: - avoid_exports: false - - avoid_dart_io: false diff --git a/packages/neon_framework/packages/account_repository/lib/src/account_repository.dart b/packages/neon_framework/packages/account_repository/lib/src/account_repository.dart index 13017367f90..28e7fcf3f7a 100644 --- a/packages/neon_framework/packages/account_repository/lib/src/account_repository.dart +++ b/packages/neon_framework/packages/account_repository/lib/src/account_repository.dart @@ -87,18 +87,17 @@ class AccountRepository { final http.Client _httpClient; final AccountStorage _storage; - (NextcloudClient, core.ClientFlowLoginV2PollRequestApplicationJson)? _flow; - final BehaviorSubject<({String? active, BuiltMap accounts})> _accounts = BehaviorSubject.seeded((active: null, accounts: BuiltMap())); /// A stream of account information. /// /// The initial state of the stream will be `(active: null, accounts: BuiltMap())`. - Stream<({String? active, BuiltList accounts})> get accounts => _accounts.stream.map((e) { + Stream<({Account? active, BuiltList accounts})> get accounts => _accounts.stream.map((e) { + final active = e.accounts[e.active]; final accounts = e.accounts.values.toBuiltList(); - return (active: e.active, accounts: accounts); + return (active: active, accounts: accounts); }).asBroadcastStream(); /// Whether accounts are logged in. @@ -157,7 +156,6 @@ class AccountRepository { ); } - // TODO: this doesn't really belong here. /// Fetches the status of the server. /// /// May throw a [FetchStatusFailure]. @@ -179,7 +177,7 @@ class AccountRepository { /// Initializes the Nextcloud login flow. /// /// May throw a [InitLoginFailure]. - Future loginFlowInit(Uri serverURL) async { + Future<(Uri loginUrl, String token)> loginFlowInit(Uri serverURL) async { final client = buildUnAuthenticatedClient( httpClient: _httpClient, userAgent: _userAgent, @@ -189,35 +187,34 @@ class AccountRepository { try { final initResponse = await client.authentication.clientFlowLoginV2.init(); - final pollBody = core.ClientFlowLoginV2PollRequestApplicationJson((b) { - b.token = initResponse.body.poll.token; - }); - _flow = (client, pollBody); + final loginUrl = Uri.parse(initResponse.body.login); + final token = initResponse.body.poll.token; - return Uri.parse(initResponse.body.login); + return (loginUrl, token); } on http.ClientException catch (error, stackTrace) { Error.throwWithStackTrace(InitLoginFailure(error), stackTrace); } } - // TODO: update docs /// Polls the login flow endpoint and returns the retrieved credentials. /// - /// Throws a [StateError] if the login flow has not been initialized with [loginFlowInit]. /// Throws a [PollLoginFailure] when polling the endpoint fails. The flow must be initialized /// again if a failure occurs. - Future loginFlowPoll() async { - final flow = _flow; - if (flow == null) { - throw StateError('No login flow initialized'); - } - final (client, body) = flow; + Future loginFlowPoll(Uri serverURL, String token) async { + final client = buildUnAuthenticatedClient( + httpClient: _httpClient, + userAgent: _userAgent, + serverURL: serverURL, + ); + + final body = core.ClientFlowLoginV2PollRequestApplicationJson((b) { + b.token = token; + }); try { final resultResponse = await client.authentication.clientFlowLoginV2.poll($body: body); final response = resultResponse.body; - _flow = null; return Credentials((b) { b ..serverURL = Uri.parse(response.server) @@ -229,7 +226,6 @@ class AccountRepository { return null; } - _flow = null; Error.throwWithStackTrace(PollLoginFailure(error), stackTrace); } } diff --git a/packages/neon_framework/packages/account_repository/lib/src/models/account.dart b/packages/neon_framework/packages/account_repository/lib/src/models/account.dart index 6854cdb0fe3..25faa3703d8 100644 --- a/packages/neon_framework/packages/account_repository/lib/src/models/account.dart +++ b/packages/neon_framework/packages/account_repository/lib/src/models/account.dart @@ -20,7 +20,6 @@ abstract class Account implements Built { String get username => credentials.username; /// App password. - String? get password => credentials.password; /// The unique ID of the account. diff --git a/packages/neon_framework/packages/account_repository/lib/src/utils/authentication_client.dart b/packages/neon_framework/packages/account_repository/lib/src/utils/authentication_client.dart index 14ff1f0bd1e..6e0a6073764 100644 --- a/packages/neon_framework/packages/account_repository/lib/src/utils/authentication_client.dart +++ b/packages/neon_framework/packages/account_repository/lib/src/utils/authentication_client.dart @@ -5,7 +5,7 @@ import 'package:nextcloud/core.dart' as nc_core; import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/provisioning_api.dart' as nc_provisioning_api; -/// A AuthenticationClient client mock for testing. +/// An AuthenticationClient client mock for testing. @internal @visibleForTesting AuthenticationClient? mockedClient; diff --git a/packages/neon_framework/packages/account_repository/pubspec.yaml b/packages/neon_framework/packages/account_repository/pubspec.yaml index fdffa4f96b2..ab5ba2d2225 100644 --- a/packages/neon_framework/packages/account_repository/pubspec.yaml +++ b/packages/neon_framework/packages/account_repository/pubspec.yaml @@ -5,6 +5,7 @@ publish_to: none environment: sdk: ^3.0.0 + flutter: ^3.22.0 dependencies: built_collection: ^5.1.1 diff --git a/packages/neon_framework/packages/account_repository/test/account_repository_test.dart b/packages/neon_framework/packages/account_repository/test/account_repository_test.dart index 0ec370d1226..a83fb30c7f3 100644 --- a/packages/neon_framework/packages/account_repository/test/account_repository_test.dart +++ b/packages/neon_framework/packages/account_repository/test/account_repository_test.dart @@ -7,43 +7,50 @@ import 'package:built_value_test/matcher.dart'; import 'package:http/http.dart' as http; import 'package:mocktail/mocktail.dart'; import 'package:neon_framework/testing.dart' show MockNeonStorage; -import 'package:nextcloud/core.dart' as nc_core; +import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/nextcloud.dart'; -import 'package:nextcloud/provisioning_api.dart' as nc_provisioning_api; +import 'package:nextcloud/provisioning_api.dart' as provisioning_api; import 'package:test/test.dart'; -class _FakeStatus extends Fake implements nc_core.Status {} +class _FakeStatus extends Fake implements core.Status {} class _FakeUri extends Fake implements Uri {} class _FakeClient extends Fake implements http.Client {} -class _FakePollRequest extends Fake implements nc_core.ClientFlowLoginV2PollRequestApplicationJson {} +class _FakePollRequest extends Fake implements core.ClientFlowLoginV2PollRequestApplicationJson {} class _FakeNextcloudClient extends Fake implements NextcloudClient {} class _DynamiteResponseMock extends Mock implements DynamiteResponse {} -class _CoreClientMock extends Mock implements nc_core.$Client {} +class _CurrentUserResponseMock extends Mock implements provisioning_api.UsersGetCurrentUserResponseApplicationJson {} -class _AppPasswordClientMock extends Mock implements nc_core.$AppPasswordClient {} +class _CurrentUserResponseOcsMock extends Mock + implements provisioning_api.UsersGetCurrentUserResponseApplicationJson_Ocs {} -class _ClientFlowLoginV2ClientMock extends Mock implements nc_core.$ClientFlowLoginV2Client {} +class _UserDetailsMock extends Mock implements provisioning_api.UserDetails {} -class _UsersClientMock extends Mock implements nc_provisioning_api.$UsersClient {} +class _CoreClientMock extends Mock implements core.$Client {} + +class _AppPasswordClientMock extends Mock implements core.$AppPasswordClient {} + +class _ClientFlowLoginV2ClientMock extends Mock implements core.$ClientFlowLoginV2Client {} + +class _UsersClientMock extends Mock implements provisioning_api.$UsersClient {} class _AccountStorageMock extends Mock implements AccountStorage {} -typedef _AccountStream = ({BuiltList accounts, String? active}); +typedef _AccountStream = ({BuiltList accounts, Account? active}); void main() { late AccountStorage storage; late AccountRepository repository; - late nc_core.$Client core; - late nc_core.$AppPasswordClient appPassword; - late nc_core.$ClientFlowLoginV2Client clientFlowLoginV2; - late nc_provisioning_api.$UsersClient users; + late core.$Client coreClient; + late core.$AppPasswordClient appPassword; + late core.$ClientFlowLoginV2Client clientFlowLoginV2; + late provisioning_api.$UsersClient users; setUpAll(() { registerFallbackValue(_FakeUri()); @@ -52,13 +59,13 @@ void main() { }); setUp(() { - core = _CoreClientMock(); + coreClient = _CoreClientMock(); appPassword = _AppPasswordClientMock(); clientFlowLoginV2 = _ClientFlowLoginV2ClientMock(); users = _UsersClientMock(); mockedClient = AuthenticationClient( - core: core, + core: coreClient, appPassword: appPassword, clientFlowLoginV2: clientFlowLoginV2, users: users, @@ -73,7 +80,7 @@ void main() { ); }); - final accountList = BuiltList([ + final credentialsList = BuiltList([ Credentials((b) { b ..serverURL = Uri.https('serverUrl') @@ -91,8 +98,28 @@ void main() { group('AccountRepository', () { test('failure equality', () { expect( - FetchStatusFailure(accountList), - equals(FetchStatusFailure(accountList)), + FetchStatusFailure(credentialsList), + equals(FetchStatusFailure(credentialsList)), + ); + + expect( + InitLoginFailure(credentialsList), + equals(InitLoginFailure(credentialsList)), + ); + + expect( + PollLoginFailure(credentialsList), + equals(PollLoginFailure(credentialsList)), + ); + + expect( + FetchAccountFailure(credentialsList), + equals(FetchAccountFailure(credentialsList)), + ); + + expect( + DeleteCredentialsFailure(credentialsList), + equals(DeleteCredentialsFailure(credentialsList)), ); }); @@ -106,23 +133,25 @@ void main() { }); test('hasAccounts', () async { + expect(repository.hasAccounts, isFalse); + when(() => storage.readAccounts()).thenAnswer((_) async => BuiltList()); await repository.loadAccounts(); expect(repository.hasAccounts, isFalse); - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); await repository.loadAccounts(); expect(repository.hasAccounts, isTrue); }); test('accountByID', () async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); await repository.loadAccounts(); expect(repository.accountByID('invalid'), isNull); - expect(repository.accountByID('c73bc81eda746a735de697a0f0cbdb08f96ea3f3'), isNotNull); + expect(repository.accountByID(credentialsList.first.id), isNotNull); }); test('isLogInQRCode', () { @@ -155,15 +184,15 @@ void main() { }); test('emits stored credentials in accounts', () async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); await repository.loadAccounts(); expect( repository.accounts, emits( isA<_AccountStream>() - .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(accountList)) - .having((e) => e.active, 'active', equals('b0682d652840ef50a4115cc77109bedd8c577ccc')), + .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(credentialsList)) + .having((e) => e.active?.credentials, 'active', equals(credentialsList.first)), ), ); @@ -171,15 +200,15 @@ void main() { }); test('uses initialAccount as active', () async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); - await repository.loadAccounts(initialAccount: 'c73bc81eda746a735de697a0f0cbdb08f96ea3f3'); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); + await repository.loadAccounts(initialAccount: credentialsList[1].id); expect( repository.accounts, emits( isA<_AccountStream>() - .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(accountList)) - .having((e) => e.active, 'active', equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')), + .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(credentialsList)) + .having((e) => e.active?.credentials, 'active', equals(credentialsList[1])), ), ); @@ -187,15 +216,15 @@ void main() { }); test('ignores initialAccount when not it accounts', () async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); await repository.loadAccounts(initialAccount: 'invalid'); expect( repository.accounts, emits( isA<_AccountStream>() - .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(accountList)) - .having((e) => e.active, 'active', equals('b0682d652840ef50a4115cc77109bedd8c577ccc')), + .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(credentialsList)) + .having((e) => e.active?.credentials, 'active', equals(credentialsList.first)), ), ); @@ -203,22 +232,24 @@ void main() { }); test('uses last active account when specified', () async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); - when(() => storage.readLastAccount()).thenAnswer((_) async => 'c73bc81eda746a735de697a0f0cbdb08f96ea3f3'); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); + when(() => storage.readLastAccount()).thenAnswer((_) async => credentialsList[1].id); await repository.loadAccounts(rememberLastUsedAccount: true); expect( repository.accounts, emits( isA<_AccountStream>() - .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(accountList)) - .having((e) => e.active, 'active', equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')), + .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(credentialsList)) + .having((e) => e.active?.credentials, 'active', equals(credentialsList[1])), ), ); + + verify(() => storage.readLastAccount()).called(1); }); test('ignores last active account when not in accounts', () async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); when(() => storage.readLastAccount()).thenAnswer((_) async => 'invalid'); await repository.loadAccounts(rememberLastUsedAccount: true); @@ -226,75 +257,67 @@ void main() { repository.accounts, emits( isA<_AccountStream>() - .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(accountList)) - .having((e) => e.active, 'active', equals('b0682d652840ef50a4115cc77109bedd8c577ccc')), + .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(credentialsList)) + .having((e) => e.active?.credentials, 'active', equals(credentialsList.first)), ), ); + + verify(() => storage.readLastAccount()).called(1); }); test('prefers last active account over initial account', () async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); - when(() => storage.readLastAccount()).thenAnswer((_) async => 'c73bc81eda746a735de697a0f0cbdb08f96ea3f3'); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); + when(() => storage.readLastAccount()).thenAnswer((_) async => credentialsList[1].id); await repository.loadAccounts( rememberLastUsedAccount: true, - initialAccount: 'b0682d652840ef50a4115cc77109bedd8c577ccc', + initialAccount: credentialsList.first.id, ); expect( repository.accounts, emits( isA<_AccountStream>() - .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(accountList)) - .having((e) => e.active, 'active', equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')), + .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(credentialsList)) + .having((e) => e.active?.credentials, 'active', equals(credentialsList[1])), ), ); + + verify(() => storage.readLastAccount()).called(1); }); }); group('getServerStatus', () { test('removes active account', () async { - final mockResponse = _DynamiteResponseMock(); - when(() => core.getStatus()).thenAnswer((_) async => mockResponse); + final mockResponse = _DynamiteResponseMock(); + when(() => coreClient.getStatus()).thenAnswer((_) async => mockResponse); when(() => mockResponse.body).thenReturn(_FakeStatus()); expect( repository.getServerStatus(Uri()), - completion(isA()), + completion(isA()), ); + + verify(() => coreClient.getStatus()).called(1); }); test('rethrows http exceptions as `FetchStatusFailure`', () async { - when(() => core.getStatus()).thenThrow(http.ClientException('')); + when(() => coreClient.getStatus()).thenThrow(http.ClientException('')); await expectLater( repository.getServerStatus(Uri()), throwsA(isA().having((e) => e.error, 'error', isA())), ); - }); - }); - - group('loginFlow', () { - test('loginFlowPoll throws StateError when uninitialized', () async { - expect( - repository.loginFlowPoll(), - throwsStateError, - ); - }); - test('loginFlowInit rethrows http exceptions as `InitLoginFailure`', () async { - when(() => clientFlowLoginV2.init()).thenThrow(http.ClientException('')); - - expect( - repository.loginFlowInit(Uri()), - throwsA(isA().having((e) => e.error, 'error', isA())), - ); + verify(() => coreClient.getStatus()).called(1); }); + }); - test('initializes and polls loginFlow', () async { + group('loginFlowInit', () { + test('returns login endpoint and token', () async { when(() => clientFlowLoginV2.init()).thenAnswer((_) async { return DynamiteResponse( 200, - nc_core.LoginFlowV2((b) { + core.LoginFlowV2((b) { b.login = 'https://login_url'; b.poll ..token = 'token' @@ -306,21 +329,30 @@ void main() { await expectLater( repository.loginFlowInit(Uri()), - completion(equals(Uri.https('login_url'))), + completion(equals((Uri.https('login_url'), 'token'))), ); - when(() => clientFlowLoginV2.poll($body: any(named: r'$body'))) - .thenThrow(DynamiteStatusCodeException(http.Response('', 404))); + verify(() => clientFlowLoginV2.init()).called(1); + }); - await expectLater( - repository.loginFlowPoll(), - completion(isNull), + test('rethrows http exceptions as `InitLoginFailure`', () async { + when(() => clientFlowLoginV2.init()).thenThrow(http.ClientException('')); + + expect( + repository.loginFlowInit(Uri()), + throwsA(isA().having((e) => e.error, 'error', isA())), ); + verify(() => clientFlowLoginV2.init()).called(1); + }); + }); + + group('loginFlowPoll', () { + test('returns fetched credentials', () async { when(() => clientFlowLoginV2.poll($body: any(named: r'$body'))).thenAnswer((_) async { return DynamiteResponse( 200, - nc_core.LoginFlowV2Credentials((b) { + core.LoginFlowV2Credentials((b) { b ..appPassword = 'appPassword' ..loginName = 'loginName' @@ -331,7 +363,7 @@ void main() { }); expect( - repository.loginFlowPoll(), + repository.loginFlowPoll(Uri(), 'token'), completion( equalsBuilt( Credentials((b) { @@ -343,106 +375,85 @@ void main() { ), ), ); + + verify( + () => clientFlowLoginV2.poll( + $body: any( + named: r'$body', + that: equalsBuilt(core.ClientFlowLoginV2PollRequestApplicationJson((b) => b.token = 'token')), + ), + ), + ).called(1); }); - test('loginFlowPoll rethrows http exceptions as `PollLoginFailure`', () async { - when(() => clientFlowLoginV2.init()).thenAnswer((_) async { - return DynamiteResponse( - 200, - nc_core.LoginFlowV2((b) { - b.login = 'login_url'; - b.poll - ..token = 'token' - ..endpoint = 'token'; - }), - null, - ); - }); - await repository.loginFlowInit(Uri()); + test('returns null when for 404 response credentials', () async { + when(() => clientFlowLoginV2.poll($body: any(named: r'$body'))) + .thenThrow(DynamiteStatusCodeException(http.Response('', 404))); + + await expectLater( + repository.loginFlowPoll(Uri(), 'token'), + completion(isNull), + ); + + verify( + () => clientFlowLoginV2.poll( + $body: any( + named: r'$body', + that: equalsBuilt(core.ClientFlowLoginV2PollRequestApplicationJson((b) => b.token = 'token')), + ), + ), + ).called(1); + }); + + test('rethrows http exceptions as `PollLoginFailure`', () async { when(() => clientFlowLoginV2.poll($body: any(named: r'$body'))).thenThrow(http.ClientException('')); await expectLater( - repository.loginFlowPoll(), + repository.loginFlowPoll(Uri(), 'token'), throwsA(isA().having((e) => e.error, 'error', isA())), ); + + verify( + () => clientFlowLoginV2.poll( + $body: any( + named: r'$body', + that: equalsBuilt(core.ClientFlowLoginV2PollRequestApplicationJson((b) => b.token = 'token')), + ), + ), + ).called(1); }); }); group('getAccount', () { test('retrieves account id from server', () async { - when(() => users.getCurrentUser()).thenAnswer( - (_) async => DynamiteResponse( - 200, - nc_provisioning_api.UsersGetCurrentUserResponseApplicationJson.fromJson({ - 'ocs': { - 'meta': {'status': 'ok', 'statuscode': 200, 'message': 'OK'}, - 'data': { - 'additional_mail': [], - 'address': '', - 'backend': 'Database', - 'backendCapabilities': {'setDisplayName': true, 'setPassword': true}, - 'biography': '', - 'display-name': 'admin', - 'displayname': 'admin', - 'fediverse': '', - 'groups': ['admin'], - 'headline': '', - 'id': 'admin', - 'language': 'en', - 'lastLogin': 1723124410000, - 'locale': '', - 'manager': '', - 'organisation': '', - 'phone': '', - 'profile_enabled': '1', - 'quota': { - 'free': 1629916880896, - 'relative': 0, - 'total': 1629956170522, - 'used': 39289626, - 'quota': -3, - }, - 'role': '', - 'subadmin': [], - 'twitter': '', - 'website': '', - 'additional_mailScope': [], - 'addressScope': 'v2-local', - 'avatarScope': 'v2-federated', - 'biographyScope': 'v2-local', - 'displaynameScope': 'v2-federated', - 'email': 'admin@example.com', - 'emailScope': 'v2-federated', - 'enabled': true, - 'fediverseScope': 'v2-local', - 'headlineScope': 'v2-local', - 'organisationScope': 'v2-local', - 'phoneScope': 'v2-local', - 'profile_enabledScope': 'v2-local', - 'roleScope': 'v2-local', - 'storageLocation': '/usr/src/nextcloud/data/admin', - 'twitterScope': 'v2-local', - 'websiteScope': 'v2-local', - }, - }, - }), - null, - ), - ); + final userDetails = _UserDetailsMock(); + when(() => userDetails.id).thenReturn('admin'); + final ocs = _CurrentUserResponseOcsMock(); + when(() => ocs.data).thenReturn(userDetails); + final userStatusResponse = _CurrentUserResponseMock(); + when(() => userStatusResponse.ocs).thenReturn(ocs); + final response = _DynamiteResponseMock<_CurrentUserResponseMock, void>(); + when(() => response.body).thenReturn(userStatusResponse); + + when(() => users.getCurrentUser()).thenAnswer((_) async => response); expect( - repository.getAccount(accountList.first), + repository.getAccount(credentialsList.first), completion(isA().having((e) => e.username, 'username', equals('admin'))), ); + + verify(() => users.getCurrentUser()).called(1); }); test('rethrows http exceptions as `FetchAccountFailure`', () async { when(() => users.getCurrentUser()).thenThrow(http.ClientException('')); await expectLater( - repository.getAccount(accountList.first), + repository.getAccount(credentialsList.first), throwsA(isA().having((e) => e.error, 'error', isA())), ); + + verify(() => users.getCurrentUser()).called(1); }); }); @@ -454,7 +465,7 @@ void main() { final account = Account((b) { b - ..credentials.replace(accountList.first) + ..credentials.replace(credentialsList.first) ..client = _FakeNextcloudClient(); }); @@ -466,46 +477,50 @@ void main() { emits( isA<_AccountStream>() .having((e) => e.accounts, 'accounts', hasLength(1)) - .having((e) => e.active, 'active', equals('b0682d652840ef50a4115cc77109bedd8c577ccc')), + .having((e) => e.active?.credentials, 'active', equals(credentialsList.first)), ), ); - verify(() => storage.saveAccounts(any(that: contains(accountList.first)))).called(1); - verify(() => storage.saveLastAccount(any(that: equals('b0682d652840ef50a4115cc77109bedd8c577ccc')))).called(1); + verify(() => storage.saveAccounts(any(that: contains(credentialsList.first)))).called(1); + verify(() => storage.saveLastAccount(credentialsList.first.id)).called(1); }); }); group('logOut', () { setUp(() async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); when(() => storage.saveLastAccount(any())).thenAnswer((_) async => {}); when(() => storage.saveAccounts(any())).thenAnswer((_) async => {}); await repository.loadAccounts(); + + resetMocktailState(); }); test('removes active account', () async { when(() => appPassword.deleteAppPassword()).thenAnswer( - (_) async => _DynamiteResponseMock(), + (_) async => _DynamiteResponseMock(), ); - await repository.logOut('b0682d652840ef50a4115cc77109bedd8c577ccc'); + await repository.logOut(credentialsList.first.id); expect( repository.accounts, emits( isA<_AccountStream>() .having((e) => e.accounts, 'accounts', hasLength(1)) - .having((e) => e.active, 'active', equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')), + .having((e) => e.active?.credentials, 'active', equals(credentialsList[1])), ), ); - verify(() => storage.saveLastAccount(any(that: equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')))).called(1); - verify(() => storage.saveAccounts(any(that: equals([accountList[1]])))).called(1); + + verify(() => appPassword.deleteAppPassword()).called(1); + verify(() => storage.saveLastAccount(credentialsList[1].id)).called(1); + verify(() => storage.saveAccounts(any(that: equals([credentialsList[1]])))).called(1); }); test('rethrows http exceptions as `DeleteCredentialsFailure`', () async { when(() => appPassword.deleteAppPassword()).thenThrow(http.ClientException('')); await expectLater( - repository.logOut('b0682d652840ef50a4115cc77109bedd8c577ccc'), + repository.logOut(credentialsList.first.id), throwsA(isA().having((e) => e.error, 'error', isA())), ); @@ -514,11 +529,13 @@ void main() { emits( isA<_AccountStream>() .having((e) => e.accounts, 'accounts', hasLength(1)) - .having((e) => e.active, 'active', equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')), + .having((e) => e.active?.credentials, 'active', equals(credentialsList[1])), ), ); - verify(() => storage.saveLastAccount(any(that: equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')))).called(1); - verify(() => storage.saveAccounts(any(that: equals([accountList[1]])))).called(1); + + verify(() => appPassword.deleteAppPassword()).called(1); + verify(() => storage.saveLastAccount(credentialsList[1].id)).called(1); + verify(() => storage.saveAccounts(any(that: equals([credentialsList[1]])))).called(1); }); }); @@ -533,8 +550,10 @@ void main() { group('switchAccount', () { setUp(() async { - when(() => storage.readAccounts()).thenAnswer((_) async => accountList); + when(() => storage.readAccounts()).thenAnswer((_) async => credentialsList); await repository.loadAccounts(); + + resetMocktailState(); }); test('throws StateError for unregistered account ids', () async { @@ -546,18 +565,18 @@ void main() { test('emits and saves active account ', () async { when(() => storage.saveLastAccount(any())).thenAnswer((_) async => {}); - await repository.switchAccount('c73bc81eda746a735de697a0f0cbdb08f96ea3f3'); + await repository.switchAccount(credentialsList[1].id); expect( repository.accounts, emits( isA<_AccountStream>() - .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(accountList)) - .having((e) => e.active, 'active', equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')), + .having((e) => e.accounts.map((e) => e.credentials), 'accounts', containsAllInOrder(credentialsList)) + .having((e) => e.active?.credentials, 'active', equals(credentialsList[1])), ), ); - verify(() => storage.saveLastAccount(any(that: equals('c73bc81eda746a735de697a0f0cbdb08f96ea3f3')))).called(1); + verify(() => storage.saveLastAccount(credentialsList[1].id)).called(1); }); }); }); diff --git a/packages/neon_framework/packages/account_repository/test/account_storage_test.dart b/packages/neon_framework/packages/account_repository/test/account_storage_test.dart index 81474986514..8cbd26aec8a 100644 --- a/packages/neon_framework/packages/account_repository/test/account_storage_test.dart +++ b/packages/neon_framework/packages/account_repository/test/account_storage_test.dart @@ -10,23 +10,23 @@ class _FakeBuiltList extends Fake implements BuiltList {} class _SingleValueStoreMock extends Mock implements SingleValueStore {} void main() { - late SingleValueStore accounts; - late SingleValueStore lastAccount; + late SingleValueStore accountsStore; + late SingleValueStore lastAccountStore; late AccountStorage storage; setUp(() { registerFallbackValue(_FakeBuiltList()); - accounts = _SingleValueStoreMock(); - lastAccount = _SingleValueStoreMock(); + accountsStore = _SingleValueStoreMock(); + lastAccountStore = _SingleValueStoreMock(); storage = AccountStorage( - accountsPersistence: accounts, - lastAccountPersistence: lastAccount, + accountsPersistence: accountsStore, + lastAccountPersistence: lastAccountStore, ); }); - final accountList = [ + final credentialsList = [ Credentials((b) { b ..serverURL = Uri.https('serverUrl') @@ -41,7 +41,7 @@ void main() { }), ]; - final serializedAccounts = BuiltList([ + final serializedCredentials = BuiltList([ '{"serverURL":"https://serverurl","username":"username","password":"password"}', '{"serverURL":"https://other-serverurl","username":"username","password":"password"}', ]); @@ -49,78 +49,78 @@ void main() { group('AccountStorage', () { group('readAccounts', () { test('returns empty list when no value is stored', () async { - when(() => accounts.hasValue()).thenReturn(false); + when(() => accountsStore.hasValue()).thenReturn(false); expect( storage.readAccounts(), completion(isEmpty), ); - verifyNever(() => accounts.getStringList()); + verifyNever(() => accountsStore.getStringList()); }); test('returns list with deserialized accounts', () async { - when(() => accounts.hasValue()).thenReturn(true); - when(() => accounts.getStringList()).thenReturn(serializedAccounts); + when(() => accountsStore.hasValue()).thenReturn(true); + when(() => accountsStore.getStringList()).thenReturn(serializedCredentials); expect( storage.readAccounts(), - completion(accountList), + completion(credentialsList), ); - verify(() => accounts.getStringList()).called(1); + verify(() => accountsStore.getStringList()).called(1); }); }); group('saveAccounts', () { test('persists accounts to storage', () async { - when(() => accounts.setStringList(any())).thenAnswer((_) async => true); + when(() => accountsStore.setStringList(any())).thenAnswer((_) async => true); - await storage.saveAccounts(accountList); + await storage.saveAccounts(credentialsList); - verify(() => accounts.setStringList(any(that: equals(serializedAccounts)))).called(1); + verify(() => accountsStore.setStringList(any(that: equals(serializedCredentials)))).called(1); }); }); group('readLastAccount', () { test('returns null when no value is stored', () async { - when(() => lastAccount.getString()).thenReturn(null); + when(() => lastAccountStore.getString()).thenReturn(null); expect( storage.readLastAccount(), completion(isNull), ); - verify(() => lastAccount.getString()).called(1); + verify(() => lastAccountStore.getString()).called(1); }); test('returns account id for the stored value', () async { - when(() => lastAccount.getString()).thenReturn('accountID'); + when(() => lastAccountStore.getString()).thenReturn('accountID'); expect( storage.readLastAccount(), completion('accountID'), ); - verify(() => lastAccount.getString()).called(1); + verify(() => lastAccountStore.getString()).called(1); }); }); group('saveLastAccount', () { test('persists account id to disk', () async { - when(() => lastAccount.setString(any())).thenAnswer((_) async => true); + when(() => lastAccountStore.setString(any())).thenAnswer((_) async => true); await storage.saveLastAccount('accountID'); - verify(() => lastAccount.setString(any(that: equals('accountID')))).called(1); + verify(() => lastAccountStore.setString('accountID')).called(1); }); test('deletes last account when id is null', () async { - when(() => lastAccount.remove()).thenAnswer((_) async => true); + when(() => lastAccountStore.remove()).thenAnswer((_) async => true); await storage.saveLastAccount(null); - verify(() => lastAccount.remove()).called(1); + verify(() => lastAccountStore.remove()).called(1); }); }); }); diff --git a/packages/neon_framework/packages/account_repository/test/models/credentials_test.dart b/packages/neon_framework/packages/account_repository/test/models/credentials_test.dart index d0672d5a3f1..82dbf3cf0f8 100644 --- a/packages/neon_framework/packages/account_repository/test/models/credentials_test.dart +++ b/packages/neon_framework/packages/account_repository/test/models/credentials_test.dart @@ -78,11 +78,13 @@ void main() { }); test('creates humanReadableID', () { + final credentials = createCredentials( + serverURL: Uri.https('example.com'), + username: 'JohnDoe', + ); + expect( - createCredentials( - serverURL: Uri.https('example.com'), - username: 'JohnDoe', - ).humanReadableID, + credentials.humanReadableID, 'JohnDoe@example.com', );