using System.Net.Http; using System.Text.Json; using Omika.App.Local.Abstractions; using Omika.App.Local.Models; using Omika.App.Local.Repositories; using Omika.App.Local.Services; using Omika.Core.Contracts.Interventions; namespace Omika.Tests.Mobile.Local; /// /// Tests du service de périmètre du compte rendu (anti-doublon du bouton Créer). /// /// Ce service ne fait que dériver « existe / n'existe pas » de la lecture du compte /// rendu : c'est donc lui qui, le 09/10 en production, a propagé l'échec réseau /// « Connection failure » jusqu'au ViewModel, faute de garde dans la couche locale. /// Les tests vérifient qu'un échec réseau ne lève plus jamais et que la copie locale /// reste la source de vérité hors ligne. /// /// Limite assumée (documentée) : hors ligne et sans copie locale, « inconnu » est /// traité comme « absent » — le compte rendu sera créé localement puis synchronisé. /// public class CompteRenduScopeServiceTests : LocalTestBase { private readonly CompteRenduScopeService _scopeService; public CompteRenduScopeServiceTests() => _scopeService = new CompteRenduScopeService( new InterventionLocalService( InterventionRepository, ClientRepository, SiteRepository, DossierRepository, RendezVousRepository, DocumentMetadataRepository, CollaborateurRepository, OutboxService, ReconciliationService, ApiClient, InterventionCacheRepository, ContextInterventionCacheRepository, Database, Network)); /// /// Client API dont la lecture de compte rendu est pilotable par le test. /// /// L'interface est redéclarée volontairement : GetCompteRenduByRendezVousAsync /// est une implémentation par défaut de , et une classe /// dérivée ne peut la remplacer qu'en réimplémentant explicitement l'interface. /// private sealed class CompteRenduApiClient : StubMobileApiClient, IMobileApiClient { public int CompteRenduCalls { get; private set; } public Exception? ExceptionToThrow { get; set; } public CompteRenduDto? Result { get; set; } public Task GetCompteRenduByRendezVousAsync( Guid rendezVousGuid, CancellationToken cancellationToken = default) { CompteRenduCalls++; return ExceptionToThrow is { } ex ? Task.FromException(ex) : Task.FromResult(Result); } } private static AgendaItem CreateRendezVous(Guid rendezVousGuid) => new() { RendezVousGuid = rendezVousGuid, Titre = "RDV", Debut = DateTime.Now.AddDays(-1).AddHours(10), Fin = DateTime.Now.AddDays(-1).AddHours(12) }; private CompteRenduScopeService CreateScopeService(IMobileApiClient apiClient, bool online) { Network.Online = online; return new CompteRenduScopeService( new InterventionLocalService( InterventionRepository, ClientRepository, SiteRepository, DossierRepository, RendezVousRepository, DocumentMetadataRepository, CollaborateurRepository, OutboxService, ReconciliationService, apiClient, InterventionCacheRepository, ContextInterventionCacheRepository, Database, Network)); } private async Task SeedCompteRenduLocalAsync(Guid rendezVousGuid, string note) { var repository = new CompteRenduLocalRepository(Database); await repository.UpsertAsync(new LocalCompteRendu { RendezVousId = rendezVousGuid, PayloadJson = JsonSerializer.Serialize(new CompteRenduDto { Id = Guid.NewGuid().ToString("D"), RendezVousId = rendezVousGuid.ToString("D"), NoteCollaborateur = note }), UpdatedAtUtc = DateTimeOffset.UtcNow }); } /// /// Ticket de production du 09/10 : le serveur est injoignable au moment du contrôle /// anti-doublon. La vérification doit répondre « pas de compte rendu connu » au lieu /// de lever — c'est ce qui permet la création hors ligne au lieu d'une erreur P1. /// [Fact] public async Task CompteRenduExistsAsync_EchecReseauSansCopieLocale_RenvoieFauxSansLever() { var client = new CompteRenduApiClient { ExceptionToThrow = new HttpRequestException("Connection failure") }; CompteRenduScopeService scope = CreateScopeService(client, online: true); bool existe = await scope.CompteRenduExistsAsync(CreateRendezVous(Guid.NewGuid())); Assert.False(existe); Assert.Equal(1, client.CompteRenduCalls); } /// /// Hors ligne, la copie locale suffit à préserver l'anti-doublon : un compte rendu /// déjà connu localement n'est pas recréé, et aucun appel réseau n'est tenté. /// [Fact] public async Task CompteRenduExistsAsync_CopieLocalePresente_HorsLigne_RenvoieVrai() { Guid rendezVousGuid = Guid.NewGuid(); await SeedCompteRenduLocalAsync(rendezVousGuid, "Note locale"); var client = new CompteRenduApiClient(); CompteRenduScopeService scope = CreateScopeService(client, online: false); bool existe = await scope.CompteRenduExistsAsync(CreateRendezVous(rendezVousGuid)); Assert.True(existe); Assert.Equal(0, client.CompteRenduCalls); } /// /// Contre-épreuve : la création est refusée quand le compte rendu existe déjà dans /// le périmètre local (le doublon reste impossible hors ligne, sans réseau). /// [Fact] public async Task CanCreateCompteRenduAsync_CopieLocalePresente_HorsLigne_RefuseLaCreation() { Guid rendezVousGuid = Guid.NewGuid(); await SeedCompteRenduLocalAsync(rendezVousGuid, "Note locale"); CompteRenduScopeService scope = CreateScopeService(new CompteRenduApiClient(), online: false); bool peutCreer = await scope.CanCreateCompteRenduAsync(CreateRendezVous(rendezVousGuid)); Assert.False(peutCreer); } /// /// Échec réseau sans copie locale : aucune validation ne peut être affirmée — /// « non validé » est retourné sans exception (jamais de blocage de l'écran). /// [Fact] public async Task CompteRenduIsValidatedAsync_EchecReseauSansCopieLocale_RenvoieFaux() { var client = new CompteRenduApiClient { ExceptionToThrow = new HttpRequestException("Connection failure") }; CompteRenduScopeService scope = CreateScopeService(client, online: true); bool valide = await scope.CompteRenduIsValidatedAsync(CreateRendezVous(Guid.NewGuid())); Assert.False(valide); } }