diff --git a/test/features/composer/data/html_analyzer_test.dart b/test/features/composer/data/html_analyzer_test.dart
new file mode 100644
index 000000000..9be54d1db
--- /dev/null
+++ b/test/features/composer/data/html_analyzer_test.dart
@@ -0,0 +1,288 @@
+import 'package:core/presentation/utils/html_transformer/html_transform.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:html/parser.dart';
+import 'package:http_parser/http_parser.dart' show MediaType;
+import 'package:jmap_dart_client/jmap/core/id.dart' show Id;
+import 'package:model/email/attachment.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
+import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
+import 'package:uuid/uuid.dart';
+import 'html_analyzer_test.mocks.dart';
+
+@GenerateMocks([HtmlTransform, FileUploader, Uuid])
+void main() {
+ group('HtmlAnalyzer::replaceImageBase64ToImageCID::', () {
+ late HtmlAnalyzer htmlAnalyzer;
+ late MockHtmlTransform mockHtmlTransform;
+ late MockFileUploader mockFileUploader;
+ late MockUuid mockUuid;
+
+ setUp(() {
+ mockHtmlTransform = MockHtmlTransform();
+ mockFileUploader = MockFileUploader();
+ mockUuid = MockUuid();
+ htmlAnalyzer = HtmlAnalyzer(mockHtmlTransform, mockFileUploader, mockUuid);
+ });
+
+ test(
+ 'When HTML content has no base64 images,\n'
+ 'should return original content and empty attachments if no inline attachments exist',
+ () async {
+ const htmlContent = '
Hello World
';
+ final inlineAttachments = {};
+ final uploadUri = Uri.parse('https://example.com/upload');
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: uploadUri,
+ );
+
+ expect(result.value1, htmlContent);
+ expect(result.value2, isEmpty);
+ });
+
+ test(
+ 'When HTML content has an image with existing CID,\n'
+ 'should update src to CID format and include attachment in result',
+ () async {
+ const htmlContent = '
';
+ final inlineAttachments = {
+ 'test123': Attachment(
+ blobId: Id('test123'),
+ type: MediaType('image', 'png'),
+ disposition: ContentDisposition.inline,
+ cid: 'test123',
+ )
+ };
+ final uploadUri = Uri.parse('https://example.com/upload');
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: uploadUri,
+ );
+
+ final document = parse(result.value1);
+ final imgTag = document.querySelector('img');
+ expect(imgTag?.attributes['src'], 'cid:test123');
+ expect(imgTag?.attributes.containsKey('id'), isFalse);
+ expect(result.value2.length, 1);
+ expect(result.value2.first.cid, 'test123');
+ });
+
+ test(
+ 'When HTML content has a base64 image and upload succeeds,\n'
+ 'should replace src with new CID and add attachment to result',
+ () async {
+ const base64Data = 'iVBORw0KGgo=';
+ const htmlContent = '
';
+ final inlineAttachments = {};
+ final uploadUri = Uri.parse('https://example.com/upload');
+
+ when(mockUuid.v1()).thenReturn('new-uuid-123');
+ when(mockFileUploader.uploadAttachment(any, any, any))
+ .thenAnswer((_) async => Attachment(
+ blobId: Id('blob123'),
+ type: MediaType('image', 'png'),
+ disposition: ContentDisposition.inline,
+ ));
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: uploadUri,
+ );
+
+ final document = parse(result.value1);
+ final imgTag = document.querySelector('img');
+ expect(imgTag?.attributes['src'], 'cid:new-uuid-123');
+ expect(result.value2.length, 1);
+ expect(result.value2.first.cid, 'new-uuid-123');
+ verify(mockFileUploader.uploadAttachment(any, any, uploadUri)).called(1);
+ });
+
+ test(
+ 'When upload URI is null and HTML has base64 images,\n'
+ 'should keep original src and not attempt uploads',
+ () async {
+ const htmlContent = '
';
+ final inlineAttachments = {};
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: null,
+ );
+
+ expect(result.value1, htmlContent);
+ expect(result.value2, isEmpty);
+ verifyNever(mockFileUploader.uploadAttachment(any, any, any));
+ });
+
+ test(
+ 'When processing multiple base64 images,\n'
+ 'should replace each with a unique CID and add corresponding attachments',
+ () async {
+ const htmlContent = '''
+
+
+ ''';
+ final inlineAttachments = {};
+ final uploadUri = Uri.parse('https://example.com/upload');
+
+ when(mockUuid.v1()).thenAnswer((_) => 'uuid1');
+
+ when(mockFileUploader.uploadAttachment(any, any, any))
+ .thenAnswer((_) async => Attachment(
+ blobId: Id('blob123'),
+ type: MediaType('image', 'png'),
+ disposition: ContentDisposition.inline,
+ ));
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: uploadUri,
+ );
+
+ final document = parse(result.value1);
+ final imgTags = document.querySelectorAll('img');
+ expect(imgTags.length, 2);
+ expect(imgTags[0].attributes['src'], 'cid:uuid1');
+ expect(imgTags[1].attributes['src'], 'cid:uuid1');
+ expect(result.value2.length, 2);
+ verify(mockFileUploader.uploadAttachment(any, any, uploadUri)).called(2);
+ });
+
+ test(
+ 'When one image upload throws an exception out of multiple base64 images,\n'
+ 'should replace successful upload with CID and keep original src for failed one',
+ () async {
+ const htmlContent = '''
+
+
+ ''';
+ final inlineAttachments = {};
+ final uploadUri = Uri.parse('https://example.com/upload');
+
+ when(mockUuid.v1()).thenAnswer((_) => 'uuid1');
+
+ int uploadCallCount = 0;
+ when(mockFileUploader.uploadAttachment(any, any, any))
+ .thenAnswer((_) async {
+ uploadCallCount++;
+ if (uploadCallCount == 1) {
+ return Attachment(
+ blobId: Id('blob123'),
+ type: MediaType('image', 'png'),
+ disposition: ContentDisposition.inline,
+ );
+ } else {
+ throw Exception('Upload failed for second image');
+ }
+ });
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: uploadUri,
+ );
+
+ final document = parse(result.value1);
+ final imgTags = document.querySelectorAll('img');
+ expect(imgTags.length, 2);
+ expect(imgTags[0].attributes['src'], 'cid:uuid1');
+ expect(imgTags[1].attributes['src'], 'data:image/jpeg;base64,/9j/4AAQSkZJRg==');
+ expect(result.value2.length, 1);
+ expect(result.value2.first.cid, 'uuid1');
+ verify(mockFileUploader.uploadAttachment(any, any, uploadUri)).called(2);
+ });
+
+ test(
+ 'When all uploads throw exceptions for multiple base64 images,\n'
+ 'should keep original src for all images and add no attachments',
+ () async {
+ const htmlContent = '''
+
+
+ ''';
+ final inlineAttachments = {};
+ final uploadUri = Uri.parse('https://example.com/upload');
+
+ when(mockUuid.v1()).thenAnswer((_) => 'uuid1');
+
+ when(mockFileUploader.uploadAttachment(any, any, any))
+ .thenThrow(Exception('Upload failed'));
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: uploadUri,
+ );
+
+ final document = parse(result.value1);
+ final imgTags = document.querySelectorAll('img');
+ expect(imgTags.length, 2);
+ expect(imgTags[0].attributes['src'], 'data:image/png;base64,iVBORw0KGgo=');
+ expect(imgTags[1].attributes['src'], 'data:image/jpeg;base64,/9j/4AAQSkZJRg==');
+ expect(result.value2.length, 0);
+ verify(mockFileUploader.uploadAttachment(any, any, uploadUri)).called(2);
+ });
+
+ test(
+ 'When processing an invalid base64 image that causes upload to throw an exception,\n'
+ 'should keep the original invalid src and add no attachments',
+ () async {
+ const htmlContent = '
';
+ final inlineAttachments = {};
+ final uploadUri = Uri.parse('https://example.com/upload');
+
+ when(mockUuid.v1()).thenAnswer((_) => 'uuid1');
+
+ when(mockFileUploader.uploadAttachment(any, any, any))
+ .thenThrow(Exception('Invalid base64 data'));
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: uploadUri,
+ );
+
+ final document = parse(result.value1);
+ final imgTags = document.querySelectorAll('img');
+ expect(imgTags.length, 1);
+ expect(imgTags[0].attributes['src'], 'data:image/png;base64,invalid-base64-data');
+ expect(result.value2.length, 0);
+ });
+
+ test(
+ 'When upload throws an exception for a base64 image,\n'
+ 'should keep the original src and add no attachments',
+ () async {
+ const htmlContent = '
';
+ final inlineAttachments = {};
+ final uploadUri = Uri.parse('https://example.com/upload');
+
+ when(mockUuid.v1()).thenAnswer((_) => 'uuid1');
+
+ when(mockFileUploader.uploadAttachment(any, any, any))
+ .thenThrow(Exception('Upload failed'));
+
+ final result = await htmlAnalyzer.replaceImageBase64ToImageCID(
+ emailContent: htmlContent,
+ inlineAttachments: inlineAttachments,
+ uploadUri: uploadUri,
+ );
+
+ final document = parse(result.value1);
+ final imgTags = document.querySelectorAll('img');
+ expect(imgTags.length, 1);
+ expect(imgTags[0].attributes['src'], 'data:image/png;base64,iVBORw0KGgo=');
+ expect(result.value2.length, 0);
+ verify(mockFileUploader.uploadAttachment(any, any, any)).called(1);
+ });
+ });
+}
diff --git a/test/features/composer/data/repository/composer_repository_impl_test.dart b/test/features/composer/data/repository/composer_repository_impl_test.dart
new file mode 100644
index 000000000..3a4a1e81e
--- /dev/null
+++ b/test/features/composer/data/repository/composer_repository_impl_test.dart
@@ -0,0 +1,236 @@
+import 'package:core/utils/application_manager.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http_parser/http_parser.dart';
+import 'package:jmap_dart_client/jmap/core/session/session.dart';
+import 'package:jmap_dart_client/jmap/core/user_name.dart';
+import 'package:jmap_dart_client/jmap/identities/identity.dart';
+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/mail/email/email_body_part.dart';
+import 'package:model/email/attachment.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:model/email/email_action_type.dart';
+import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart';
+import 'package:tmail_ui_user/features/composer/data/repository/composer_repository_impl.dart';
+import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
+import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
+import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_datasource.dart';
+import 'package:uuid/uuid.dart';
+
+import 'composer_repository_impl_test.mocks.dart';
+
+@GenerateMocks([
+ AttachmentUploadDataSource,
+ ComposerDataSource,
+ HtmlDataSource,
+ ApplicationManager,
+ Uuid,
+ CreateEmailRequest,
+ Session,
+ Identity,
+])
+void main() {
+ group('ComposerRepositoryImpl::generateEmail::', () {
+ late ComposerRepositoryImpl repository;
+ late MockAttachmentUploadDataSource mockAttachmentUploadDataSource;
+ late MockComposerDataSource mockComposerDataSource;
+ late MockHtmlDataSource mockHtmlDataSource;
+ late MockApplicationManager mockApplicationManager;
+ late MockUuid mockUuid;
+ late MockCreateEmailRequest mockCreateEmailRequest;
+ late MockSession mockSession;
+ late MockIdentity mockIdentity;
+
+ setUp(() {
+ mockAttachmentUploadDataSource = MockAttachmentUploadDataSource();
+ mockComposerDataSource = MockComposerDataSource();
+ mockHtmlDataSource = MockHtmlDataSource();
+ mockApplicationManager = MockApplicationManager();
+ mockUuid = MockUuid();
+ mockCreateEmailRequest = MockCreateEmailRequest();
+ mockSession = MockSession();
+ mockIdentity = MockIdentity();
+
+ when(mockSession.username).thenReturn(UserName('test@example.com'));
+ when(mockIdentity.email).thenReturn('identity@example.com');
+ when(mockIdentity.name).thenReturn('Test User');
+ when(mockIdentity.replyTo).thenReturn({});
+
+ when(mockCreateEmailRequest.session).thenReturn(mockSession);
+ when(mockCreateEmailRequest.identity).thenReturn(mockIdentity);
+ when(mockCreateEmailRequest.replyToRecipients).thenReturn({});
+
+ repository = ComposerRepositoryImpl(
+ mockAttachmentUploadDataSource,
+ mockComposerDataSource,
+ mockHtmlDataSource,
+ mockApplicationManager,
+ mockUuid,
+ );
+ });
+
+ test(
+ 'When the email content contains base64 images,\n'
+ 'generateEmail should replace them with CIDs and return an email without base64',
+ () async {
+ // Arrange
+ const base64Image =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==';
+ const emailContentWithBase64 = 'Hello 
';
+ const emailContentWithCid = 'Hello 
';
+ const userAgent = 'TestUserAgent';
+ const partIdValue = 'part-id-123';
+ final inlineAttachments = {
+ 'unique-cid': Attachment(name: 'test.png'),
+ };
+ final attachments = [Attachment(name: 'test.png')];
+ final emailBodyParts = {
+ EmailBodyPart(partId: PartId(partIdValue), type: MediaType('image', 'png')),
+ };
+
+ when(mockCreateEmailRequest.emailContent).thenReturn(emailContentWithBase64);
+ when(mockCreateEmailRequest.inlineAttachments).thenReturn(inlineAttachments);
+ when(mockCreateEmailRequest.uploadUri).thenReturn(Uri.parse('https://example.com/upload'));
+ when(mockCreateEmailRequest.attachments).thenReturn(attachments);
+ when(mockCreateEmailRequest.toRecipients).thenReturn({});
+ when(mockCreateEmailRequest.ccRecipients).thenReturn(null);
+ when(mockCreateEmailRequest.bccRecipients).thenReturn(null);
+ when(mockCreateEmailRequest.subject).thenReturn('Test Subject');
+ when(mockCreateEmailRequest.draftsMailboxId).thenReturn(null);
+ when(mockCreateEmailRequest.outboxMailboxId).thenReturn(null);
+ when(mockCreateEmailRequest.emailActionType).thenReturn(EmailActionType.compose);
+ when(mockCreateEmailRequest.hasRequestReadReceipt).thenReturn(false);
+
+ when(mockHtmlDataSource.replaceImageBase64ToImageCID(
+ emailContent: emailContentWithBase64,
+ inlineAttachments: inlineAttachments,
+ uploadUri: Uri.parse('https://example.com/upload'),
+ )).thenAnswer((_) async => Tuple2(emailContentWithCid, emailBodyParts));
+
+ when(mockHtmlDataSource.removeCollapsedExpandedSignatureEffect(emailContent: emailContentWithCid))
+ .thenAnswer((_) async => emailContentWithCid);
+
+ when(mockApplicationManager.generateApplicationUserAgent())
+ .thenAnswer((_) async => userAgent);
+
+ when(mockUuid.v1()).thenReturn(partIdValue);
+
+ // Act
+ final result = await repository.generateEmail(mockCreateEmailRequest);
+
+ // Assert
+ expect(result, isA());
+ expect(result.from, {EmailAddress('Test User', 'identity@example.com')});
+ expect(result.bodyValues![PartId(partIdValue)]!.value, emailContentWithCid);
+ expect(result.attachments, containsAll(emailBodyParts));
+ expect(emailContentWithCid.contains('data:image'), isFalse);
+ verify(mockHtmlDataSource.replaceImageBase64ToImageCID(
+ emailContent: emailContentWithBase64,
+ inlineAttachments: inlineAttachments,
+ uploadUri: Uri.parse('https://example.com/upload'),
+ )).called(1);
+ });
+
+ test(
+ 'When replacing base64 with CID fails,\n'
+ 'generateEmail should still successfully create an email with the original content',
+ () async {
+ // Arrange
+ const emailContentWithBase64 = 'Hello 
';
+ const userAgent = 'TestUserAgent';
+ const partIdValue = 'part-id-123';
+ final inlineAttachments = {
+ 'cid-1': Attachment(name: 'test.png'),
+ };
+ final attachments = [Attachment(name: 'test.png')];
+
+ when(mockCreateEmailRequest.emailContent).thenReturn(emailContentWithBase64);
+ when(mockCreateEmailRequest.inlineAttachments).thenReturn(inlineAttachments);
+ when(mockCreateEmailRequest.uploadUri).thenReturn(Uri.parse('https://example.com/upload'));
+ when(mockCreateEmailRequest.attachments).thenReturn(attachments);
+ when(mockCreateEmailRequest.toRecipients).thenReturn({});
+ when(mockCreateEmailRequest.ccRecipients).thenReturn(null);
+ when(mockCreateEmailRequest.bccRecipients).thenReturn(null);
+ when(mockCreateEmailRequest.subject).thenReturn('Test Subject');
+ when(mockCreateEmailRequest.draftsMailboxId).thenReturn(null);
+ when(mockCreateEmailRequest.outboxMailboxId).thenReturn(null);
+ when(mockCreateEmailRequest.emailActionType).thenReturn(EmailActionType.compose);
+ when(mockCreateEmailRequest.hasRequestReadReceipt).thenReturn(false);
+
+ when(mockHtmlDataSource.replaceImageBase64ToImageCID(
+ emailContent: emailContentWithBase64,
+ inlineAttachments: inlineAttachments,
+ uploadUri: Uri.parse('https://example.com/upload'),
+ )).thenThrow(Exception('Failed to replace base64'));
+
+ when(mockHtmlDataSource.removeCollapsedExpandedSignatureEffect(emailContent: emailContentWithBase64))
+ .thenAnswer((_) async => emailContentWithBase64);
+
+ when(mockApplicationManager.generateApplicationUserAgent())
+ .thenAnswer((_) async => userAgent);
+
+ when(mockUuid.v1()).thenReturn(partIdValue);
+
+ // Act
+ final result = await repository.generateEmail(mockCreateEmailRequest);
+
+ // Assert
+ expect(result, isA());
+ expect(result.bodyValues![PartId(partIdValue)]!.value, emailContentWithBase64);
+ expect(result.attachments, isNotEmpty);
+ verify(mockHtmlDataSource.replaceImageBase64ToImageCID(
+ emailContent: emailContentWithBase64,
+ inlineAttachments: inlineAttachments,
+ uploadUri: Uri.parse('https://example.com/upload'),
+ )).called(1);
+ });
+
+ test(
+ 'When the email content has no base64 images,\n'
+ 'generateEmail should process it correctly and return a valid email',
+ () async {
+ const plainEmailContent = 'Hello, this is a simple email
';
+ const userAgent = 'TestUserAgent';
+ const partIdValue = 'part-id-123';
+ final attachments = [Attachment(name: 'test.png')];
+ final emailBodyParts = {
+ EmailBodyPart(partId: PartId(partIdValue), type: MediaType('image', 'png')),
+ };
+
+ when(mockCreateEmailRequest.emailContent).thenReturn(plainEmailContent);
+ when(mockCreateEmailRequest.inlineAttachments).thenReturn({});
+ when(mockCreateEmailRequest.uploadUri).thenReturn(Uri.parse('https://example.com/upload'));
+ when(mockCreateEmailRequest.attachments).thenReturn(attachments);
+ when(mockCreateEmailRequest.toRecipients).thenReturn({});
+ when(mockCreateEmailRequest.ccRecipients).thenReturn(null);
+ when(mockCreateEmailRequest.bccRecipients).thenReturn(null);
+ when(mockCreateEmailRequest.subject).thenReturn('Test Subject');
+ when(mockCreateEmailRequest.draftsMailboxId).thenReturn(null);
+ when(mockCreateEmailRequest.outboxMailboxId).thenReturn(null);
+ when(mockCreateEmailRequest.emailActionType).thenReturn(EmailActionType.compose);
+ when(mockCreateEmailRequest.hasRequestReadReceipt).thenReturn(false);
+
+ when(mockHtmlDataSource.replaceImageBase64ToImageCID(
+ emailContent: plainEmailContent,
+ inlineAttachments: {},
+ uploadUri: Uri.parse('https://example.com/upload'),
+ )).thenAnswer((_) async => Tuple2(plainEmailContent, emailBodyParts));
+
+ when(mockHtmlDataSource.removeCollapsedExpandedSignatureEffect(emailContent: plainEmailContent))
+ .thenAnswer((_) async => plainEmailContent);
+
+ when(mockApplicationManager.generateApplicationUserAgent())
+ .thenAnswer((_) async => userAgent);
+
+ when(mockUuid.v1()).thenReturn(partIdValue);
+
+ final result = await repository.generateEmail(mockCreateEmailRequest);
+
+ expect(result, isA());
+ expect(result.bodyValues![PartId(partIdValue)]!.value, plainEmailContent);
+ expect(result.attachments, containsAll(emailBodyParts));
+ });
+ });
+}
\ No newline at end of file