using System.Text.Json; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using MimeKit; using Omika.Core.Contracts.Documents; using Omika.Core.Interfaces; using Omika.Core.Services; namespace Omika.Tests.Services; /// /// Tests des images inline dans le corps HTML d'un email : résolution ChpImage /// (chemin absolu / Base64), marqueurs DOCX → <img> data URI (aperçu), puis /// conversion SMTP data URI → ressources MIME CID (multipart/related). Batik non /// régressé. /// public class EmailInlineImageTests { private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ private static byte[] OctetsJpeg() => [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46]; private static byte[] OctetsPng() => [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D]; private static byte[] OctetsBmp() => [0x42, 0x4D, 0x00, 0x00, 0x00, 0x00]; private static string EcrireFichierTemporaire(byte[] octets, string extension) { string chemin = Path.Combine(Path.GetTempPath(), $"omika-img-{Guid.NewGuid():N}.{extension}"); File.WriteAllBytes(chemin, octets); return chemin; } private static RapportChampDto ChampImageSimple(string rubrique, string? valeur) => new() { Id = "1", Rubrique = rubrique, Type = "ChpImage", TypeMembre = "ChpImage", ListeDonnesBD = valeur }; private static RapportChampDto TableauPhotos(string rubrique, string prefixe, string json, params (string Rubrique, string Type, string MembreMob)[] colonnes) => new() { Id = "1", Rubrique = rubrique, Type = "Tableau", ListeDonnesBD = json, ListeMembre = colonnes.Select(c => new RapportChampSousElementDto { Rubrique = $"{prefixe}.{c.Rubrique}$", Type = c.Type, TypeMembre = c.Type, MembreMob = c.MembreMob }).ToList() }; private static byte[] CreerDocx(params string[] paragraphes) { using var stream = new MemoryStream(); using (WordprocessingDocument doc = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document)) { MainDocumentPart main = doc.AddMainDocumentPart(); main.Document = new Document(); Body body = new Body(); foreach (string texte in paragraphes) { body.Append(new Paragraph(new Run(new Text(texte)))); } main.Document.Append(body); main.Document.Save(); } return stream.ToArray(); } private static byte[] CreerDocxAvecTable(params string[][] lignes) { using var stream = new MemoryStream(); using (WordprocessingDocument doc = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document)) { MainDocumentPart main = doc.AddMainDocumentPart(); main.Document = new Document(); Body body = new Body(); var table = new Table(); foreach (string[] ligne in lignes) { var row = new TableRow(); foreach (string cellule in ligne) { row.Append(new TableCell(new Paragraph(new Run(new Text(cellule))))); } table.Append(row); } body.Append(table); main.Document.Append(body); main.Document.Save(); } return stream.ToArray(); } private static string RendreCorps(IReadOnlyList champs, byte[] docx, out IReadOnlyList images) { var liste = new List(); byte[] rendu = EmailDocxRenderer.Rendre(docx, champs, liste); images = liste; return EmailImageInliner.Inliner(DocxHtmlConverter.Convertir(rendu), liste); } // ------------------------------------------------------------------ // TEST 1 : UNPHOTO JPEG chemin absolu → data URI + ressource inline // ------------------------------------------------------------------ [Fact] public void UnPhoto_JpegCheminAbsolu_ProduitImageInline() { string chemin = EcrireFichierTemporaire(OctetsJpeg(), "jpg"); string html = RendreCorps( [ChampImageSimple("$UNPHOTO.IMAGE$", chemin)], CreerDocx("Photo: $UNPHOTO.IMAGE$"), out IReadOnlyList images); Assert.Single(images); Assert.Equal("image/jpeg", images[0].MimeType); Assert.Contains(" ressources) = EmailSmtpInline.Convertir(html); Assert.Contains("cid:", htmlSmtp); Assert.Single(ressources); Assert.Equal("image/jpeg", ressources[0].MimeType); } // ------------------------------------------------------------------ // TEST 2 : TABPHOTO 2 photos → 2 images distinctes // ------------------------------------------------------------------ [Fact] public void TabPhoto_DeuxPhotos_DeuxImagesDistinctes() { string chemin1 = EcrireFichierTemporaire(OctetsJpeg(), "jpg"); string chemin2 = EcrireFichierTemporaire(OctetsPng(), "png"); string json = JsonSerializer.Serialize(new[] { new { cheminComplet = chemin1, date = "30/01/2026", notes = "Note 1", nom = "a.jpg" }, new { cheminComplet = chemin2, date = "30/01/2026", notes = "Note 2", nom = "b.png" } }, Json); var champs = new[] { TableauPhotos( "$TABPHOTO$", "$TABPHOTO", json, ("IMAGE", "ChpImage", "cheminComplet"), ("DATEIMAGE", "ChpSimpleDate", "saDate"), ("NOTEIMAGE", "ChpSimpleChaine", "saNotes")) }; string html = RendreCorps( champs, CreerDocxAvecTable(["$TABPHOTO.IMAGE$", "$TABPHOTO.DATEIMAGE$", "$TABPHOTO.NOTEIMAGE$"]), out IReadOnlyList images); // Deux images distinctes (JPEG + PNG). Assert.Equal(2, images.Count); Assert.Contains("data:image/jpeg;base64,", html); Assert.Contains("data:image/png;base64,", html); (string htmlSmtp, IReadOnlyList ressources) = EmailSmtpInline.Convertir(html); Assert.Equal(2, ressources.Count); Assert.Equal(2, ressources.Select(r => r.Cid).Distinct().Count()); } // ------------------------------------------------------------------ // TEST 3 : photo + date + note + extra sur la même ligne // ------------------------------------------------------------------ [Fact] public void Photo_Date_Note_Extra_MemeLigne() { string chemin1 = EcrireFichierTemporaire(OctetsJpeg(), "jpg"); string chemin2 = EcrireFichierTemporaire(OctetsJpeg(), "jpg"); string json = JsonSerializer.Serialize(new[] { new { cheminComplet = chemin1, date = "30/01/2026", notes = "Note 1", extra = "Extra A", nom = "a.jpg" }, new { cheminComplet = chemin2, date = "31/01/2026", notes = "Note 2", extra = "Extra B", nom = "b.jpg" } }, Json); var champs = new[] { TableauPhotos( "$TABPHOTO$", "$TABPHOTO", json, ("IMAGE", "ChpImage", "cheminComplet"), ("DATEIMAGE", "ChpSimpleDate", "saDate"), ("NOTEIMAGE", "ChpSimpleChaine", "saNotes"), ("EXTRA", "ChpSimpleChaine", "saExtra")) }; string html = RendreCorps( champs, CreerDocxAvecTable(["$TABPHOTO.IMAGE$", "$TABPHOTO.DATEIMAGE$", "$TABPHOTO.NOTEIMAGE$", "$TABPHOTO.EXTRA$"]), out _); // Chaque ligne contient son image, sa date, sa note et son extra. Assert.Contains("data:image/jpeg;base64,", html); Assert.Contains("30/01/2026", html); Assert.Contains("31/01/2026", html); Assert.Contains("Note 1", html); Assert.Contains("Note 2", html); Assert.Contains("Extra A", html); Assert.Contains("Extra B", html); Assert.DoesNotContain("$TABPHOTO", html); } // ------------------------------------------------------------------ // TEST 4 : signature PNG Base64 → ressource inline // ------------------------------------------------------------------ [Fact] public void Signature_PngBase64_RessourceInline() { string base64 = Convert.ToBase64String(OctetsPng()); string html = RendreCorps( [ChampImageSimple("$UNCR.IMAGESIGNATURE$", base64)], CreerDocx("Signature: $UNCR.IMAGESIGNATURE$"), out IReadOnlyList images); Assert.Single(images); Assert.Equal("image/png", images[0].MimeType); Assert.Contains(" images); Assert.Empty(images); Assert.DoesNotContain("$UNPHOTO.IMAGE$", html); Assert.DoesNotContain("absent-", html); Assert.DoesNotContain(" images = mime.BodyParts .OfType() .Where(p => p.ContentType.MediaType == "image") .ToList(); Assert.Single(images); string cid = images[0].ContentId; Assert.Contains($"src=\"cid:{cid}\"", htmlSmtp); Assert.DoesNotContain("base64", htmlSmtp); } // ------------------------------------------------------------------ // TEST 8 : placeholder ChpImage absent du résultat // ------------------------------------------------------------------ [Fact] public void Placeholder_ChpImage_Absent_DuResultat() { string chemin = EcrireFichierTemporaire(OctetsJpeg(), "jpg"); string html = RendreCorps( [ChampImageSimple("$TABPHOTO.IMAGE$", chemin)], CreerDocx("Photo: $TABPHOTO.IMAGE$"), out _); Assert.DoesNotContain("$TABPHOTO.IMAGE$", html); Assert.Contains(" toujours substituée) // ------------------------------------------------------------------ [Fact] public void Batik_SimplePlaceholder_ToujoursRemplace() { var champs = new[] { new RapportChampDto { Id = "1", Rubrique = "", Type = "ChpSimpleChaine", TypeMembre = "ChpSimpleChaine", ListeDonnesBD = "Acme" } }; string html = RendreCorps(champs, CreerDocx("Client: "), out IReadOnlyList images); Assert.Empty(images); Assert.Contains("Client: Acme", html); Assert.DoesNotContain("", html); } }