From 769bca0ddaac25bbe47978fd70be462ea44805e9 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 7 Mar 2025 10:41:27 +0700 Subject: [PATCH] TF-3514 Add test case for unit test `HtmlAnalyzer` when html content existing CID but inlineAttachments is empty Signed-off-by: dab246 --- .../email/data/local/html_analyzer.dart | 50 ++++++++++--------- .../composer/data/html_analyzer_test.dart | 41 ++++++++++++++- 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart index ebc30749a..cac60a928 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -1,4 +1,3 @@ -import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:core/data/constants/constant.dart'; @@ -128,6 +127,7 @@ class HtmlAnalyzer { }) async { final document = parse(emailContent); final listImgTag = document.querySelectorAll('img[src^="data:image/"]'); + log('HtmlAnalyzer::replaceImageBase64ToImageCID:listImgTagLength = ${listImgTag.length} | inlineAttachments = ${inlineAttachments.length}'); if (listImgTag.isEmpty) { @@ -143,40 +143,45 @@ class HtmlAnalyzer { final List> asyncTasks = []; for (final imgTag in listImgTag) { - late final LinkedHashMap attributes = imgTag.attributes; - final idImg = attributes['id']; + final attributes = imgTag.attributes; final imageSrc = attributes['src']; - if (imageSrc?.isEmpty ?? true) continue; + final idImg = attributes['id']; if (idImg?.startsWith(cidPrefixKey) == true) { final cid = idImg!.substring(cidPrefixKey.length).trim(); - attributes['src'] = '$cidPrefixKey$cid'; - attributes.remove('id'); - final attachment = inlineAttachments[cid]; - log('HtmlAnalyzer::replaceImageBase64ToImageCID:attachment = $attachment'); + if (attachment != null) { + attributes['src'] = '$cidPrefixKey$cid'; + attributes.remove('id'); inlineAttachmentsSet.add(attachment.toEmailBodyPart(charset: Constant.base64Charset)); + continue; } - continue; } if (uploadUri == null) continue; + final taskId = idImg?.startsWith(cidPrefixKey) == true + ? idImg!.substring(cidPrefixKey.length) + : _uuid.v1(); + asyncTasks.add(_retrieveAttachmentFromUpload( + taskId: taskId, uploadUri: uploadUri, base64ImageTag: imageSrc!, - ).then((newAttachment) { - if (newAttachment == null) return; + ).then((attachmentRecord) { + if (attachmentRecord == null) return; - final newInlineAttachment = newAttachment.toAttachmentWithDisposition( + final newInlineAttachment = attachmentRecord.$1.toAttachmentWithDisposition( disposition: ContentDisposition.inline, - cid: _uuid.v1(), + cid: attachmentRecord.$2, ); - final newCid = newInlineAttachment.cid; - inlineAttachments[newCid!] = newInlineAttachment; + + final newCid = newInlineAttachment.cid!; + inlineAttachments[newCid] = newInlineAttachment; attributes['src'] = '$cidPrefixKey$newCid'; + attributes.remove('id'); inlineAttachmentsSet.add(newInlineAttachment.toEmailBodyPart(charset: Constant.base64Charset)); })); @@ -186,8 +191,7 @@ class HtmlAnalyzer { await Future.wait(asyncTasks); } - final newContent = document.body?.innerHtml ?? emailContent; - return Tuple2(newContent, inlineAttachmentsSet); + return Tuple2(document.body?.innerHtml ?? emailContent, inlineAttachmentsSet); } Future removeCollapsedExpandedSignatureEffect({required String emailContent}) async { @@ -210,7 +214,8 @@ class HtmlAnalyzer { return newContent; } - Future _retrieveAttachmentFromUpload({ + Future<(Attachment attachment, String taskId)?> _retrieveAttachmentFromUpload({ + required String taskId, required Uri uploadUri, required String base64ImageTag, }) async { @@ -218,20 +223,19 @@ class HtmlAnalyzer { final imageBytes = StringConvert.convertBase64ImageTagToBytes(base64ImageTag); final mediaType = StringConvert.getMediaTypeFromBase64ImageTag(base64ImageTag); log('HtmlAnalyzer::_retrieveAttachmentFromUpload: mimeType = ${mediaType?.mimeType} | imageBytesLength = ${imageBytes.length}'); - final generateId = _uuid.v1(); final fileInfo = FileInfo.fromBytes( bytes: imageBytes, - name: '$generateId.${mediaType?.subtype ?? 'png'}', + name: '$taskId.${mediaType?.subtype ?? 'png'}', type: mediaType?.mimeType, ); final attachment = await _fileUploader.uploadAttachment( - UploadTaskId(generateId), + UploadTaskId(taskId), fileInfo, uploadUri, ); - log('HtmlAnalyzer::_retrieveAttachmentFromUpload:Attachment = $attachment'); - return attachment; + log('HtmlAnalyzer::_retrieveAttachmentFromUpload:Attachment = $attachment | taskId = $taskId'); + return (attachment, taskId); } catch (e) { logError('HtmlAnalyzer::_retrieveAttachmentFromUpload:Exception = $e'); return null; diff --git a/test/features/composer/data/html_analyzer_test.dart b/test/features/composer/data/html_analyzer_test.dart index 9be54d1db..ea2d81189 100644 --- a/test/features/composer/data/html_analyzer_test.dart +++ b/test/features/composer/data/html_analyzer_test.dart @@ -73,6 +73,37 @@ void main() { expect(result.value2.first.cid, 'test123'); }); + test( + 'When HTML content has an image with existing CID,\n' + 'but inlineAttachments is empty,\n' + 'should re-upload base64 succeeds then replace src with that CID and add attachment to result', + () async { + const htmlContent = ''; + final inlineAttachments = {}; + final uploadUri = Uri.parse('https://example.com/upload'); + + when(mockFileUploader.uploadAttachment(any, any, any)) + .thenAnswer((_) async => Attachment( + blobId: Id('test123'), + type: MediaType('image', 'png'), + disposition: ContentDisposition.inline, + cid: 'test123', + )); + + 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', @@ -101,6 +132,7 @@ void main() { expect(imgTag?.attributes['src'], 'cid:new-uuid-123'); expect(result.value2.length, 1); expect(result.value2.first.cid, 'new-uuid-123'); + verify(mockUuid.v1()).called(1); verify(mockFileUploader.uploadAttachment(any, any, uploadUri)).called(1); }); @@ -151,9 +183,10 @@ void main() { 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(imgTags[0].attributes['src'], contains('cid:')); + expect(imgTags[1].attributes['src'], contains('cid:')); expect(result.value2.length, 2); + verify(mockUuid.v1()).called(2); verify(mockFileUploader.uploadAttachment(any, any, uploadUri)).called(2); }); @@ -198,6 +231,7 @@ void main() { expect(imgTags[1].attributes['src'], 'data:image/jpeg;base64,/9j/4AAQSkZJRg=='); expect(result.value2.length, 1); expect(result.value2.first.cid, 'uuid1'); + verify(mockUuid.v1()).called(2); verify(mockFileUploader.uploadAttachment(any, any, uploadUri)).called(2); }); @@ -229,6 +263,7 @@ void main() { 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(mockUuid.v1()).called(2); verify(mockFileUploader.uploadAttachment(any, any, uploadUri)).called(2); }); @@ -256,6 +291,7 @@ void main() { expect(imgTags.length, 1); expect(imgTags[0].attributes['src'], 'data:image/png;base64,invalid-base64-data'); expect(result.value2.length, 0); + verify(mockUuid.v1()).called(1); }); test( @@ -282,6 +318,7 @@ void main() { expect(imgTags.length, 1); expect(imgTags[0].attributes['src'], 'data:image/png;base64,iVBORw0KGgo='); expect(result.value2.length, 0); + verify(mockUuid.v1()).called(1); verify(mockFileUploader.uploadAttachment(any, any, any)).called(1); }); });