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

168 lines
6.8 KiB
C#

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;
/// <summary>
/// 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é.
/// </summary>
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));
/// <summary>
/// Client API dont la lecture de compte rendu est pilotable par le test.
///
/// L'interface est redéclarée volontairement : <c>GetCompteRenduByRendezVousAsync</c>
/// est une implémentation par défaut de <see cref="IMobileApiClient"/>, et une classe
/// dérivée ne peut la remplacer qu'en réimplémentant explicitement l'interface.
/// </summary>
private sealed class CompteRenduApiClient : StubMobileApiClient, IMobileApiClient
{
public int CompteRenduCalls { get; private set; }
public Exception? ExceptionToThrow { get; set; }
public CompteRenduDto? Result { get; set; }
public Task<CompteRenduDto?> GetCompteRenduByRendezVousAsync(
Guid rendezVousGuid,
CancellationToken cancellationToken = default)
{
CompteRenduCalls++;
return ExceptionToThrow is { } ex
? Task.FromException<CompteRenduDto?>(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
});
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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é.
/// </summary>
[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);
}
/// <summary>
/// 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).
/// </summary>
[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);
}
/// <summary>
/// É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).
/// </summary>
[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);
}
}