434 lines
15 KiB
C#
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);
|
|
}
|
|
}
|