using System.IO.Packaging; using System.Text.Json; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using Omika.App.ViewModels; using Omika.Core.Contracts.Documents; using Omika.Core.Services; namespace Omika.Tests.Mobile.Local; /// /// Trace E2E d'une photo de rapport : la valeur serveur (ListeDonnesBD avec /// cheminComplet + notes) doit survivre au roundtrip mobile /// (désérialisation → sélection → sérialisation ListeDonnesMob) puis être /// exploitée par (image + note dans le DOCX). /// public class RapportPhotoRoundtripTests { private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ private static byte[] FakePng(int largeur, int hauteur) { byte[] png = new byte[24]; png[0] = 0x89; png[1] = 0x50; png[2] = 0x4E; png[3] = 0x47; png[4] = 0x0D; png[5] = 0x0A; png[6] = 0x1A; png[7] = 0x0A; png[12] = 0x49; png[13] = 0x48; png[14] = 0x44; png[15] = 0x52; png[16] = (byte)(largeur >> 24); png[17] = (byte)(largeur >> 16); png[18] = (byte)(largeur >> 8); png[19] = (byte)largeur; png[20] = (byte)(hauteur >> 24); png[21] = (byte)(hauteur >> 16); png[22] = (byte)(hauteur >> 8); png[23] = (byte)hauteur; return png; } private static string PhotoJson(string id, string? guid, string nom, string date, string? cheminComplet, string? notes) => JsonSerializer.Serialize(new[] { new { id, guid, nom, date, chemin = "SousDossier", cheminComplet, notes } }, Json); private static RapportChampSousElementDto SousElement(string rubrique, string? membreMob, string? type = null) => new() { Rubrique = rubrique, MembreMob = membreMob, Type = type ?? "ChpSimpleChaine" }; private static RapportChampDto TableauPhoto(string listeDonnesBD, params RapportChampSousElementDto[] membres) => new() { Id = "1", Nom = "Photos", Rubrique = "$TABPHOTO$", Type = "Tableau", GroupeApp = "PHOTO_INTER", ListeDonnesBD = listeDonnesBD, ListeMembre = membres }; private static byte[] CreerDocxAvecTableau(params string[] cellules) { 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(); var row = new TableRow(); foreach (string cellule in cellules) { 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 (int ImageParts, int Drawings, string Texte) Inspecter(byte[] docx) { int imageParts; using (var pkgStream = new MemoryStream(docx)) using (Package package = Package.Open(pkgStream, FileMode.Open, FileAccess.Read)) { imageParts = package.GetParts() .Count(p => p.Uri.OriginalString.StartsWith("/word/media/", StringComparison.Ordinal)); } using var stream = new MemoryStream(docx); using WordprocessingDocument doc = WordprocessingDocument.Open(stream, false); MainDocumentPart main = doc.MainDocumentPart!; int drawings = main.Document.Body!.Descendants().Count(); string texte = string.Concat(main.Document.Body.Descendants().Select(t => t.Text)); return (imageParts, drawings, texte); } // ------------------------------------------------------------------ // Tests // ------------------------------------------------------------------ [Fact] public void Photo_Roundtrip_CheminCompletEtNotes_SontConserves() { string chemin = Path.Combine(Path.GetTempPath(), "omika-photo-e2e", Guid.NewGuid().ToString("N"), "photo.png"); Directory.CreateDirectory(Path.GetDirectoryName(chemin)!); File.WriteAllBytes(chemin, FakePng(320, 240)); try { string json = PhotoJson("35", Guid.NewGuid().ToString(), "photo123.jpg", "2024-01-01", chemin, "Peinture mur nord"); var edition = new RapportChampEdition(TableauPhoto( json, SousElement("$TABPHOTO.IMAGE$", "cheminComplet", "ChpImage"), SousElement("$TABPHOTO.DATEIMAGE$", "saDate"), SousElement("$TABPHOTO.NOTEIMAGE$", "saNotes"))); // Le mobile sélectionne la photo. edition.Options[0].EstSelectionne = true; string mob = edition.SerialiserValeur()!; using JsonDocument doc = JsonDocument.Parse(mob); JsonElement item = doc.RootElement.EnumerateArray().Single(); // Le chemin physique ABSOLU et la note survivent au roundtrip. Assert.Equal(chemin, item.GetProperty("cheminComplet").GetString()); Assert.Equal("Peinture mur nord", item.GetProperty("notes").GetString()); Assert.Equal("photo123.jpg", item.GetProperty("nom").GetString()); } finally { if (Directory.Exists(Path.GetDirectoryName(chemin)!)) { Directory.Delete(Path.GetDirectoryName(chemin)!, recursive: true); } } } [Fact] public void Photo_E2E_DocxContientImageEtNote() { string chemin = Path.Combine(Path.GetTempPath(), "omika-photo-e2e", Guid.NewGuid().ToString("N"), "photo.png"); Directory.CreateDirectory(Path.GetDirectoryName(chemin)!); File.WriteAllBytes(chemin, FakePng(320, 240)); try { string json = PhotoJson("35", Guid.NewGuid().ToString(), "photo123.jpg", "2024-01-01", chemin, "Peinture mur nord"); var edition = new RapportChampEdition(TableauPhoto( json, SousElement("$TABPHOTO.IMAGE$", "cheminComplet", "ChpImage"), SousElement("$TABPHOTO.DATEIMAGE$", "saDate"), SousElement("$TABPHOTO.NOTEIMAGE$", "saNotes"))); edition.Options[0].EstSelectionne = true; // Ce que le mobile envoie au serveur (ListeDonnesMob). var champMob = new RapportChampDto { Id = "1", Rubrique = "$TABPHOTO$", Type = "Tableau", ListeDonnesMob = edition.SerialiserValeur(), ListeMembre = [ SousElement("$TABPHOTO.IMAGE$", "cheminComplet", "ChpImage"), SousElement("$TABPHOTO.DATEIMAGE$", "saDate"), SousElement("$TABPHOTO.NOTEIMAGE$", "saNotes") ] }; byte[] docx = CreerDocxAvecTableau("$TABPHOTO.IMAGE$", "$TABPHOTO.DATEIMAGE$", "$TABPHOTO.NOTEIMAGE$"); byte[] resultat = RapportPdfService.RemplirRubriques(docx, new[] { champMob }); var (imageParts, drawings, texte) = Inspecter(resultat); // L'image est réellement insérée (ImagePart + Drawing) et la note présente. Assert.Equal(1, imageParts); Assert.Equal(1, drawings); Assert.Contains("2024-01-01", texte); Assert.Contains("Peinture mur nord", texte); Assert.DoesNotContain("$TABPHOTO", texte); } finally { if (Directory.Exists(Path.GetDirectoryName(chemin)!)) { Directory.Delete(Path.GetDirectoryName(chemin)!, recursive: true); } } } [Fact] public void Photo_NoteNomDifferents_MappingCorrect() { // Nom ≠ note : l'UI ne doit jamais utiliser le nom de fichier comme note. string json = PhotoJson("35", Guid.NewGuid().ToString(), "photo123.jpg", "2024-01-01", null, "Peinture mur nord"); var edition = new RapportChampEdition(TableauPhoto(json, SousElement("$TABPHOTO.NOTEIMAGE$", "saNotes"))); TableauOptionEdition option = Assert.Single(edition.Options); Assert.Equal("photo123.jpg", option.NomPhoto); Assert.Equal("Peinture mur nord", option.NotePhoto); } [Fact] public void Photo_NoteAbsente_NomJamaisUtiliseCommeNote() { // Aucune note : la note est vide (jamais le nom de fichier). string json = PhotoJson("35", Guid.NewGuid().ToString(), "photo123.jpg", "2024-01-01", null, null); var edition = new RapportChampEdition(TableauPhoto(json, SousElement("$TABPHOTO.NOTEIMAGE$", "saNotes"))); TableauOptionEdition option = Assert.Single(edition.Options); Assert.Equal("photo123.jpg", option.NomPhoto); Assert.Null(option.NotePhoto); } }