Files
2026-10-23 18:46:50 +02:00

380 lines
14 KiB
C#

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;
/// <summary>
/// Tests des images inline dans le corps HTML d'un email : résolution ChpImage
/// (chemin absolu / Base64), marqueurs DOCX → &lt;img&gt; data URI (aperçu), puis
/// conversion SMTP data URI → ressources MIME CID (multipart/related). Batik non
/// régressé.
/// </summary>
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<RapportChampDto> champs, byte[] docx, out IReadOnlyList<EmailInlineImage> images)
{
var liste = new List<EmailInlineImage>();
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<EmailInlineImage> images);
Assert.Single(images);
Assert.Equal("image/jpeg", images[0].MimeType);
Assert.Contains("<img src=\"data:image/jpeg;base64,", html);
Assert.DoesNotContain("$UNPHOTO.IMAGE$", html);
// Conversion SMTP : data URI → cid + ressource.
(string htmlSmtp, IReadOnlyList<EmailSmtpInline.Ressource> 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<EmailInlineImage> 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<EmailSmtpInline.Ressource> 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<EmailInlineImage> images);
Assert.Single(images);
Assert.Equal("image/png", images[0].MimeType);
Assert.Contains("<img src=\"data:image/png;base64,", html);
Assert.DoesNotContain("$UNCR.IMAGESIGNATURE$", html);
}
// ------------------------------------------------------------------
// TEST 5 : image absente → email généré, aucun chemin visible
// ------------------------------------------------------------------
[Fact]
public void ImageAbsente_EmailGenere_SansChemin()
{
string cheminInexistant = Path.Combine(Path.GetTempPath(), $"absent-{Guid.NewGuid():N}.jpg");
string html = RendreCorps(
[ChampImageSimple("$UNPHOTO.IMAGE$", cheminInexistant)],
CreerDocx("Photo: $UNPHOTO.IMAGE$"),
out IReadOnlyList<EmailInlineImage> images);
Assert.Empty(images);
Assert.DoesNotContain("$UNPHOTO.IMAGE$", html);
Assert.DoesNotContain("absent-", html);
Assert.DoesNotContain("<img", html);
}
// ------------------------------------------------------------------
// TEST 7 : aucun chemin serveur (C:\, Documents_Demo) dans le HTML
// ------------------------------------------------------------------
[Fact]
public void AucunCheminAbsolu_DansHtml()
{
string chemin = EcrireFichierTemporaire(OctetsJpeg(), "jpg");
string html = RendreCorps(
[ChampImageSimple("$UNPHOTO.IMAGE$", chemin)],
CreerDocx("Photo: $UNPHOTO.IMAGE$"),
out _);
Assert.DoesNotContain(":\\", html);
Assert.DoesNotContain(Path.GetFileName(chemin), html);
Assert.Contains("data:image/jpeg;base64,", html);
}
// ------------------------------------------------------------------
// TEST 6 : aucun Base64 brut dans le HTML SMTP (CID)
// ------------------------------------------------------------------
[Fact]
public void Smtp_AucunBase64Brut_DansHtml()
{
string chemin = EcrireFichierTemporaire(OctetsJpeg(), "jpg");
string htmlApercu = RendreCorps(
[ChampImageSimple("$UNPHOTO.IMAGE$", chemin)],
CreerDocx("Photo: $UNPHOTO.IMAGE$"),
out _);
(string htmlSmtp, _) = EmailSmtpInline.Convertir(htmlApercu);
Assert.Contains("cid:", htmlSmtp);
Assert.DoesNotContain("base64", htmlSmtp);
}
// ------------------------------------------------------------------
// TEST 9 : MIME resource ContentId correspond au src="cid:"
// ------------------------------------------------------------------
[Fact]
public void Mime_ContentId_Correspond_SrcCid()
{
string chemin = EcrireFichierTemporaire(OctetsJpeg(), "jpg");
string htmlApercu = RendreCorps(
[ChampImageSimple("$UNPHOTO.IMAGE$", chemin)],
CreerDocx("Photo: $UNPHOTO.IMAGE$"),
out _);
var message = new EmailMessage
{
Sujet = "Sujet",
CorpsHtml = htmlApercu,
Destinataires = ["dest@test.fr"]
};
MimeMessage mime = MailKitEmailSender.ConstruireMime(message, "expediteur@test.fr");
string htmlSmtp = mime.HtmlBody;
Assert.NotNull(htmlSmtp);
Assert.Contains("cid:", htmlSmtp);
IReadOnlyList<MimePart> images = mime.BodyParts
.OfType<MimePart>()
.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("<img", html);
}
// ------------------------------------------------------------------
// TEST 10 : Batik non-régression (syntaxe <...> toujours substituée)
// ------------------------------------------------------------------
[Fact]
public void Batik_SimplePlaceholder_ToujoursRemplace()
{
var champs = new[]
{
new RapportChampDto
{
Id = "1",
Rubrique = "<CLIENT.NOM>",
Type = "ChpSimpleChaine",
TypeMembre = "ChpSimpleChaine",
ListeDonnesBD = "Acme"
}
};
string html = RendreCorps(champs, CreerDocx("Client: <CLIENT.NOM>"), out IReadOnlyList<EmailInlineImage> images);
Assert.Empty(images);
Assert.Contains("Client: Acme", html);
Assert.DoesNotContain("<CLIENT.NOM>", html);
}
}