Files
Omika/tests/Omika.Tests/Mobile/Local/RapportPhotoMiniatureTests.cs
T
2026-10-23 18:46:50 +02:00

288 lines
11 KiB
C#

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;
/// <summary>
/// Tests du chargement de miniature des photos du rapport : téléchargement à la
/// demande, repli local, placeholder hors ligne, concurrence bornée et annulation.
/// </summary>
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<Guid, CancellationToken, Task<bool>>? EnsureDownloaded { get; set; }
public Func<Guid, CancellationToken, Task<string?>>? GetCachedFilePath { get; set; }
public async Task<bool> 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<string?> GetCachedFilePathAsync(Guid documentGuid, CancellationToken ct = default)
=> GetCachedFilePath is not null
? GetCachedFilePath(documentGuid, ct)
: Task.FromResult<string?>(null);
// Membres non utilisés.
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken ct = default) => throw new NotImplementedException();
public Task<IReadOnlyList<DocumentNoteItem>> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<DocumentNoteItem> AddNoteAsync(Guid documentGuid, string contenu, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDocumentItem?> AddPhotoAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDocumentItem?> 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<string?>(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<string?>(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<string?>(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<OperationCanceledException>(
() => edition.ChargerMiniaturesAsync(service, cts.Token));
}
}