using Omika.App.Services; using Omika.App.ViewModels; using Omika.Core.Constants; using SkiaSharp; namespace Omika.Tests.Mobile.Local; /// /// Tests de l'orientation EXIF et des règles de la politique de poids des photos. /// /// L'orientation est vérifiée sur une image 2×1 dont les deux pixels sont /// distincts, ce qui permet de valider les huit orientations sans dépendre d'un /// fichier EXIF réel. /// public class ImageOrientationTests { private static readonly SKColor Rouge = new(255, 0, 0); private static readonly SKColor Bleu = new(0, 0, 255); /// Source 2×1 : pixel gauche rouge, pixel droit bleu. private static SKBitmap CreateSource() { var bitmap = new SKBitmap(new SKImageInfo(2, 1, SKColorType.Rgba8888, SKAlphaType.Premul)); bitmap.SetPixel(0, 0, Rouge); bitmap.SetPixel(1, 0, Bleu); return bitmap; } private static SKColor Pixel(SKBitmap bitmap, int x, int y) => bitmap.GetPixel(x, y); [Fact] public void TopLeft_AucuneTransformation() { // L'orientation normale ne doit produire aucune copie inutile. using SKBitmap source = CreateSource(); Assert.Null(ImageOrientation.ApplyOrientation(source, SKEncodedOrigin.TopLeft)); } [Theory] // Miroir horizontal : [R B] → [B R] [InlineData(SKEncodedOrigin.TopRight, 2, 1, 1, 0, true)] // Rotation 180° : [R B] → [B R] [InlineData(SKEncodedOrigin.BottomRight, 2, 1, 1, 0, true)] // Miroir vertical sur une seule ligne : identité [R B] [InlineData(SKEncodedOrigin.BottomLeft, 2, 1, 0, 0, true)] // Transposition : colonne [R ; B] [InlineData(SKEncodedOrigin.LeftTop, 1, 2, 0, 0, true)] // Rotation 90° horaire : colonne [R ; B] [InlineData(SKEncodedOrigin.RightTop, 1, 2, 0, 0, true)] // Miroir + 90° horaire : colonne [B ; R] [InlineData(SKEncodedOrigin.RightBottom, 1, 2, 0, 0, false)] // Rotation 90° anti-horaire : colonne [B ; R] [InlineData(SKEncodedOrigin.LeftBottom, 1, 2, 0, 0, false)] public void Orientation_AxesEtCouleursCorrects( SKEncodedOrigin origin, int expectedWidth, int expectedHeight, int xDuPremierPixel, int yDuPremierPixel, bool premierPixelRouge) { // Act using SKBitmap source = CreateSource(); using SKBitmap? result = ImageOrientation.ApplyOrientation(source, origin); // Assert : dimensions permutées quand l'orientation le requiert… Assert.NotNull(result); Assert.Equal(expectedWidth, result.Width); Assert.Equal(expectedHeight, result.Height); // … et orientation réellement appliquée (aucun pixel perdu, aucun mélange). SKColor premier = Pixel(result, xDuPremierPixel, yDuPremierPixel); SKColor attendu = premierPixelRouge ? Rouge : Bleu; Assert.Equal(attendu, premier); // Les deux couleurs d'origine sont toujours présentes exactement une fois. int rouges = 0; int bleus = 0; for (int x = 0; x < result.Width; x++) { for (int y = 0; y < result.Height; y++) { SKColor pixel = Pixel(result, x, y); if (pixel == Rouge) rouges++; if (pixel == Bleu) bleus++; } } Assert.Equal(1, rouges); Assert.Equal(1, bleus); } } /// /// Tests des règles de la politique de poids des photos : détection d'image, /// normalisation de la valeur configurée et formatage lisible. /// public class PhotoOptimizationSettingsTests { [Theory] [InlineData("photo.jpg", true)] [InlineData("photo.JPEG", true)] [InlineData("capture.png", true)] [InlineData("anim.gif", true)] [InlineData("image.webp", true)] [InlineData("photo.heic", true)] [InlineData("scan.tiff", true)] [InlineData("rapport.pdf", false)] [InlineData("lettre.docx", false)] [InlineData("classeur.xlsx", false)] [InlineData("archive.zip", false)] [InlineData("notes.txt", false)] [InlineData("sans_extension", false)] [InlineData("", false)] public void IsImageExtension_ClasseCorrectement(string fileName, bool expected) => Assert.Equal(expected, ImageFileTypes.IsImageExtension(fileName)); [Fact] public void IsImage_MimeSuffitSansExtension() => Assert.True(ImageFileTypes.IsImage("fichier", "image/jpeg")); [Fact] public void IsImage_ExtensionSuffitSansMime() => Assert.True(ImageFileTypes.IsImage("photo.jpg", null)); [Fact] public void IsImage_PdfJamaisImage() => Assert.False(ImageFileTypes.IsImage("rapport.pdf", "application/pdf")); [Theory] [InlineData(null, 500, PhotoLimitSource.DefaultFallback)] [InlineData("", 500, PhotoLimitSource.DefaultFallback)] [InlineData(" ", 500, PhotoLimitSource.DefaultFallback)] [InlineData("abc", 500, PhotoLimitSource.DefaultFallback)] [InlineData("-10", 500, PhotoLimitSource.DefaultFallback)] // 0 est une valeur métier valide : « aucune limite », jamais remplacée par le repli. [InlineData("0", 0, PhotoLimitSource.ApplicationParameter)] [InlineData("500", 500, PhotoLimitSource.ApplicationParameter)] [InlineData(" 2048 ", 2048, PhotoLimitSource.ApplicationParameter)] [InlineData("10240", 10240, PhotoLimitSource.ApplicationParameter)] public void Resolve_ValeurEtSource(string? valeur, int expectedLimit, PhotoLimitSource expectedSource) { PhotoLimitResolution resolution = PhotoOptimizationSettings.Resolve(valeur); Assert.Equal(expectedLimit, resolution.LimitKb); Assert.Equal(expectedSource, resolution.Source); Assert.Equal(expectedSource == PhotoLimitSource.DefaultFallback, resolution.IsFallback); Assert.Equal(expectedLimit <= 0, resolution.IsDisabled); } [Fact] public void Resolve_Zero_NEStJamaisRemplaceParLeRepli() { // Règle métier critique : 0 ≠ absent. PhotoLimitResolution zero = PhotoOptimizationSettings.Resolve("0"); PhotoLimitResolution absent = PhotoOptimizationSettings.Resolve(null); Assert.Equal(0, zero.LimitKb); Assert.True(zero.IsDisabled); Assert.False(zero.IsFallback); Assert.Equal(500, absent.LimitKb); Assert.True(absent.IsFallback); Assert.NotEqual(zero.LimitKb, absent.LimitKb); } [Theory] [InlineData(0, "Aucune limite")] [InlineData(250, "250 Ko")] [InlineData(500, "500 Ko")] [InlineData(750, "750 Ko")] [InlineData(1024, "1 Mo")] [InlineData(1536, "1,5 Mo")] [InlineData(2048, "2 Mo")] [InlineData(7680, "7,5 Mo")] [InlineData(10240, "10 Mo")] public void FormatPhotoSize_LibelleUtilisateur(int kiloOctets, string expected) => Assert.Equal(expected, ParametresViewModel.FormatPhotoSize(kiloOctets)); [Theory] [InlineData(500, 2)] [InlineData(2, 0)] [InlineData(0, 0)] [InlineData(10240, 11)] [InlineData(9000, 11)] [InlineData(1100, 4)] public void IndexDuPalierLePlusProche_PositionneLaJauge(int kiloOctets, int expectedIndex) => Assert.Equal(expectedIndex, ParametresViewModel.IndexDuPalierLePlusProche(kiloOctets)); }