using System.Text.Json; using Omika.App.Local.Abstractions; using Omika.App.Local.Models; using Omika.App.ViewModels; using Omika.Core.Contracts.Documents; namespace Omika.Tests.Mobile.Local; /// /// Tests du chargement de miniature des photos du rapport : téléchargement à la /// demande, repli local, placeholder hors ligne, concurrence bornée et annulation. /// public class RapportPhotoMiniatureTests { private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); private static TableauOptionEdition PhotoOption(string? guid = null, string? cheminComplet = null) { using JsonDocument doc = JsonDocument.Parse(JsonSerializer.Serialize( new { guid, cheminComplet, nom = "photo.jpg", date = "2024-01-01", notes = "Note" }, Json)); return new TableauOptionEdition(doc.RootElement.Clone(), "photo.jpg", TypeTableau.Photos); } 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 }; // ------------------------------------------------------------------ // Fake IDocumentLocalService (seuls EnsureDownloaded/GetCachedFilePath // sont utilisés par le chargement de miniature) // ------------------------------------------------------------------ private sealed class FakeDocumentService : IDocumentLocalService { private int _active; private int _maxActive; public int MaxActive => _maxActive; public Func>? EnsureDownloaded { get; set; } public Func>? GetCachedFilePath { get; set; } public async Task EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default) { int current = Interlocked.Increment(ref _active); UpdateMax(current); try { if (EnsureDownloaded is not null) { return await EnsureDownloaded(documentGuid, ct); } return false; } finally { Interlocked.Decrement(ref _active); } } private void UpdateMax(int value) { int snapshot = _maxActive; while (value > snapshot) { int original = Interlocked.CompareExchange(ref _maxActive, value, snapshot); if (original == snapshot) { break; } snapshot = original; } } public Task GetCachedFilePathAsync(Guid documentGuid, CancellationToken ct = default) => GetCachedFilePath is not null ? GetCachedFilePath(documentGuid, ct) : Task.FromResult(null); // Membres non utilisés. public Task> GetDocumentsForInterventionAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task OpenAsync(Guid documentGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddNoteAsync(Guid documentGuid, string contenu, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddPhotoAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default) => throw new NotImplementedException(); } // ------------------------------------------------------------------ // Tests // ------------------------------------------------------------------ [Fact] public async Task Miniature_PhotoLocale_CheminCacheRetourne() { string fichier = Path.Combine(Path.GetTempPath(), "omika-mini", Guid.NewGuid().ToString("N"), "photo.png"); Directory.CreateDirectory(Path.GetDirectoryName(fichier)!); File.WriteAllBytes(fichier, [0x89, 0x50, 0x4E, 0x47]); try { Guid guid = Guid.NewGuid(); var service = new FakeDocumentService { EnsureDownloaded = (_, _) => Task.FromResult(true), GetCachedFilePath = (_, _) => Task.FromResult(fichier) }; TableauOptionEdition option = PhotoOption(guid.ToString()); string? chemin = await option.ResoudreCheminMiniatureAsync(service, CancellationToken.None); Assert.Equal(fichier, chemin); } finally { if (Directory.Exists(Path.GetDirectoryName(fichier)!)) { Directory.Delete(Path.GetDirectoryName(fichier)!, recursive: true); } } } [Fact] public async Task Miniature_TelechargementADemande_Effectue() { string fichier = Path.Combine(Path.GetTempPath(), "omika-mini", Guid.NewGuid().ToString("N"), "photo.png"); Directory.CreateDirectory(Path.GetDirectoryName(fichier)!); File.WriteAllBytes(fichier, [0x89, 0x50, 0x4E, 0x47]); try { Guid guid = Guid.NewGuid(); var service = new FakeDocumentService { EnsureDownloaded = (_, _) => Task.FromResult(true), GetCachedFilePath = (_, _) => Task.FromResult(fichier) }; TableauOptionEdition option = PhotoOption(guid.ToString(), null); string? chemin = await option.ResoudreCheminMiniatureAsync(service, CancellationToken.None); Assert.Equal(fichier, chemin); } finally { if (Directory.Exists(Path.GetDirectoryName(fichier)!)) { Directory.Delete(Path.GetDirectoryName(fichier)!, recursive: true); } } } [Fact] public async Task Miniature_OfflineEtAbsente_PlaceholderIndisponible() { var service = new FakeDocumentService { EnsureDownloaded = (_, _) => Task.FromResult(false), GetCachedFilePath = (_, _) => Task.FromResult(null) }; TableauOptionEdition option = PhotoOption(Guid.NewGuid().ToString(), null); await option.ChargerMiniatureAsync(service, CancellationToken.None); Assert.True(option.MiniatureIndisponible); Assert.Null(option.Miniature); Assert.False(option.EstChargementMiniature); } [Fact] public async Task Miniature_RepliCheminCompletServeur() { string fichier = Path.Combine(Path.GetTempPath(), "omika-mini", Guid.NewGuid().ToString("N"), "photo.png"); Directory.CreateDirectory(Path.GetDirectoryName(fichier)!); File.WriteAllBytes(fichier, [0x89, 0x50, 0x4E, 0x47]); try { // Sans GUID : seul le chemin absolu serveur (cheminComplet) est disponible. var service = new FakeDocumentService(); TableauOptionEdition option = PhotoOption(null, fichier); string? chemin = await option.ResoudreCheminMiniatureAsync(service, CancellationToken.None); Assert.Equal(fichier, chemin); } finally { if (Directory.Exists(Path.GetDirectoryName(fichier)!)) { Directory.Delete(Path.GetDirectoryName(fichier)!, recursive: true); } } } [Fact] public async Task Miniature_ConcurrenceBornee_TroisMax() { // 10 photos : le chargement ne doit jamais dépasser 3 téléchargements simultanés. var items = Enumerable.Range(0, 10) .Select(i => new { guid = Guid.NewGuid().ToString(), cheminComplet = (string?)null, nom = $"photo{i}.jpg", date = "2024-01-01", notes = "Note" }); string json = JsonSerializer.Serialize(items, Json); var edition = new RapportChampEdition(TableauPhoto(json, new RapportChampSousElementDto { Rubrique = "$TABPHOTO.IMAGE$", MembreMob = "cheminComplet", Type = "ChpImage" })); var service = new FakeDocumentService { EnsureDownloaded = async (_, ct) => { await Task.Delay(30, ct); return false; } }; await edition.ChargerMiniaturesAsync(service, CancellationToken.None); Assert.Equal(10, edition.Options.Count); Assert.True(service.MaxActive <= 3, $"Concurrence observée : {service.MaxActive}"); } [Fact] public async Task Miniature_Annulation_PropageOperationCanceled() { var items = Enumerable.Range(0, 3).Select(i => new { guid = Guid.NewGuid().ToString(), cheminComplet = (string?)null, nom = $"photo{i}.jpg", date = "2024-01-01", notes = "Note" }); string json = JsonSerializer.Serialize(items, Json); var edition = new RapportChampEdition(TableauPhoto(json, new RapportChampSousElementDto { Rubrique = "$TABPHOTO.IMAGE$", MembreMob = "cheminComplet", Type = "ChpImage" })); var service = new FakeDocumentService { EnsureDownloaded = async (_, ct) => { await Task.Delay(30, ct); return false; } }; using var cts = new CancellationTokenSource(); cts.Cancel(); await Assert.ThrowsAnyAsync( () => edition.ChargerMiniaturesAsync(service, cts.Token)); } }