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

459 lines
21 KiB
C#

using Omika.App.Local.Abstractions;
using Omika.Core.Contracts.Documents;
using Omika.App.Local.Models;
using Omika.App.Local.Repositories;
using Omika.App.Local.Services;
using Omika.App.Navigation;
using Omika.App.Services;
using Omika.App.ViewModels;
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Mobile.License;
using Omika.Core.Contracts.Mobile.Sync;
using Omika.Core.Contracts.Planning;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Tests du RootViewModel : décision de démarrage (activation OU Shell),
/// bascule après activation réussie et comportement offline.
/// </summary>
public class RootViewModelTests : LocalTestBase
{
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 sealed class FakeDocumentService : IDocumentLocalService
{
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(
Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<InterventionDocumentItem>>([]);
public Task<bool> EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default)
=> Task.FromResult(false);
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken ct = default)
=> Task.FromResult(false);
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)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult(new AddPhotosResult());
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<InterventionDocumentItem?> AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
}
private sealed class FakeContactsService : IInterventionContactsService
{
public Task<IReadOnlyList<InterventionContact>> GetContactsForInterventionAsync(
Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<InterventionContact>>([]);
public Task<InterventionContact?> GetContactByIdAsync(Guid contactGuid, CancellationToken ct = default)
=> Task.FromResult<InterventionContact?>(null);
public Task<IReadOnlyList<InterventionContact>> GetContactsDisponiblesAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<InterventionContact>>([]);
}
private sealed class FakeDossierService : IDossierLocalService
{
public Task<IReadOnlyList<DossierListItem>> GetDossiersAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DossierListItem>>([]);
public Task<IReadOnlyList<DossierListItem>> SearchDossiersAsync(string? refInterne, string? titre, string? client, string? statut, string? type, string? refExterne, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DossierListItem>>([]);
public Task<IReadOnlyList<DossierListItem>> GetLastSearchAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DossierListItem>>([]);
public Task ClearLastSearchAsync(CancellationToken ct = default)
=> Task.CompletedTask;
public Task<DossierDetail?> GetDossierByIdAsync(Guid dossierGuid, CancellationToken ct = default)
=> Task.FromResult<DossierDetail?>(null);
public Task SetPinnedAsync(Guid dossierGuid, bool isPinned, DossierListItem? item, CancellationToken ct = default)
=> Task.CompletedTask;
public Task<IReadOnlyList<DossierListItem>> GetPinnedAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DossierListItem>>([]);
public Task<IReadOnlyList<DossierIntervention>> GetDossierInterventionsAsync(Guid dossierGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DossierIntervention>>([]);
public Task<IReadOnlyList<DossierIntervention>> RefreshDossierInterventionsAsync(Guid dossierGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DossierIntervention>>([]);
}
private sealed class FakeClientService : IClientLocalService
{
public Task<IReadOnlyList<ClientListItem>> GetClientsAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ClientListItem>>([]);
public Task<ClientDetail?> GetClientByIdAsync(Guid clientGuid, CancellationToken ct = default)
=> Task.FromResult<ClientDetail?>(null);
}
private sealed class FakeSiteService : ISiteLocalService
{
public Task<IReadOnlyList<SiteListItem>> GetSitesAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<SiteListItem>>([]);
public Task<SiteDetail?> GetSiteByIdAsync(Guid siteGuid, CancellationToken ct = default)
=> Task.FromResult<SiteDetail?>(null);
}
private sealed class FakeCollaborateurService : ICollaborateurLocalService
{
public Task<IReadOnlyList<CollaborateurListItem>> GetCollaborateursAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<CollaborateurListItem>>([]);
public Task<CollaborateurDetail?> GetCollaborateurByIdAsync(Guid collaborateurGuid, CancellationToken ct = default)
=> Task.FromResult<CollaborateurDetail?>(null);
}
private sealed class FakeEquipeService : IEquipeLocalService
{
public Task<IReadOnlyList<EquipeListItem>> GetEquipesAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<EquipeListItem>>([]);
public Task<IReadOnlyList<EquipeListItem>> GetAllEquipesAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<EquipeListItem>>([]);
}
private sealed class FakeSuiviTempsService : ISuiviTempsLocalService
{
public Task<Guid> CreateAsync(SuiviTempsSyncPayload payload, CancellationToken ct = default)
=> Task.FromResult(Guid.NewGuid());
public Task<SuiviTempsDto?> GetByGuidAsync(Guid suiviTempsGuid, CancellationToken ct = default)
=> Task.FromResult<SuiviTempsDto?>(null);
public Task<IReadOnlyList<SuiviTempsDto>> GetAllAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
public Task<IReadOnlyList<SuiviTempsDto>> GetByInterventionAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
public Task<IReadOnlyList<SuiviTempsDto>> GetByRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
public Task<IReadOnlyList<SuiviTempsDraftEntry>> GetDraftsByRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<SuiviTempsDraftEntry>>([]);
public Task CreateDraftAsync(SuiviTempsDraftEntry draft, CancellationToken ct = default)
=> Task.CompletedTask;
public Task DeleteDraftAsync(Guid rendezVousGuid, string typeTempsKey, CancellationToken ct = default)
=> Task.CompletedTask;
public Task<bool> HasTypeTempsAsync(Guid rendezVousGuid, string typeTempsKey, CancellationToken ct = default)
=> Task.FromResult(false);
public Task<bool> IsValideAsync(Guid rendezVousGuid, CancellationToken ct = default)
=> Task.FromResult(false);
public Task ValidateRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
=> Task.CompletedTask;
public Task SaveRendezVousPourBrouillonAsync(AgendaItem rendezVous, CancellationToken ct = default)
=> Task.CompletedTask;
public Task<IReadOnlyList<AgendaItem>> GetRendezVousAvecBrouillonAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<AgendaItem>>([]);
public Task DeleteRendezVousPourBrouillonAsync(Guid rendezVousId, CancellationToken ct = default)
=> Task.CompletedTask;
}
private sealed class FakeParametreMetierService : IParametreMetierLocalService
{
public Task<IReadOnlyList<ReferenceValueDto>> GetTypeTempsAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
public Task<Omika.Core.Contracts.Configuration.TypeTempsParametresDto?> GetTypeTempsParametresAsync(string typeTempsKey, CancellationToken ct = default)
=> Task.FromResult<Omika.Core.Contracts.Configuration.TypeTempsParametresDto?>(null);
public Task<Omika.Core.Contracts.Configuration.TypeRdvParametresDto?> GetTypeRdvParametresAsync(string typeRdvKey, CancellationToken ct = default)
=> Task.FromResult<Omika.Core.Contracts.Configuration.TypeRdvParametresDto?>(null);
public Task<Omika.Core.Contracts.Configuration.StatutRdvParametresDto?> GetStatutRdvParametresAsync(string statutRdvKey, CancellationToken ct = default)
=> Task.FromResult<Omika.Core.Contracts.Configuration.StatutRdvParametresDto?>(null);
public Task<IReadOnlyList<ReferenceValueDto>> GetInterventionTypesAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
public Task<IReadOnlyList<ReferenceValueDto>> GetInterventionEtatsAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
public Task<IReadOnlyList<ReferenceValueDto>> GetDossierTypesAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
public Task<IReadOnlyList<ReferenceValueDto>> GetDossierStatutsAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
public Task<IReadOnlyList<ReferenceValueDto>> GetClientTypesAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
public Task<IReadOnlyList<ReferenceValueDto>> GetClientStatutsAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
public Task<IReadOnlyList<ReferenceValueDto>> GetSiteTypesAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
public Task<IReadOnlyList<ReferenceValueDto>> GetTypeSignatairesAsync(CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
}
private sealed class FakePhoneLauncher : IPhoneLauncher
{
public Task<bool> DialAsync(string phoneNumber) => Task.FromResult(true);
}
private sealed class FakeEmailLauncher : IEmailLauncher
{
public Task<bool> ComposeAsync(string email) => Task.FromResult(true);
}
private sealed class FakeMapLauncher : IMapLauncher
{
public Task<bool> OpenItineraireAsync(string address) => Task.FromResult(true);
}
private sealed class FakeSessionService : IMobileSessionService
{
public bool HasValid { get; set; }
public bool HasExpired { get; set; }
public LicenseResponse ActivateToReturn { get; set; } = new();
public SessionRevalidationResult EnsureValidSessionResult { get; set; } =
new() { Outcome = SessionRevalidationOutcome.NoSession };
public Task<bool> HasValidSessionAsync(CancellationToken ct = default) => Task.FromResult(HasValid);
public Task<bool> HasExpiredSessionAsync(CancellationToken ct = default) => Task.FromResult(HasExpired);
public Task<SessionRevalidationResult> EnsureValidSessionAsync(CancellationToken ct = default)
=> Task.FromResult(EnsureValidSessionResult);
public Task<LicenseResponse> ActivateAsync(
string codeClient, string numeroLicence, CancellationToken ct = default)
=> Task.FromResult(ActivateToReturn);
public Task<LicenseResponse> ValidateAsync(CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<LocalBootstrapState?> GetBootstrapAsync(CancellationToken ct = default)
=> Task.FromResult<LocalBootstrapState?>(null);
public Task<string?> GetAccessTokenAsync(CancellationToken ct = default)
=> Task.FromResult<string?>(null);
public Task ClearSessionAsync(CancellationToken ct = default) => Task.CompletedTask;
public Task EnsureDevelopmentSessionAsync(CancellationToken ct = default) => Task.CompletedTask;
}
private sealed class FakeEnrollmentService : IEnrollmentService
{
public Task<MobileEnrollmentConfiguration?> LoadAsync(CancellationToken ct = default)
=> Task.FromResult<MobileEnrollmentConfiguration?>(null);
public Task<EnrollmentResult> EnrollAsync(string qrContent, CancellationToken ct = default)
=> Task.FromResult(EnrollmentResult.Failed("refusée"));
public Task ClearAsync(CancellationToken ct = default) => Task.CompletedTask;
}
private sealed class FakeQrScanner : IEnrollmentQrScanner
{
public Task<string?> ScanAsync(IQrScannerPreviewSink? previewSink, CancellationToken ct = default)
=> Task.FromResult<string?>(null);
}
private readonly FakeSessionService _session = new();
private readonly RootViewModel _root;
public RootViewModelTests()
{
var preferencesRepository = new UiPreferencesRepository(Database);
var themeService = new ThemeService(preferencesRepository);
var navigation = new NavigationService();
var syncEngine = new FakeSyncEngine();
IAgendaService agendaService = new AgendaService(
RendezVousRepository, ClientRepository, SiteRepository, InterventionRepository, CollaborateurRepository, ApiClient, SessionService);
IInterventionLocalService interventionService = new InterventionLocalService(
InterventionRepository, ClientRepository, SiteRepository, DossierRepository,
RendezVousRepository, DocumentMetadataRepository, CollaborateurRepository,
OutboxService, ReconciliationService, ApiClient, InterventionCacheRepository, ContextInterventionCacheRepository, Database);
var pageFactory = new PageViewModelFactory(
themeService, agendaService, interventionService, new FakeDocumentService(),
new FakeContactsService(), new FakeDossierService(), new FakeClientService(),
new FakeSiteService(), new FakeCollaborateurService(), new FakeEquipeService(),
new FakeSuiviTempsService(), new FakeParametreMetierService(), InterventionRepository, OutboxRepository, null!,
new FakePhoneLauncher(), new FakeEmailLauncher(), new FakeMapLauncher(), null!, navigation, _session, new PermissionService(),
syncEngine,
null!,
new FakePhotoPolicyProvider(),
new PauseDejeunerPreferenceService(PreferencesUtilisateurRepository, SessionService),
new RendezVousAnnulationLocalService(RendezVousRepository, OutboxService, new RendezVousStateNotifier()),
new RendezVousFinalisationLocalService(RendezVousRepository, new RendezVousStateNotifier()),
new CompteRenduScopeService(interventionService),
new RendezVousStateNotifier(),
null!,
Omika.Core.Observability.NullTelemetryService.Instance,
Network,
new FakeLangueSecondairePreferenceService(),
new FakeLanguagePackService(),
new FakeTranslationService());
var shell = new ShellViewModel(navigation, themeService, syncEngine, pageFactory, _session, new PermissionService(), OutboxService, null!);
pageFactory.SetShell(shell);
var activation = new LicenseActivationViewModel(new FakeEnrollmentService(), new FakeQrScanner());
_root = new RootViewModel(_session, shell, activation);
}
[Fact]
public void AuDemarrage_AfficheActivation()
{
// Assert : par défaut, on affiche l'activation (pas de flash du Shell).
Assert.IsType<LicenseActivationViewModel>(_root.CurrentViewModel);
Assert.True(_root.IsActivationRequired);
}
[Fact]
public async Task SessionValide_AuDemarrage_BasculeVersShell()
{
// Arrange
_session.HasValid = true;
// Act
await _root.EvaluateSessionAsync();
// Assert
Assert.IsType<ShellViewModel>(_root.CurrentViewModel);
Assert.False(_root.IsActivationRequired);
}
[Fact]
public async Task SessionInvalide_AuDemarrage_ResteSurActivation()
{
// Arrange
_session.HasValid = false;
// Act
await _root.EvaluateSessionAsync();
// Assert
Assert.IsType<LicenseActivationViewModel>(_root.CurrentViewModel);
Assert.True(_root.IsActivationRequired);
}
[Fact]
public async Task SessionExpiree_DansGrace_BasculeVersShell()
{
// Arrange : session valide offline (dans la période de grace).
_session.HasValid = true;
// Act
await _root.EvaluateSessionAsync();
// Assert
Assert.IsType<ShellViewModel>(_root.CurrentViewModel);
Assert.False(_root.IsActivationRequired);
}
[Fact]
public void ActivationReussie_BasculeVersShell()
{
// Act
_root.OnActivationSucceeded();
// Assert
Assert.IsType<ShellViewModel>(_root.CurrentViewModel);
Assert.False(_root.IsActivationRequired);
}
[Fact]
public async Task Startup_RejetLicenceServeur_AfficheActivationAvecNoticeEtQr()
{
// Arrange : le serveur confirme une licence terminalement invalide.
_session.EnsureValidSessionResult = new SessionRevalidationResult
{
Outcome = SessionRevalidationOutcome.InvalidLicense,
License = new LicenseResponse { IsValid = false, ErrorCode = "E3003", ErrorMessage = "Licence expirée." }
};
// Act
await _root.EvaluateSessionAsync();
// Assert : activation, QR disponible (seul parcours), notice posée.
Assert.IsType<LicenseActivationViewModel>(_root.CurrentViewModel);
Assert.True(_root.IsActivationRequired);
Assert.NotNull(_root.Activation.Notice);
Assert.True(_root.Activation.IsEnrollmentEnabled);
}
[Fact]
public async Task Startup_HorsLigne_HorsGrace_AfficheConnexionRequiseSansQr()
{
// Arrange : token expiré, serveur injoignable, hors période de grace.
_session.EnsureValidSessionResult = new SessionRevalidationResult { Outcome = SessionRevalidationOutcome.TransientError };
_session.HasValid = false;
_session.HasExpired = true;
// Act
await _root.EvaluateSessionAsync();
// Assert : pas de QR, message « Connexion requise ».
Assert.IsType<LicenseActivationViewModel>(_root.CurrentViewModel);
Assert.True(_root.IsActivationRequired);
Assert.False(_root.Activation.IsEnrollmentEnabled);
Assert.Contains("Connexion requise", _root.Activation.Notice);
}
[Fact]
public async Task Startup_RenouvellementReussi_SessionValide_BasculeShell()
{
// Arrange
_session.EnsureValidSessionResult = new SessionRevalidationResult { Outcome = SessionRevalidationOutcome.Success };
_session.HasValid = true;
// Act
await _root.EvaluateSessionAsync();
// Assert
Assert.IsType<ShellViewModel>(_root.CurrentViewModel);
Assert.False(_root.IsActivationRequired);
}
}