Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user