244 lines
11 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|