288 lines
11 KiB
C#
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));
|
|
}
|
|
}
|