diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart index 4f2c77e55..1f66fa736 100644 --- a/core/lib/data/constants/constant.dart +++ b/core/lib/data/constants/constant.dart @@ -1,4 +1,5 @@ class Constant { static const acceptHeaderDefault = 'application/json'; static const contentTypeHeaderDefault = 'application/json'; + static const pdfMimeType = 'application/pdf'; } \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index d692c43b6..bd8ff0438 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -19,6 +19,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mdn/disposition.dart'; import 'package:jmap_dart_client/jmap/mdn/mdn.dart'; +import 'package:mime/mime.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -44,6 +45,7 @@ import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state. import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/send_receipt_to_sender_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/unsubscribe_email_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/view_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; @@ -54,6 +56,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_int import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/send_receipt_to_sender_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/view_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/calendar_event_interactor_bindings.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; @@ -107,6 +110,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final StoreOpenedEmailInteractor _storeOpenedEmailInteractor; + final ViewAttachmentForWebInteractor _viewAttachmentForWebInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor; @@ -141,7 +145,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { this._markAsStarEmailInteractor, this._downloadAttachmentForWebInteractor, this._getAllIdentitiesInteractor, - this._storeOpenedEmailInteractor + this._storeOpenedEmailInteractor, + this._viewAttachmentForWebInteractor, ); @override @@ -174,6 +179,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _moveToMailboxSuccess(success); } else if (success is MarkAsStarEmailSuccess) { _markAsEmailStarSuccess(success); + } else if (success is ViewAttachmentForWebSuccess) { + _viewAttachmentForWebSuccessAction(success); } else if (success is DownloadAttachmentForWebSuccess) { _downloadAttachmentForWebSuccessAction(success); } else if (success is GetAllIdentitiesSuccess) { @@ -198,6 +205,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _downloadAttachmentsFailure(failure); } else if (failure is ExportAttachmentFailure) { _exportAttachmentFailureAction(failure); + } else if (failure is ViewAttachmentForWebFailure) { + _viewAttachmentForWebFailureAction(failure); } else if (failure is DownloadAttachmentForWebFailure) { _downloadAttachmentForWebFailureAction(failure); } else if (failure is ParseCalendarEventFailure) { @@ -721,11 +730,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void downloadAttachmentForWeb(BuildContext context, Attachment attachment) { - _downloadAttachmentForWebAction(context, attachment); - } - - void _downloadAttachmentForWebAction(BuildContext context, Attachment attachment) async { + void downloadAttachmentForWeb(Attachment attachment) { final accountId = mailboxDashBoardController.accountId.value; final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null) { @@ -744,6 +749,25 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } + void viewAttachmentForWeb(Attachment attachment) { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + if (accountId != null && session != null) { + final baseDownloadUrl = session.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); + final generateTaskId = DownloadTaskId(uuid.v4()); + consumeState(_viewAttachmentForWebInteractor.execute( + generateTaskId, + attachment, + accountId, + baseDownloadUrl, + _downloadProgressStateController)); + } else { + consumeState(Stream.value( + Left(ViewAttachmentForWebFailure(exception: NotFoundSessionException())) + )); + } + } + void _downloadAttachmentForWebSuccessAction(DownloadAttachmentForWebSuccess success) { log('SingleEmailController::_downloadAttachmentForWebSuccessAction():'); mailboxDashBoardController.deleteDownloadTask(success.taskId); @@ -753,6 +777,24 @@ class SingleEmailController extends BaseController with AppLoaderMixin { success.attachment.generateFileName()); } + void _viewAttachmentForWebSuccessAction( + ViewAttachmentForWebSuccess success, + ) { + log('SingleEmailController::_viewAttachmentForWebSuccessAction():'); + final mimeType = success.attachment.type?.mimeType ?? + lookupMimeType('', headerBytes: success.bytes); + if (mimeType != Constant.pdfMimeType) { + _downloadAttachmentForWebSuccessAction(success); + return; + } + + mailboxDashBoardController.deleteDownloadTask(success.taskId); + + _downloadManager.openDownloadedFileWeb( + success.bytes, + success.attachment.type?.mimeType); + } + void _downloadAttachmentForWebFailureAction(DownloadAttachmentForWebFailure failure) { log('SingleEmailController::_downloadAttachmentForWebFailureAction(): $failure'); if (failure.taskId != null) { @@ -766,6 +808,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } + void _viewAttachmentForWebFailureAction(ViewAttachmentForWebFailure failure) { + log('SingleEmailController::_viewAttachmentForWebFailureAction(): $failure'); + _downloadAttachmentForWebFailureAction(failure); + } + void moveToMailbox(BuildContext context, PresentationEmail email) async { final currentMailbox = getMailboxContain(email); final accountId = mailboxDashBoardController.accountId.value; @@ -1384,7 +1431,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ..onCloseButtonAction(() => popBack()) ..onDownloadAttachmentFileAction((attachment) { if (PlatformInfo.isWeb) { - downloadAttachmentForWeb(context, attachment); + downloadAttachmentForWeb(attachment); } else { exportAttachment(context, attachment); } @@ -1402,7 +1449,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { onCloseButtonAction: () => popBack(), onDownloadAttachmentFileAction: (attachment) { if (PlatformInfo.isWeb) { - downloadAttachmentForWeb(context, attachment); + downloadAttachmentForWeb(attachment); } else { exportAttachment(context, attachment); } diff --git a/pubspec.lock b/pubspec.lock index 05a9e62e2..0a07e03f1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1192,7 +1192,7 @@ packages: source: hosted version: "1.10.0" mime: - dependency: transitive + dependency: "direct main" description: name: mime sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e diff --git a/pubspec.yaml b/pubspec.yaml index 8e0f14ef2..7747da9d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -229,6 +229,8 @@ dependencies: flutter_secure_storage: 8.0.0 + mime: 1.0.4 + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart new file mode 100644 index 000000000..3c7d9de79 --- /dev/null +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -0,0 +1,247 @@ +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart' hide State; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +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/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:tmail_ui_user/features/email/domain/state/view_attachment_for_web_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/view_attachment_for_web_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; +import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; +import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; +import 'package:uuid/uuid.dart'; + +import 'single_email_controller_test.mocks.dart'; + +mockControllerCallback() => InternalFinalCallback(callback: () {}); +const fallbackGenerators = { + #onStart: mockControllerCallback, + #onDelete: mockControllerCallback, +}; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final getEmailContentInteractor = MockGetEmailContentInteractor(); + final markAsEmailReadInteractor = MockMarkAsEmailReadInteractor(); + final downloadAttachmentsInteractor = MockDownloadAttachmentsInteractor(); + final deviceManager = MockDeviceManager(); + final exportAttachmentInteractor = MockExportAttachmentInteractor(); + final moveToMailboxInteractor = MockMoveToMailboxInteractor(); + final markAsStarEmailInteractor = MockMarkAsStarEmailInteractor(); + final downloadAttachmentForWebInteractor = + MockDownloadAttachmentForWebInteractor(); + final getAllIdentitiesInteractor = MockGetAllIdentitiesInteractor(); + final storeOpenedEmailInteractor = MockStoreOpenedEmailInteractor(); + final viewAttachmentForWebInteractor = MockViewAttachmentForWebInteractor(); + final mailboxDashboardController = MockMailboxDashBoardController(); + final emailSupervisorController = MockEmailSupervisorController(); + final downloadManager = MockDownloadManager(); + final cachingManager = MockCachingManager(); + final languageCacheManager = MockLanguageCacheManager(); + final authorizationInterceptors = MockAuthorizationInterceptors(); + final dynamicUrlInterceptors = MockDynamicUrlInterceptors(); + final deleteCredentialInteractor = MockDeleteCredentialInteractor(); + final logoutOidcInteractor = MockLogoutOidcInteractor(); + final deleteAuthorityOidcInteractor = MockDeleteAuthorityOidcInteractor(); + final appToast = MockAppToast(); + final imagePaths = MockImagePaths(); + final responsiveUtils = MockResponsiveUtils(); + final uuid = MockUuid(); + + late SingleEmailController singleEmailController = SingleEmailController( + getEmailContentInteractor, + markAsEmailReadInteractor, + downloadAttachmentsInteractor, + deviceManager, + exportAttachmentInteractor, + moveToMailboxInteractor, + markAsStarEmailInteractor, + downloadAttachmentForWebInteractor, + getAllIdentitiesInteractor, + storeOpenedEmailInteractor, + viewAttachmentForWebInteractor, + ); + + final testAccountId = AccountId(Id('123')); + final google = Uri.parse('https://www.google.com'); + final testSession = + Session({}, {}, {}, UserName('data'), google, google, google, google, State('1')); + const testTaskId = 'taskId'; + final testDownloadTaskId = DownloadTaskId(testTaskId); + final testBytes = Uint8List(123); + + setUpAll(() { + Get.put(mailboxDashboardController); + Get.put(emailSupervisorController); + Get.put(downloadManager); + Get.put(cachingManager); + Get.put(languageCacheManager); + Get.put(authorizationInterceptors); + Get.put( + authorizationInterceptors, + tag: BindingTag.isolateTag, + ); + Get.put(dynamicUrlInterceptors); + Get.put(deleteCredentialInteractor); + Get.put(logoutOidcInteractor); + Get.put(deleteAuthorityOidcInteractor); + Get.put(appToast); + Get.put(imagePaths); + Get.put(responsiveUtils); + Get.put(uuid); + + when(mailboxDashboardController.accountId).thenReturn(Rxn(testAccountId)); + when(uuid.v4()).thenReturn(testTaskId); + }); + + group('single email controller', () { + test( + 'should call execute on ViewAttachmentForWebInteractor ' + 'when viewAttachmentForWeb is called', + () { + // arrange + when(mailboxDashboardController.sessionCurrent).thenReturn(testSession); + final testAttachment = Attachment(); + + // act + singleEmailController.viewAttachmentForWeb(testAttachment); + + // assert + verify(viewAttachmentForWebInteractor.execute( + any, + testAttachment, + testAccountId, + any, + any, + )).called(1); + }, + ); + + test( + 'should trigger mailboxDashBoardController.deleteDownloadTask & downloadManager.openDownloadedFileWeb' + 'when attachment mime type is pdf', + () async { + // arrange + final testAttachment = Attachment(type: MediaType('application', 'pdf')); + when(mailboxDashboardController.sessionCurrent).thenReturn(testSession); + when(viewAttachmentForWebInteractor.execute( + testDownloadTaskId, + testAttachment, + testAccountId, + any, + any, + )).thenAnswer((_) => Stream.value( + right(ViewAttachmentForWebSuccess( + testDownloadTaskId, + testAttachment, + testBytes, + )), + )); + + // act + singleEmailController.viewAttachmentForWeb(testAttachment); + + // assert + await untilCalled(mailboxDashboardController.deleteDownloadTask(any)); + verify(mailboxDashboardController.deleteDownloadTask(testDownloadTaskId)) + .called(1); + await untilCalled(downloadManager.openDownloadedFileWeb(any, any)); + verify(downloadManager.openDownloadedFileWeb( + testBytes, + Constant.pdfMimeType, + )).called(1); + }, + ); + + test( + 'should trigger mailboxDashBoardController.deleteDownloadTask & downloadManager.createAnchorElementDownloadFileWeb' + 'when attachment mime type is not pdf', + () async { + // arrange + const testFileName = 'test_file.txt'; + final testAttachment = Attachment(name: testFileName); + when(mailboxDashboardController.sessionCurrent).thenReturn(testSession); + when(viewAttachmentForWebInteractor.execute( + testDownloadTaskId, + testAttachment, + testAccountId, + any, + any, + )).thenAnswer((_) => Stream.value( + right(ViewAttachmentForWebSuccess( + testDownloadTaskId, + testAttachment, + testBytes, + )), + )); + + // act + singleEmailController.viewAttachmentForWeb(testAttachment); + + // assert + await untilCalled(mailboxDashboardController.deleteDownloadTask(any)); + verify(mailboxDashboardController.deleteDownloadTask(testDownloadTaskId)) + .called(1); + await untilCalled( + downloadManager.createAnchorElementDownloadFileWeb(any, any), + ); + verify(downloadManager.createAnchorElementDownloadFileWeb( + testBytes, + testFileName, + )).called(1); + }, + ); + }); +}