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

244 lines
11 KiB
C#

using Omika.App.Local.Abstractions;
using Omika.App.Local.Models;
using Omika.App.Local.Services;
using Omika.Core.Contracts.Interventions;
using Omika.Core.Contracts.Mobile.Bootstrap;
using Omika.Core.Contracts.Mobile.Documents;
using Omika.Core.Contracts.Mobile.Interventions;
using Omika.Core.Contracts.Mobile.License;
using Omika.Core.Contracts.Mobile.Snapshot;
using Omika.Core.Contracts.Mobile.Sync;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Tests du cache local Interventions (recherche à la demande + favoris),
/// au niveau service réel (InterventionLocalService + InterventionCacheRepository).
/// Vérifie le comportement métier offline-first sans API réelle.
/// </summary>
public class InterventionOnDemandTests : LocalTestBase
{
[Fact]
public async Task Search_Reussi_RemplaceDerniereRecherche()
{
var api = new ConfigurableApiClient
{
SearchResults = [Result("A", "Intervention A"), Result("B", "Intervention B")]
};
var service = CreateService(api);
var result = await service.SearchAsync(new MobileInterventionSearchRequest());
Assert.Equal(2, result.Count);
Assert.Equal(2, (await service.GetLastSearchAsync()).Count);
}
[Fact]
public async Task Search_Echoue_ConservePrecedente()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "A")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
api.SearchException = new InvalidOperationException("offline");
await Assert.ThrowsAsync<InvalidOperationException>(() => service.SearchAsync(new MobileInterventionSearchRequest()));
Assert.Single(await service.GetLastSearchAsync());
}
[Fact]
public async Task Search_ResultatsStockesLocalement_Redemarrage()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "A"), Result("B", "B")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
// Nouvelle instance sur la même base (simule un redémarrage).
var service2 = CreateService(api);
Assert.Equal(2, (await service2.GetLastSearchAsync()).Count);
}
[Fact]
public async Task PremiereOuverture_SansRecherche_ListeVide()
{
var service = CreateService(new ConfigurableApiClient());
Assert.Empty(await service.GetLastSearchAsync());
Assert.Empty(await service.GetPinnedAsync());
}
[Fact]
public async Task Pin_Persiste_SurvitANouvelleRecherche()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "A")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
Guid idA = (await service.GetLastSearchAsync())[0].InterventionGuid;
await service.SetPinnedAsync(idA, true);
// Nouvelle recherche remplaçant A par B.
api.SearchResults = [Result("B", "B")];
await service.SearchAsync(new MobileInterventionSearchRequest());
Assert.Single(await service.GetPinnedAsync());
Assert.Equal(idA, (await service.GetPinnedAsync())[0].InterventionGuid);
}
[Fact]
public async Task Unpin_DisparaitDesFavoris()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "A")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
Guid idA = (await service.GetLastSearchAsync())[0].InterventionGuid;
await service.SetPinnedAsync(idA, true);
Assert.Single(await service.GetPinnedAsync());
await service.SetPinnedAsync(idA, false);
Assert.Empty(await service.GetPinnedAsync());
}
[Fact]
public async Task Favoris_DisponiblesOffline()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "A")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
Guid idA = (await service.GetLastSearchAsync())[0].InterventionGuid;
await service.SetPinnedAsync(idA, true);
// Aucun appel API : lecture directe du cache.
var pinned = await service.GetPinnedAsync();
Assert.Single(pinned);
Assert.True(pinned[0].IsPinned);
}
[Fact]
public async Task RefreshPinned_UneSeuleRequeteBulk()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "A"), Result("B", "B")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
foreach (var item in await service.GetLastSearchAsync())
{
await service.SetPinnedAsync(item.InterventionGuid, true);
}
api.ByIdsResults = [Result("A", "A refresh"), Result("B", "B refresh")];
await service.RefreshPinnedAsync();
Assert.Equal(1, api.ByIdsCallCount);
Assert.Equal(2, api.LastIds?.Count);
}
[Fact]
public async Task RefreshPinned_Echoue_ConserveCache()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "Titre original")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
Guid idA = (await service.GetLastSearchAsync())[0].InterventionGuid;
await service.SetPinnedAsync(idA, true);
api.ByIdsException = new InvalidOperationException("offline");
await Assert.ThrowsAsync<InvalidOperationException>(() => service.RefreshPinnedAsync());
var pinned = await service.GetPinnedAsync();
Assert.Single(pinned);
Assert.Equal("Titre original", pinned[0].Titre);
}
[Fact]
public async Task RefreshDetail_MetAJourLeCache()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "Titre original")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
Guid idA = (await service.GetLastSearchAsync())[0].InterventionGuid;
api.ByIdsResults = [Result("A", "Titre rafraîchi", idA)];
await service.RefreshInterventionAsync(idA);
var cached = await service.GetLastSearchAsync();
Assert.Equal("Titre rafraîchi", cached[0].Titre);
}
[Fact]
public async Task SnapshotApplier_NeSupprimePasLeCache()
{
var api = new ConfigurableApiClient { SearchResults = [Result("A", "A")] };
var service = CreateService(api);
await service.SearchAsync(new MobileInterventionSearchRequest());
Guid idA = (await service.GetLastSearchAsync())[0].InterventionGuid;
await service.SetPinnedAsync(idA, true);
// Un refresh snapshot général (sans interventions) ne touche pas le cache.
var applier = new SnapshotApplier(Database);
await applier.ApplyAsync(new MobileSnapshotResponse { GeneratedAtUtc = DateTimeOffset.UtcNow });
Assert.Single(await service.GetLastSearchAsync());
Assert.Single(await service.GetPinnedAsync());
}
private InterventionLocalService CreateService(ConfigurableApiClient api)
=> new InterventionLocalService(
InterventionRepository, ClientRepository, SiteRepository, DossierRepository,
RendezVousRepository, DocumentMetadataRepository, CollaborateurRepository,
OutboxService, ReconciliationService, api, InterventionCacheRepository, ContextInterventionCacheRepository, Database);
private static MobileInterventionSearchResult Result(string reference, string titre, Guid? id = null)
=> new MobileInterventionSearchResult
{
Detail = new InterventionDetailsDto { Id = (id ?? Guid.NewGuid()).ToString(), Titre = titre, Reference = reference },
NbRendezVous = 1,
SiteNom = "Site",
ClientNom = "Client",
ContactNom = "Contact"
};
private sealed class ConfigurableApiClient : IMobileApiClient
{
public List<MobileInterventionSearchResult> SearchResults { get; set; } = [];
public List<MobileInterventionSearchResult> ByIdsResults { get; set; } = [];
public Exception? SearchException { get; set; }
public Exception? ByIdsException { get; set; }
public int ByIdsCallCount { get; set; }
public IReadOnlyList<string>? LastIds { get; set; }
public string? AccessToken { get; set; }
public Task<MobileLicenseSessionResponse> ActivateAsync(LicenseActivateRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<MobileLicenseSessionResponse> ValidateAsync(LicenseValidateRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<MobileLicenseSessionResponse> GetDevelopmentSessionAsync(string deviceId, CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<MobileBootstrapResponse> GetBootstrapAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<SyncOperationReceipt> SendOperationAsync(SyncOperation operation, CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<SyncStatusResponse> GetStatusAsync(Guid operationId, CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<MobileSnapshotResponse> GetSnapshotAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<DocumentDownloadResponse> DownloadDocumentAsync(Guid documentGuid, CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<DocumentUploadResponse> UploadDocumentAsync(DocumentUploadRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<IReadOnlyList<MobileInterventionSearchResult>> SearchInterventionsAsync(MobileInterventionSearchRequest request, CancellationToken cancellationToken = default)
{
if (SearchException is not null)
{
throw SearchException;
}
return Task.FromResult<IReadOnlyList<MobileInterventionSearchResult>>(SearchResults);
}
public Task<IReadOnlyList<MobileInterventionSearchResult>> GetInterventionsByIdsAsync(MobileInterventionsByIdsRequest request, CancellationToken cancellationToken = default)
{
ByIdsCallCount++;
LastIds = request.Ids;
if (ByIdsException is not null)
{
throw ByIdsException;
}
return Task.FromResult<IReadOnlyList<MobileInterventionSearchResult>>(ByIdsResults);
}
}
}