Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
using MimeKit;
|
||||
using Omika.Core.Interfaces;
|
||||
using Omika.Core.Services;
|
||||
|
||||
namespace Omika.Tests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Tests E2E de la STRUCTURE MIME des images inline Email : les images doivent
|
||||
/// être inline dans le corps (multipart/related + Content-Disposition:inline +
|
||||
/// cid exact), JAMAIS en pièces jointes classiques (multipart/mixed/attachment).
|
||||
/// </summary>
|
||||
public class EmailMimeStructureTests
|
||||
{
|
||||
private static byte[] Jpeg() => [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46];
|
||||
private static byte[] Jpeg2() => [0xFF, 0xD8, 0xFF, 0xE1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
|
||||
private static byte[] Png() => [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00];
|
||||
|
||||
private static string DataUri(byte[] bytes, string mime)
|
||||
=> $"data:{mime};base64,{Convert.ToBase64String(bytes)}";
|
||||
|
||||
private static EmailMessage MessageAvecCorpsHtml(string html, string? corpsTexte = null)
|
||||
=> new()
|
||||
{
|
||||
Sujet = "Test",
|
||||
CorpsHtml = html,
|
||||
CorpsTexte = corpsTexte,
|
||||
Destinataires = ["dest@test.fr"]
|
||||
};
|
||||
|
||||
private static IEnumerable<MimePart> Parcourir(MimeEntity entity)
|
||||
{
|
||||
if (entity is MimePart part)
|
||||
{
|
||||
yield return part;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (entity is Multipart multipart)
|
||||
{
|
||||
foreach (MimeEntity enfant in multipart)
|
||||
{
|
||||
foreach (MimePart descendant in Parcourir(enfant))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<MimePart> Images(MimeMessage mime)
|
||||
=> Parcourir(mime.Body).Where(p => p.ContentType.MediaType == "image").ToList();
|
||||
|
||||
private static List<MimePart> Attachements(MimeMessage mime)
|
||||
=> Parcourir(mime.Body)
|
||||
.Where(p => p.ContentDisposition?.Disposition == ContentDisposition.Attachment)
|
||||
.ToList();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 1 : 2 photos → LinkedResources=2, Attachments=0
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void DeuxPhotos_DeuxRessourcesInline_ZeroAttachements()
|
||||
{
|
||||
string html =
|
||||
$"<table><tr><td><img src=\"{DataUri(Jpeg(), "image/jpeg")}\"></td><td>30/01/2026</td></tr>" +
|
||||
$"<tr><td><img src=\"{DataUri(Jpeg2(), "image/jpeg")}\"></td><td>31/01/2026</td></tr></table>";
|
||||
|
||||
MimeMessage mime = MailKitEmailSender.ConstruireMime(MessageAvecCorpsHtml(html), "exp@test.fr");
|
||||
|
||||
Assert.Equal(2, Images(mime).Count);
|
||||
Assert.Empty(Attachements(mime));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 2 : toutes les ressources ont Content-Disposition:inline
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void ToutesRessources_ContentDispositionInline()
|
||||
{
|
||||
string html =
|
||||
$"<img src=\"{DataUri(Jpeg(), "image/jpeg")}\">" +
|
||||
$"<img src=\"{DataUri(Png(), "image/png")}\">";
|
||||
|
||||
MimeMessage mime = MailKitEmailSender.ConstruireMime(MessageAvecCorpsHtml(html), "exp@test.fr");
|
||||
|
||||
List<MimePart> images = Images(mime);
|
||||
Assert.NotEmpty(images);
|
||||
Assert.All(images, img => Assert.Equal(ContentDisposition.Inline, img.ContentDisposition?.Disposition));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 3 : HTML cid ↔ ContentId exact
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void HtmlCid_CorrespondExactementContentId()
|
||||
{
|
||||
string html = $"<img src=\"{DataUri(Jpeg(), "image/jpeg")}\">";
|
||||
|
||||
MimeMessage mime = MailKitEmailSender.ConstruireMime(MessageAvecCorpsHtml(html), "exp@test.fr");
|
||||
|
||||
string htmlFinal = mime.HtmlBody;
|
||||
Assert.NotNull(htmlFinal);
|
||||
|
||||
List<MimePart> images = Images(mime);
|
||||
Assert.Single(images);
|
||||
|
||||
string cid = images[0].ContentId;
|
||||
Assert.False(string.IsNullOrEmpty(cid));
|
||||
Assert.Contains($"src=\"cid:{cid}\"", htmlFinal);
|
||||
// Le HTML ne contient pas les chevrons <…> du header MIME.
|
||||
Assert.DoesNotContain($"cid:<{cid}>", htmlFinal);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 4 : multipart/related contient les images
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Images_DansMultipartRelated()
|
||||
{
|
||||
string html = $"<img src=\"{DataUri(Jpeg(), "image/jpeg")}\">";
|
||||
|
||||
MimeMessage mime = MailKitEmailSender.ConstruireMime(MessageAvecCorpsHtml(html), "exp@test.fr");
|
||||
|
||||
// Sans texte alternatif : corps racine = multipart/related (MultipartRelated).
|
||||
Assert.IsAssignableFrom<Multipart>(mime.Body);
|
||||
Assert.Equal("related", mime.Body.ContentType.MediaSubtype);
|
||||
Assert.Single(Images(mime));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 5 : aucune image sous multipart/mixed (pièce jointe)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void AucuneImage_SousMultipartMixed()
|
||||
{
|
||||
string html =
|
||||
$"<img src=\"{DataUri(Jpeg(), "image/jpeg")}\">" +
|
||||
$"<img src=\"{DataUri(Png(), "image/png")}\">";
|
||||
|
||||
MimeMessage mime = MailKitEmailSender.ConstruireMime(MessageAvecCorpsHtml(html), "exp@test.fr");
|
||||
|
||||
Assert.Empty(Attachements(mime));
|
||||
// Aucune image ne doit se trouver sous un conteneur multipart/mixed.
|
||||
foreach (MimePart image in Images(mime))
|
||||
{
|
||||
Assert.False(ParentMixed(mime.Body, image), "Une image inline est tombée sous multipart/mixed.");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 6 : 2 photos + signature → 3 ressources distinctes
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void DeuxPhotos_UneSignature_TroisRessourcesDistinctes()
|
||||
{
|
||||
string html =
|
||||
$"<img src=\"{DataUri(Jpeg(), "image/jpeg")}\">" +
|
||||
$"<img src=\"{DataUri(Jpeg2(), "image/jpeg")}\">" +
|
||||
$"<img src=\"{DataUri(Png(), "image/png")}\">";
|
||||
|
||||
MimeMessage mime = MailKitEmailSender.ConstruireMime(MessageAvecCorpsHtml(html), "exp@test.fr");
|
||||
|
||||
List<MimePart> images = Images(mime);
|
||||
Assert.Equal(3, images.Count);
|
||||
Assert.Equal(3, images.Select(i => i.ContentId).Distinct().Count());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 7 : plusieurs data URI → toutes converties (aucune ignorée)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void PlusieursDataUri_ToutesConverties()
|
||||
{
|
||||
string html =
|
||||
$"<img src=\"{DataUri(Jpeg(), "image/jpeg")}\">" +
|
||||
$"<img src=\"{DataUri(Jpeg2(), "image/jpeg")}\">" +
|
||||
$"<img src=\"{DataUri(Png(), "image/png")}\">";
|
||||
|
||||
(string htmlSmtp, IReadOnlyList<EmailSmtpInline.Ressource> ressources) = EmailSmtpInline.Convertir(html);
|
||||
|
||||
Assert.Equal(3, ressources.Count);
|
||||
Assert.DoesNotContain("data:", htmlSmtp);
|
||||
Assert.Equal(3, System.Text.RegularExpressions.Regex.Matches(htmlSmtp, "cid:").Count);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 8 : attributs img différents (guillemets simples / ordre) → robuste
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void AttributsImgDifferents_ConversionRobuste()
|
||||
{
|
||||
// Guillemets simples + attribut style AVANT src.
|
||||
string a = $"<img style=\"width:100%\" src='{DataUri(Jpeg(), "image/jpeg")}'>";
|
||||
// Guillemets doubles + src en premier.
|
||||
string b = $"<img src=\"{DataUri(Png(), "image/png")}\" alt=\"sig\">";
|
||||
|
||||
(string htmlSmtp, IReadOnlyList<EmailSmtpInline.Ressource> ressources) = EmailSmtpInline.Convertir(a + b);
|
||||
|
||||
Assert.Equal(2, ressources.Count);
|
||||
Assert.DoesNotContain("data:", htmlSmtp);
|
||||
Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(htmlSmtp, "cid:").Count);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 9 : .eml sauvegardé → relecture MimeKit → images inline retrouvées
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Eml_SauvegardePuisRelu_ImagesInlineRetrouvees()
|
||||
{
|
||||
string html =
|
||||
$"<img src=\"{DataUri(Jpeg(), "image/jpeg")}\">" +
|
||||
$"<img src=\"{DataUri(Png(), "image/png")}\">";
|
||||
|
||||
MimeMessage mime = MailKitEmailSender.ConstruireMime(MessageAvecCorpsHtml(html), "exp@test.fr");
|
||||
|
||||
string repertoire = Path.Combine(Path.GetTempPath(), "omika-eml-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(repertoire);
|
||||
string eml = Path.Combine(repertoire, "email-inline-diagnostic.eml");
|
||||
mime.WriteTo(eml);
|
||||
|
||||
MimeMessage relu = MimeMessage.Load(eml);
|
||||
|
||||
Assert.Equal(2, Images(relu).Count);
|
||||
Assert.All(Images(relu), img => Assert.Equal(ContentDisposition.Inline, img.ContentDisposition?.Disposition));
|
||||
Assert.Empty(Attachements(relu));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TEST 10 : Batik non-régression (pas d'image → aucun changement)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Batik_SansImage_AucuneRessourceInline()
|
||||
{
|
||||
// Batik : corps HTML simple (aucune image) → aucune ressource inline.
|
||||
MimeMessage mime = MailKitEmailSender.ConstruireMime(
|
||||
MessageAvecCorpsHtml("<p>Bonjour Batik</p>", corpsTexte: "Bonjour Batik"),
|
||||
"exp@test.fr");
|
||||
|
||||
Assert.Empty(Images(mime));
|
||||
Assert.Empty(Attachements(mime));
|
||||
Assert.Contains("Bonjour Batik", mime.HtmlBody);
|
||||
}
|
||||
|
||||
private static bool ParentMixed(MimeEntity parent, MimePart cible)
|
||||
{
|
||||
if (parent is not Multipart multipart)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (multipart.ContentType.MediaSubtype == "mixed" && multipart.Contains(cible))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return multipart.OfType<MimeEntity>().Any(enfant => ParentMixed(enfant, cible));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user