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

434 lines
15 KiB
C#

using Omika.App.Local.Abstractions;
using Omika.App.Local.Models;
using Omika.App.Navigation;
using Omika.App.Services;
using Omika.App.ViewModels;
using Omika.Core.Contracts.Documents;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Tests du ViewModel Documents / Photos : chargement local, états vide/erreur,
/// téléchargement à la demande, ouverture, ajout photo/document et navigation.
/// </summary>
public class InterventionDocumentsViewModelTests
{
private sealed class FakeDocumentService : IDocumentLocalService
{
public IReadOnlyList<InterventionDocumentItem> ItemsToReturn { get; set; } = [];
public bool ThrowOnLoad { get; set; }
public bool DownloadResult { get; set; } = true;
public int DownloadCalls { get; private set; }
public bool OpenResult { get; set; } = true;
public int OpenCalls { get; private set; }
public InterventionDocumentItem? PhotoToReturn { get; set; }
public InterventionDocumentItem? DocumentToReturn { get; set; }
public AddPhotosResult PhotosBatchToReturn { get; set; } = new();
public InterventionDocumentItem? CameraPhotoToReturn { get; set; }
public Exception? CameraExceptionToThrow { get; set; }
public int AddPhotosCalls { get; private set; }
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(
Guid interventionGuid, CancellationToken ct = default)
{
if (ThrowOnLoad)
{
throw new InvalidOperationException("Erreur de chargement");
}
return Task.FromResult(ItemsToReturn);
}
public Task<bool> EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default)
{
DownloadCalls++;
return Task.FromResult(DownloadResult);
}
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken ct = default)
{
OpenCalls++;
return Task.FromResult(OpenResult);
}
public Task<string?> GetCachedFilePathAsync(Guid documentGuid, CancellationToken ct = default)
=> Task.FromResult<string?>(null);
public Task<IReadOnlyList<DocumentNoteItem>> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DocumentNoteItem>>([]);
public Task<DocumentNoteItem> AddNoteAsync(Guid documentGuid, string contenu, CancellationToken ct = default)
=> Task.FromResult(new DocumentNoteItem());
public Task<InterventionDocumentItem?> AddPhotoAsync(Guid interventionGuid, CancellationToken ct = default)
{
if (PhotoToReturn is not null)
{
var items = ItemsToReturn.ToList();
items.Add(PhotoToReturn);
ItemsToReturn = items;
}
return Task.FromResult(PhotoToReturn);
}
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default)
{
if (DocumentToReturn is not null)
{
var items = ItemsToReturn.ToList();
items.Add(DocumentToReturn);
ItemsToReturn = items;
}
return Task.FromResult(DocumentToReturn);
}
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default)
{
AddPhotosCalls++;
foreach (InterventionDocumentItem item in PhotosBatchToReturn.Added)
{
var items = ItemsToReturn.ToList();
items.Add(item);
ItemsToReturn = items;
}
return Task.FromResult(PhotosBatchToReturn);
}
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default)
{
if (CameraExceptionToThrow is not null)
{
return Task.FromException<InterventionDocumentItem?>(CameraExceptionToThrow);
}
if (CameraPhotoToReturn is not null)
{
var items = ItemsToReturn.ToList();
items.Add(CameraPhotoToReturn);
ItemsToReturn = items;
}
return Task.FromResult(CameraPhotoToReturn);
}
public Task<InterventionDocumentItem?> AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
}
private static InterventionDocumentItem CreateItem(Guid id, string nom, bool isPhoto = false, bool isCached = false)
=> new()
{
DocumentGuid = id,
Nom = nom,
Titre = nom,
IsPhoto = isPhoto,
IsCached = isCached
};
private sealed class PermissivePermissionService : IPermissionService
{
public IReadOnlyDictionary<string, bool> Rights => new Dictionary<string, bool>();
public void SetRights(IReadOnlyDictionary<string, bool> rights) { }
public bool HasRight(string key) => true;
public bool MessagerieExterne => false;
public void SetMessagerieExterne(bool externe) { }
public bool PeutRechercherInterventions => true;
public bool VoirRdvSansRestriction => true;
public bool PeutVoirAgendaEquipe => true;
public bool PeutEnvoyerEmail => true;
public bool PeutComposerEmail => true;
public bool PeutCreerRapport => true;
public bool PeutGererLicence => true;
public bool PeutSaisirTempsAutrui => true;
public bool PeutVoirNotesAdmin => true;
public bool PeutVoirClients => true;
public bool PeutVoirSites => true;
public bool PeutVoirDossiers => true;
}
private sealed class FakeSyncEngine : ISyncEngine
{
public Task<SyncCycleResult> SyncAsync(CancellationToken ct = default)
=> Task.FromResult(new SyncCycleResult { Executed = true });
public Task<SyncCycleResult> PushAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new SyncCycleResult { Executed = true });
}
private readonly FakeDocumentService _service = new();
private readonly NavigationService _navigation = new();
private readonly InterventionDocumentsViewModel _viewModel;
public InterventionDocumentsViewModelTests()
{
_viewModel = new InterventionDocumentsViewModel(
_service, _navigation, new PermissivePermissionService(), new FakeSyncEngine());
}
[Fact]
public async Task Load_ChargeLesDocuments()
{
// Arrange
Guid interventionId = Guid.NewGuid();
Guid docId = Guid.NewGuid();
_service.ItemsToReturn = [CreateItem(docId, "doc.pdf")];
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert
Assert.False(_viewModel.IsLoading);
Assert.Null(_viewModel.ErrorMessage);
Assert.Single(_viewModel.Documents);
Assert.Equal(docId, _viewModel.Documents[0].DocumentGuid);
Assert.True(_viewModel.HasDocuments);
Assert.False(_viewModel.IsEmpty);
}
[Fact]
public async Task Load_AucunDocument_IsEmpty()
{
// Arrange
_service.ItemsToReturn = [];
// Act
await _viewModel.LoadCommand.ExecuteAsync(Guid.NewGuid());
// Assert
Assert.Empty(_viewModel.Documents);
Assert.False(_viewModel.HasDocuments);
Assert.True(_viewModel.IsEmpty);
}
[Fact]
public async Task Load_Erreur_MessageAffiché()
{
// Arrange
_service.ThrowOnLoad = true;
// Act
await _viewModel.LoadCommand.ExecuteAsync(Guid.NewGuid());
// Assert
Assert.False(_viewModel.IsLoading);
Assert.NotNull(_viewModel.ErrorMessage);
Assert.Empty(_viewModel.Documents);
}
[Fact]
public async Task Download_AppelleLeService()
{
// Arrange
Guid interventionId = Guid.NewGuid();
Guid docId = Guid.NewGuid();
var item = CreateItem(docId, "doc.pdf", isCached: false);
_service.ItemsToReturn = [item];
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.DownloadCommand.ExecuteAsync(item);
// Assert
Assert.Equal(1, _service.DownloadCalls);
}
[Fact]
public async Task Download_DejaEnCache_NeReappellePas()
{
// Arrange
Guid interventionId = Guid.NewGuid();
Guid docId = Guid.NewGuid();
var item = CreateItem(docId, "doc.pdf", isCached: true);
_service.ItemsToReturn = [item];
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.DownloadCommand.ExecuteAsync(item);
// Assert : déjà en cache, aucun appel de téléchargement.
Assert.Equal(0, _service.DownloadCalls);
}
[Fact]
public async Task Open_AppelleLeService()
{
// Arrange
Guid interventionId = Guid.NewGuid();
Guid docId = Guid.NewGuid();
var item = CreateItem(docId, "doc.pdf", isCached: true);
_service.ItemsToReturn = [item];
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.OpenCommand.ExecuteAsync(item);
// Assert
Assert.Equal(1, _service.OpenCalls);
Assert.Null(_viewModel.ErrorMessage);
}
[Fact]
public async Task Open_NonDisponible_MessageErreur()
{
// Arrange
Guid interventionId = Guid.NewGuid();
Guid docId = Guid.NewGuid();
var item = CreateItem(docId, "doc.pdf", isCached: false);
_service.ItemsToReturn = [item];
_service.OpenResult = false;
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.OpenCommand.ExecuteAsync(item);
// Assert
Assert.Equal(1, _service.OpenCalls);
Assert.NotNull(_viewModel.ErrorMessage);
}
[Fact]
public async Task AddPhoto_AjouteALaListe()
{
// Arrange
Guid interventionId = Guid.NewGuid();
var photo = CreateItem(Guid.NewGuid(), "photo.jpg", isPhoto: true, isCached: true);
_service.PhotosBatchToReturn = new AddPhotosResult { Added = [photo] };
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.AddPhotoCommand.ExecuteAsync(null);
// Assert
Assert.Contains(_viewModel.Documents, d => d.DocumentGuid == photo.DocumentGuid);
Assert.True(_viewModel.HasDocuments);
}
[Fact]
public async Task AddDocument_AjouteALaListe()
{
// Arrange
Guid interventionId = Guid.NewGuid();
var doc = CreateItem(Guid.NewGuid(), "doc.pdf", isCached: true);
_service.DocumentToReturn = doc;
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.AddDocumentCommand.ExecuteAsync(null);
// Assert
Assert.Contains(_viewModel.Documents, d => d.DocumentGuid == doc.DocumentGuid);
Assert.True(_viewModel.HasDocuments);
}
[Fact]
public async Task AddPhoto_Annule_NeRienAjoute()
{
// Arrange : sélection multiple annulée → lot vide (Added vide, 0 échec).
Guid interventionId = Guid.NewGuid();
_service.PhotosBatchToReturn = new AddPhotosResult();
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.AddPhotoCommand.ExecuteAsync(null);
// Assert
Assert.Empty(_viewModel.Documents);
Assert.Null(_viewModel.ErrorMessage);
}
[Fact]
public async Task AddPhoto_Multiple_ToutesAjoutees()
{
// Arrange : 3 photos sélectionnées.
Guid interventionId = Guid.NewGuid();
var p1 = CreateItem(Guid.NewGuid(), "p1.jpg", isPhoto: true, isCached: true);
var p2 = CreateItem(Guid.NewGuid(), "p2.jpg", isPhoto: true, isCached: true);
var p3 = CreateItem(Guid.NewGuid(), "p3.jpg", isPhoto: true, isCached: true);
_service.PhotosBatchToReturn = new AddPhotosResult { Added = [p1, p2, p3] };
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.AddPhotoCommand.ExecuteAsync(null);
// Assert
Assert.Equal(3, _viewModel.Documents.Count);
Assert.Null(_viewModel.ErrorMessage);
}
[Fact]
public async Task AddPhoto_Partiel_MessageErreurEtSuccesConserves()
{
// Arrange : 2 succès + 1 échec.
Guid interventionId = Guid.NewGuid();
var p1 = CreateItem(Guid.NewGuid(), "p1.jpg", isPhoto: true, isCached: true);
var p2 = CreateItem(Guid.NewGuid(), "p2.jpg", isPhoto: true, isCached: true);
_service.PhotosBatchToReturn = new AddPhotosResult { Added = [p1, p2], FailedCount = 1 };
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.AddPhotoCommand.ExecuteAsync(null);
// Assert : les 2 succès sont conservés + message utilisateur cohérent.
Assert.Equal(2, _viewModel.Documents.Count);
Assert.NotNull(_viewModel.ErrorMessage);
Assert.Contains("2 images ajoutées", _viewModel.ErrorMessage);
Assert.Contains("1 image n'a pas pu être ajoutée", _viewModel.ErrorMessage);
}
[Fact]
public async Task AddPhotoFromCamera_Annule_AucunAjout()
{
// Arrange
Guid interventionId = Guid.NewGuid();
_service.CameraPhotoToReturn = null;
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.AddPhotoFromCameraCommand.ExecuteAsync(null);
// Assert : aucun document, aucune erreur.
Assert.Empty(_viewModel.Documents);
Assert.Null(_viewModel.ErrorMessage);
}
[Fact]
public async Task AddPhotoFromCamera_Erreur_MessageAffiché()
{
// Arrange : échec du launcher caméra → exception contrôlée.
Guid interventionId = Guid.NewGuid();
_service.CameraExceptionToThrow = new PhotoCaptureException("Caméra indisponible.");
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.AddPhotoFromCameraCommand.ExecuteAsync(null);
// Assert : l'application ne plante pas, message utilisateur affiché.
Assert.Empty(_viewModel.Documents);
Assert.Equal("Caméra indisponible.", _viewModel.ErrorMessage);
}
[Fact]
public void Back_RetourneALaFicheIntervention()
{
// Arrange
Guid interventionId = Guid.NewGuid();
_viewModel.InterventionGuid = interventionId;
// Act
_viewModel.BackCommand.Execute(null);
// Assert
Assert.Equal(AppPage.InterventionDetail, _navigation.CurrentPage);
Assert.Equal(interventionId, _navigation.CurrentParameter);
}
}