380 lines
14 KiB
C#
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 → <img> 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);
|
|
}
|
|
}
|