From 783daa2051f9814ed9ebcedc613409bb2357f024 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Tue, 19 Nov 2024 16:25:38 +0100 Subject: [PATCH] feat(account_repository): Implement remote wipe Signed-off-by: provokateurin --- .../lib/src/account_repository.dart | 65 +++++++++ .../lib/src/utils/authentication_client.dart | 4 + .../test/account_repository_test.dart | 136 ++++++++++++++++++ 3 files changed, 205 insertions(+) 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 1561e262113..c5649f434bd 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 @@ -70,6 +70,22 @@ final class DeleteCredentialsFailure extends AccountFailure { const DeleteCredentialsFailure(super.error); } +/// {@template get_remote_wipe_status_failure} +/// Thrown when getting the device remote wipe status fails. +/// {@endtemplate} +final class GetRemoteWipeStatusFailure extends AccountFailure { + /// {@macro get_remote_wipe_status_failure} + const GetRemoteWipeStatusFailure(super.error); +} + +/// {@template post_remote_wipe_success_failure} +/// Thrown when posting the device remote wipe success fails. +/// {@endtemplate} +final class PostRemoteWipeSuccessFailure extends AccountFailure { + /// {@macro post_remote_wipe_success_failure} + const PostRemoteWipeSuccessFailure(super.error); +} + /// {@template account_repository} /// A repository that manages the account data. /// {@endtemplate} @@ -321,4 +337,53 @@ class AccountRepository { _accounts.add((active: accountID, accounts: value.accounts)); await _storage.saveLastAccount(accountID); } + + /// Gets the device remote wipe status. + /// + /// May throw a [GetRemoteWipeStatusFailure]. + Future getRemoteWipeStatus(Account account) async { + final client = buildUnauthenticatedClient( + httpClient: _httpClient, + userAgent: _userAgent, + serverURL: account.credentials.serverURL, + ); + + try { + final response = await client.authentication.wipe.checkWipe( + $body: core.WipeCheckWipeRequestApplicationJson( + (b) => b..token = account.credentials.appPassword, + ), + ); + + // This is always true, as otherwise 404 is returned, but just to be safe in the future use the returned value. + return response.body.wipe; + } on http.ClientException catch (error, stackTrace) { + if (error case DynamiteStatusCodeException() when error.statusCode == 404) { + return false; + } + + Error.throwWithStackTrace(GetRemoteWipeStatusFailure(error), stackTrace); + } + } + + /// Posts the remote wipe success. + /// + /// May throw a [PostRemoteWipeSuccessFailure]. + Future postRemoteWipeSuccess(Account account) async { + final client = buildUnauthenticatedClient( + httpClient: _httpClient, + userAgent: _userAgent, + serverURL: account.credentials.serverURL, + ); + + try { + await client.authentication.wipe.wipeDone( + $body: core.WipeWipeDoneRequestApplicationJson( + (b) => b..token = account.credentials.appPassword, + ), + ); + } on http.ClientException catch (error, stackTrace) { + Error.throwWithStackTrace(PostRemoteWipeSuccessFailure(error), stackTrace); + } + } } 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 a17b579097e..8318971ba2d 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 @@ -16,6 +16,7 @@ class AuthenticationClient { required this.appPassword, required this.clientFlowLoginV2, required this.users, + required this.wipe, }); final $core.$Client core; @@ -25,6 +26,8 @@ class AuthenticationClient { final $core.$ClientFlowLoginV2Client clientFlowLoginV2; final $provisioning_api.$UsersClient users; + + final $core.$WipeClient wipe; } /// Extension for getting the [AuthenticationClient]. @@ -38,5 +41,6 @@ extension AuthenticationClientExtension on NextcloudClient { appPassword: core.appPassword, clientFlowLoginV2: core.clientFlowLoginV2, users: provisioningApi.users, + wipe: core.wipe, ); } 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 87557014c55..2f24cbd25e4 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 @@ -4,6 +4,7 @@ import 'package:account_repository/account_repository.dart'; import 'package:account_repository/src/testing/testing.dart'; import 'package:account_repository/src/utils/authentication_client.dart'; import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; import 'package:built_value_test/matcher.dart'; import 'package:http/http.dart' as http; import 'package:mocktail/mocktail.dart'; @@ -38,6 +39,14 @@ class _ClientFlowLoginV2ClientMock extends Mock implements core.$ClientFlowLogin class _UsersClientMock extends Mock implements provisioning_api.$UsersClient {} +class _WipeClientMock extends Mock implements core.$WipeClient {} + +class _WipeCheckResponseMock extends Mock implements core.WipeCheckWipeResponseApplicationJson {} + +class _FakeWipeCheckRequest extends Fake implements core.WipeCheckWipeRequestApplicationJson {} + +class _FakeWipeDoneRequest extends Fake implements core.WipeWipeDoneRequestApplicationJson {} + class _AccountStorageMock extends Mock implements AccountStorage {} typedef _AccountStream = ({BuiltList accounts, Account? active}); @@ -50,10 +59,13 @@ void main() { late core.$AppPasswordClient appPassword; late core.$ClientFlowLoginV2Client clientFlowLoginV2; late provisioning_api.$UsersClient users; + late core.$WipeClient wipe; setUpAll(() { registerFallbackValue(_FakeUri()); registerFallbackValue(_FakePollRequest()); + registerFallbackValue(_FakeWipeCheckRequest()); + registerFallbackValue(_FakeWipeDoneRequest()); MockNeonStorage(); }); @@ -62,12 +74,14 @@ void main() { appPassword = _AppPasswordClientMock(); clientFlowLoginV2 = _ClientFlowLoginV2ClientMock(); users = _UsersClientMock(); + wipe = _WipeClientMock(); mockedClient = AuthenticationClient( core: coreClient, appPassword: appPassword, clientFlowLoginV2: clientFlowLoginV2, users: users, + wipe: wipe, ); storage = _AccountStorageMock(); @@ -587,5 +601,127 @@ void main() { verify(() => storage.saveLastAccount(credentialsList[1].id)).called(1); }); }); + + group('getRemoteWipeStatus', () { + group('retrieves remote wipe status from server', () { + test('should wipe', () async { + final wipeCheckResponse = _WipeCheckResponseMock(); + when(() => wipeCheckResponse.wipe).thenReturn(true); + final response = _DynamiteResponseMock<_WipeCheckResponseMock, void>(); + when(() => response.body).thenReturn(wipeCheckResponse); + + when(() => wipe.checkWipe($body: any(named: r'$body'))).thenAnswer((_) async => response); + + await expectLater( + repository.getRemoteWipeStatus(accountsList.first), + completion(true), + ); + + verify( + () => wipe.checkWipe( + $body: any( + named: r'$body', + that: isA().having( + (b) => b.token, + 'token', + 'appPassword', + ), + ), + ), + ).called(1); + }); + + test('should not wipe', () async { + when(() => wipe.checkWipe($body: any(named: r'$body'))) + .thenThrow(DynamiteStatusCodeException(http.Response('', 404))); + + await expectLater( + repository.getRemoteWipeStatus(accountsList.first), + completion(false), + ); + + verify( + () => wipe.checkWipe( + $body: any( + named: r'$body', + that: isA().having( + (b) => b.token, + 'token', + 'appPassword', + ), + ), + ), + ).called(1); + }); + }); + + test('rethrows http exceptions as `GetRemoteWipeStatusFailure`', () async { + when(() => wipe.checkWipe($body: any(named: r'$body'))).thenThrow(http.ClientException('')); + + await expectLater( + repository.getRemoteWipeStatus(accountsList.first), + throwsA(isA().having((e) => e.error, 'error', isA())), + ); + + verify( + () => wipe.checkWipe( + $body: any( + named: r'$body', + that: isA().having( + (b) => b.token, + 'token', + 'appPassword', + ), + ), + ), + ).called(1); + }); + }); + + group('postRemoteWipeSuccess', () { + test('posts remote wipe success server', () async { + final response = _DynamiteResponseMock(); + when(() => response.body).thenReturn(JsonObject('')); + + when(() => wipe.wipeDone($body: any(named: r'$body'))).thenAnswer((_) async => response); + + await repository.postRemoteWipeSuccess(accountsList.first); + + verify( + () => wipe.wipeDone( + $body: any( + named: r'$body', + that: isA().having( + (b) => b.token, + 'token', + 'appPassword', + ), + ), + ), + ).called(1); + }); + + test('rethrows http exceptions as `PostRemoteWipeSuccessFailure`', () async { + when(() => wipe.wipeDone($body: any(named: r'$body'))).thenThrow(http.ClientException('')); + + await expectLater( + repository.postRemoteWipeSuccess(accountsList.first), + throwsA(isA().having((e) => e.error, 'error', isA())), + ); + + verify( + () => wipe.wipeDone( + $body: any( + named: r'$body', + that: isA().having( + (b) => b.token, + 'token', + 'appPassword', + ), + ), + ), + ).called(1); + }); + }); }); }