diff --git a/lib/features/public_asset/data/datasource/public_asset_datasource.dart b/lib/features/public_asset/data/datasource/public_asset_datasource.dart index 98cff7fb5..a38aa278d 100644 --- a/lib/features/public_asset/data/datasource/public_asset_datasource.dart +++ b/lib/features/public_asset/data/datasource/public_asset_datasource.dart @@ -3,6 +3,7 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/public_asset.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/repository/public_asset_repository.dart'; abstract class PublicAssetDatasource { const PublicAssetDatasource(); @@ -33,4 +34,10 @@ abstract class PublicAssetDatasource { AccountId accountId, {required List publicAssets} ); + + Future partialUpdatePublicAssets( + Session session, + AccountId accountId, + {required Map mapPublicAssetIdToUpdatingIdentityIds} + ); } \ No newline at end of file diff --git a/lib/features/public_asset/data/datasource_impl/remote_public_asset_datasource_impl.dart b/lib/features/public_asset/data/datasource_impl/remote_public_asset_datasource_impl.dart index 453554b37..16bf3ca59 100644 --- a/lib/features/public_asset/data/datasource_impl/remote_public_asset_datasource_impl.dart +++ b/lib/features/public_asset/data/datasource_impl/remote_public_asset_datasource_impl.dart @@ -5,6 +5,7 @@ import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/public_asset.dart'; import 'package:tmail_ui_user/features/public_asset/data/datasource/public_asset_datasource.dart'; import 'package:tmail_ui_user/features/public_asset/data/network/public_asset_api.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/repository/public_asset_repository.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class RemotePublicAssetDatasourceImpl implements PublicAssetDatasource { @@ -67,4 +68,17 @@ class RemotePublicAssetDatasourceImpl implements PublicAssetDatasource { publicAssets: publicAssets ); }).catchError(_exceptionThrower.throwException); + + @override + Future partialUpdatePublicAssets( + Session session, + AccountId accountId, + {required Map mapPublicAssetIdToUpdatingIdentityIds} + ) => Future.sync(() async { + return await _publicAssetApi.partialUpdatePublicAssets( + session, + accountId, + mapPublicAssetIdToUpdatingIdentityIds: mapPublicAssetIdToUpdatingIdentityIds + ); + }).catchError(_exceptionThrower.throwException); } \ No newline at end of file diff --git a/lib/features/public_asset/data/network/public_asset_api.dart b/lib/features/public_asset/data/network/public_asset_api.dart index c26deb454..3e0e0dff4 100644 --- a/lib/features/public_asset/data/network/public_asset_api.dart +++ b/lib/features/public_asset/data/network/public_asset_api.dart @@ -12,6 +12,8 @@ import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/public_asset. import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/set/set_public_asset_method.dart'; import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/set/set_public_asset_response.dart'; import 'package:tmail_ui_user/features/public_asset/domain/exceptions/public_asset_exceptions.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/repository/public_asset_repository.dart'; +import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_controller.dart'; import 'package:uuid/uuid.dart'; class PublicAssetApi { @@ -29,6 +31,20 @@ class PublicAssetApi { return MapEntry(publicAsset.id!, patchObject); } + MapEntry _toPartialPatchObjectMapEntry( + PublicAssetId publicAssetId, + UpdatingIdentityIds updatingIdentityIds, + ) { + assert( + updatingIdentityIds.values.every((value) => value != false), + 'All updating identity id values must be true or null' + ); + final patchObject = PatchObject(updatingIdentityIds.map( + (key, value) => MapEntry('${PatchObject.identityIdsProperty}/${key.id.value}', value))); + + return MapEntry(publicAssetId, patchObject); + } + Future> getPublicAssets( Session session, AccountId accountId, @@ -134,4 +150,29 @@ class PublicAssetApi { throw const CannotUpdatePublicAssetException(); } } + + Future partialUpdatePublicAssets( + Session session, + AccountId accountId, + {required Map mapPublicAssetIdToUpdatingIdentityIds} + ) async { + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final method = SetPublicAssetMethod(accountId); + method.addUpdates( + mapPublicAssetIdToUpdatingIdentityIds.map(_toPartialPatchObjectMapEntry) + ); + final invocation = requestBuilder.invocation(method); + final response = await (requestBuilder..usings(method.requiredCapabilities)) + .build() + .execute(); + + final notUpdatedPublicAssetIds = response.parse( + invocation.methodCallId, + SetPublicAssetResponse.deserialize + )?.notUpdated; + + if (notUpdatedPublicAssetIds?.isNotEmpty == true) { + throw const CannotUpdatePublicAssetException(); + } + } } \ No newline at end of file diff --git a/lib/features/public_asset/data/repository/public_asset_repository_impl.dart b/lib/features/public_asset/data/repository/public_asset_repository_impl.dart index a113aad75..4deb31764 100644 --- a/lib/features/public_asset/data/repository/public_asset_repository_impl.dart +++ b/lib/features/public_asset/data/repository/public_asset_repository_impl.dart @@ -54,4 +54,14 @@ class PublicAssetRepositoryImpl implements PublicAssetRepository { session, accountId, publicAssets: publicAssets); + + @override + Future partialUpdatePublicAssets( + Session session, + AccountId accountId, + {required Map mapPublicAssetIdToUpdatingIdentityIds} + ) => _publicAssetDatasource.partialUpdatePublicAssets( + session, + accountId, + mapPublicAssetIdToUpdatingIdentityIds: mapPublicAssetIdToUpdatingIdentityIds); } \ No newline at end of file diff --git a/lib/features/public_asset/domain/repository/public_asset_repository.dart b/lib/features/public_asset/domain/repository/public_asset_repository.dart index dc2729ddb..f1041cf32 100644 --- a/lib/features/public_asset/domain/repository/public_asset_repository.dart +++ b/lib/features/public_asset/domain/repository/public_asset_repository.dart @@ -4,6 +4,8 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/public_asset.dart'; +typedef UpdatingIdentityIds = Map; + abstract class PublicAssetRepository { Future> getPublicAssetsFromIds( Session session, @@ -31,4 +33,10 @@ abstract class PublicAssetRepository { AccountId accountId, {required List publicAssets} ); + + Future partialUpdatePublicAssets( + Session session, + AccountId accountId, + {required Map mapPublicAssetIdToUpdatingIdentityIds} + ); } \ No newline at end of file diff --git a/lib/features/public_asset/domain/usecase/add_identity_to_public_assets_interactor.dart b/lib/features/public_asset/domain/usecase/add_identity_to_public_assets_interactor.dart index 00c38dfb5..abeaf3dd3 100644 --- a/lib/features/public_asset/domain/usecase/add_identity_to_public_assets_interactor.dart +++ b/lib/features/public_asset/domain/usecase/add_identity_to_public_assets_interactor.dart @@ -4,10 +4,8 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; -import 'package:tmail_ui_user/features/public_asset/domain/extensions/public_asset_extension.dart'; import 'package:tmail_ui_user/features/public_asset/domain/repository/public_asset_repository.dart'; import 'package:tmail_ui_user/features/public_asset/domain/state/add_identity_to_public_assets_state.dart'; -import 'package:tmail_ui_user/features/public_asset/domain/state/remove_identity_from_public_assets_state.dart'; import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_controller.dart'; class AddIdentityToPublicAssetsInteractor { @@ -25,24 +23,14 @@ class AddIdentityToPublicAssetsInteractor { ) async* { try { yield Right(AddingIdentityToPublicAssetsState()); - final publicAssets = await _publicAssetRepository.getPublicAssetsFromIds( + await _publicAssetRepository.partialUpdatePublicAssets( session, accountId, - publicAssetIds: publicAssetIds + mapPublicAssetIdToUpdatingIdentityIds: Map.fromEntries(publicAssetIds.map( + (publicAssetId) => MapEntry(publicAssetId, {identityId: true}) + )) ); - if (publicAssets.isEmpty) { - yield Left(NotFoundAnyPublicAssetsFailureState(identityId: identityId)); - } else { - final publicAssetsWithCurrentIdentity = publicAssets - .map((publicAsset) => publicAsset.withAddedIdentityId(identityId)) - .toList(); - await _publicAssetRepository.updatePublicAssets( - session, - accountId, - publicAssets: publicAssetsWithCurrentIdentity - ); - yield Right(AddIdentityToPublicAssetsSuccessState()); - } + yield Right(AddIdentityToPublicAssetsSuccessState()); } catch (exception) { yield Left(AddIdentityToPublicAssetsFailureState(exception: exception)); } diff --git a/lib/features/public_asset/domain/usecase/remove_identity_from_public_assets_interactor.dart b/lib/features/public_asset/domain/usecase/remove_identity_from_public_assets_interactor.dart index 09ac3cbba..abc84d605 100644 --- a/lib/features/public_asset/domain/usecase/remove_identity_from_public_assets_interactor.dart +++ b/lib/features/public_asset/domain/usecase/remove_identity_from_public_assets_interactor.dart @@ -4,7 +4,6 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; -import 'package:tmail_ui_user/features/public_asset/domain/extensions/public_asset_extension.dart'; import 'package:tmail_ui_user/features/public_asset/domain/repository/public_asset_repository.dart'; import 'package:tmail_ui_user/features/public_asset/domain/state/remove_identity_from_public_assets_state.dart'; import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_controller.dart'; @@ -24,24 +23,14 @@ class RemoveIdentityFromPublicAssetsInteractor { ) async* { try { yield Right(RemovingIdentityFromPublicAssetsState()); - final publicAssets = await _publicAssetRepository.getPublicAssetsFromIds( + await _publicAssetRepository.partialUpdatePublicAssets( session, accountId, - publicAssetIds: publicAssetIds + mapPublicAssetIdToUpdatingIdentityIds: Map.fromEntries(publicAssetIds.map( + (publicAssetId) => MapEntry(publicAssetId, {identityId: null}), + )) ); - if (publicAssets.isEmpty) { - yield Left(NotFoundAnyPublicAssetsFailureState(identityId: identityId)); - } else { - final publicAssetsWithCurrentIdentity = publicAssets - .map((publicAsset) => publicAsset.withRemovedIdentityId(identityId)) - .toList(); - await _publicAssetRepository.updatePublicAssets( - session, - accountId, - publicAssets: publicAssetsWithCurrentIdentity - ); - yield Right(RemoveIdentityFromPublicAssetsSuccessState(identityId: identityId)); - } + yield Right(RemoveIdentityFromPublicAssetsSuccessState(identityId: identityId)); } catch (exception) { yield Left(RemoveIdentityFromPublicAssetsFailureState( exception: exception, diff --git a/test/features/public_asset/data/network/public_asset_api_test.dart b/test/features/public_asset/data/network/public_asset_api_test.dart new file mode 100644 index 000000000..13db67f86 --- /dev/null +++ b/test/features/public_asset/data/network/public_asset_api_test.dart @@ -0,0 +1,219 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http_mock_adapter/http_mock_adapter.dart'; +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/patch_object.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/jmap_request.dart'; +import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/public_asset.dart'; +import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/set/set_public_asset_method.dart'; +import 'package:jmap_dart_client/jmap/mail/extensions/public_asset/set/set_public_asset_response.dart'; +import 'package:mockito/annotations.dart'; +import 'package:tmail_ui_user/features/public_asset/data/network/public_asset_api.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/exceptions/public_asset_exceptions.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../fixtures/account_fixtures.dart'; +import '../../../../fixtures/session_fixtures.dart'; +import 'public_asset_api_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + final baseOption = BaseOptions(method: 'POST'); + final dio = Dio(baseOption)..options.baseUrl = 'http://domain.com/jmap'; + final dioAdapter = DioAdapter(dio: dio); + final dioAdapterHeaders = {"accept": "application/json;jmapVersion=rfc-8621"}; + final httpClient = HttpClient(dio); + final processingInvocation = ProcessingInvocation(); + final requestBuilder = JmapRequestBuilder(httpClient, processingInvocation); + final identityId = IdentityId(Id('some-identity-id')); + final methodCallId = MethodCallId('c0'); + final publicAssetToBeDereferenced = PublicAsset( + id: Id('abc123'), + blobId: Id('def456'), + size: 123, + contentType: 'image/jpeg', + publicURI: 'http://domain.com/public/abc123', + identityIds: {identityId: true} + ); + final publicAssetToBeReferenced = PublicAsset( + id: Id('ghi789'), + blobId: Id('jkl012'), + size: 456, + contentType: 'image/png', + publicURI: 'http://domain.com/public/ghi789', + identityIds: {} + ); + + final publicAssetApi = PublicAssetApi(httpClient, MockUuid()); + final accountId = AccountFixtures.aliceAccountId; + final session = SessionFixtures.aliceSession; + final mapPublicAssetIdToUpdatingIdentityIds = { + publicAssetToBeDereferenced.id!: {identityId: null}, + publicAssetToBeReferenced.id!: {identityId: true} + }; + final dereferencedUpdateObject = PatchObject({ + '${PatchObject.identityIdsProperty}/${identityId.id.value}': null, + }); + final referencedUpdateObject = PatchObject({ + '${PatchObject.identityIdsProperty}/${identityId.id.value}': true, + }); + final method = SetPublicAssetMethod(accountId) + ..addUpdates({ + publicAssetToBeDereferenced.id!: dereferencedUpdateObject, + publicAssetToBeReferenced.id!: referencedUpdateObject + }); + + group('public asset api test:', () { + test( + 'should complete without exception ' + 'when server returns updated public asset ids', + () async { + // arrange + final invocation = requestBuilder.invocation( + method, + methodCallId: methodCallId); + dioAdapter.onPost( + '', + (server) => server.reply(200, { + "sessionState": "abcdefghij", + "methodResponses": [[ + method.methodName.value, + { + "accountId": accountId.id.value, + "newState": 'some-state', + "updated": { + publicAssetToBeDereferenced.id?.value: null, + publicAssetToBeReferenced.id?.value: null + }, + }, + methodCallId.value + ]] + }), + data: { + "using": method.requiredCapabilities + .map((capability) => capability.value.toString()) + .toList(), + "methodCalls": [ + [ + method.methodName.value, + { + "accountId": accountId.id.value, + "update": { + publicAssetToBeDereferenced.id?.value: dereferencedUpdateObject.toJson(), + publicAssetToBeReferenced.id?.value: referencedUpdateObject.toJson(), + }, + }, + methodCallId.value + ], + ] + }, + headers: dioAdapterHeaders, + ); + + // act + final response = (await (requestBuilder..usings(method.requiredCapabilities)) + .build() + .execute()) + .parse( + invocation.methodCallId, + SetPublicAssetResponse.deserialize); + + // assert + expect( + response?.updated, + equals({ + publicAssetToBeDereferenced.id: null, + publicAssetToBeReferenced.id: null + }) + ); + await expectLater( + publicAssetApi.partialUpdatePublicAssets( + session, + accountId, + mapPublicAssetIdToUpdatingIdentityIds: mapPublicAssetIdToUpdatingIdentityIds), + completes); + }); + + test( + 'should complete with exception ' + 'when server returns notUpdated public asset ids', + () async { + // arrange + const errorDescription = 'Invalid identity'; + final invocation = requestBuilder.invocation( + method, + methodCallId: methodCallId); + dioAdapter.onPost( + '', + (server) => server.reply(200, { + "sessionState": "abcdefghij", + "methodResponses": [[ + method.methodName.value, + { + "accountId": accountId.id.value, + "newState": 'some-state', + "notUpdated": { + publicAssetToBeDereferenced.id?.value: { + "type": "invalidArguments", + "description": errorDescription + }, + publicAssetToBeReferenced.id?.value: { + "type": "invalidArguments", + "description": errorDescription + } + } + }, + methodCallId.value + ]] + }), + data: { + "using": method.requiredCapabilities + .map((capability) => capability.value.toString()) + .toList(), + "methodCalls": [ + [ + method.methodName.value, + { + "accountId": accountId.id.value, + "update": { + publicAssetToBeDereferenced.id?.value: dereferencedUpdateObject.toJson(), + publicAssetToBeReferenced.id?.value: referencedUpdateObject.toJson(), + }, + }, + methodCallId.value + ], + ] + }, + headers: dioAdapterHeaders, + ); + + // act + final response = (await (requestBuilder..usings(method.requiredCapabilities)) + .build() + .execute()) + .parse( + invocation.methodCallId, + SetPublicAssetResponse.deserialize); + + // assert + expect( + response?.notUpdated?[publicAssetToBeDereferenced.id], + SetError(SetError.invalidArguments, description: errorDescription), + ); + expect( + response?.notUpdated?[publicAssetToBeReferenced.id], + SetError(SetError.invalidArguments, description: errorDescription), + ); + await expectLater( + publicAssetApi.partialUpdatePublicAssets( + session, + accountId, + mapPublicAssetIdToUpdatingIdentityIds: mapPublicAssetIdToUpdatingIdentityIds), + throwsA(isA())); + }); + }); +} \ No newline at end of file