Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,545 @@
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using Omika.App.Local.Abstractions;
|
||||
using Omika.App.Local.Models;
|
||||
using Omika.App.Local.Services;
|
||||
using Omika.App.Navigation;
|
||||
using Omika.App.ViewModels;
|
||||
using Omika.Core.Contracts.Clients;
|
||||
using Omika.Core.Contracts.Common;
|
||||
using Omika.Core.Contracts.Interventions;
|
||||
using Omika.Core.Observability;
|
||||
|
||||
namespace Omika.Tests.Mobile.Local;
|
||||
|
||||
/// <summary>
|
||||
/// Tests du ViewModel de la liste des dossiers : chargement, états vide/erreur,
|
||||
/// six critères de recherche (Titre, Client, Statut, Type, Référence externe,
|
||||
/// Référence interne), épinglage local, reset et navigation. La recherche est
|
||||
/// déléguée au service (côté serveur) : le fake filtre les données locales
|
||||
/// comme le ferait la requête SQL LIKE.
|
||||
///
|
||||
/// Couvre aussi la politique d'erreur de la recherche : elle atteint l'API sans
|
||||
/// garde intermédiaire (<c>DossierLocalService.SearchDossiersAsync</c> →
|
||||
/// <c>_apiClient</c>), donc un échec réseau — ticket de production du 14/10,
|
||||
/// « Connection failure » — doit partir en Warning/P2, jamais en Error/P1.
|
||||
/// </summary>
|
||||
[Collection("TelemetrieStatique")]
|
||||
public class DossiersViewModelTests : LocalTestBase
|
||||
{
|
||||
private readonly DossiersViewModel _viewModel;
|
||||
private readonly FakeDossierService _service;
|
||||
private readonly StubParametreMetierService _parametreMetier = new();
|
||||
private readonly NavigationService _navigation;
|
||||
|
||||
public DossiersViewModelTests()
|
||||
{
|
||||
var inner = new DossierLocalService(DossierRepository, ClientRepository, DossierStateRepository, DossierInterventionCacheRepository, ApiClient);
|
||||
_service = new FakeDossierService(inner);
|
||||
_navigation = new NavigationService();
|
||||
_viewModel = new DossiersViewModel(_service, _parametreMetier, _navigation);
|
||||
}
|
||||
|
||||
private static LocalDossier CreateDossier(
|
||||
Guid id,
|
||||
string reference,
|
||||
string titre,
|
||||
Guid? clientId = null,
|
||||
string? statut = null,
|
||||
string? type = null,
|
||||
string? refInterne = null,
|
||||
string? refExterne = null,
|
||||
int nbInterventions = 0)
|
||||
{
|
||||
var dto = new DossierDetailsDto
|
||||
{
|
||||
Id = id.ToString(),
|
||||
ReferenceMetier = reference,
|
||||
Titre = titre,
|
||||
ClientId = clientId?.ToString(),
|
||||
Statut = statut is null ? null : new ReferenceValueDto { Label = statut },
|
||||
Type = type is null ? null : new ReferenceValueDto { Label = type },
|
||||
RefInterne = refInterne,
|
||||
RefExterne = refExterne,
|
||||
NbInterventionsNonArchivees = nbInterventions
|
||||
};
|
||||
return new LocalDossier
|
||||
{
|
||||
Id = id,
|
||||
ClientId = clientId,
|
||||
PayloadJson = JsonSerializer.Serialize(dto),
|
||||
UpdatedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static LocalClient CreateClient(Guid id, string nom)
|
||||
{
|
||||
var dto = new ClientDetailsDto { Id = id.ToString(), Nom = nom };
|
||||
return new LocalClient
|
||||
{
|
||||
Id = id,
|
||||
PayloadJson = JsonSerializer.Serialize(dto),
|
||||
UpdatedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_NAfficheAucunResultatAvantRecherche()
|
||||
{
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "A"));
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.False(_viewModel.IsLoading);
|
||||
Assert.False(_viewModel.HasDossiers);
|
||||
Assert.Empty(_viewModel.Dossiers);
|
||||
Assert.False(_viewModel.HasSearched);
|
||||
Assert.True(_viewModel.IsSearchNotStarted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_AucunDossier_Empty()
|
||||
{
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.False(_viewModel.IsLoading);
|
||||
Assert.False(_viewModel.HasDossiers);
|
||||
Assert.True(_viewModel.IsSearchNotStarted);
|
||||
Assert.Null(_viewModel.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FiltreTitre_InsensibleCasse()
|
||||
{
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "Alpha"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "Beta"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.TitreText = "alpha";
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
var item = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Equal("Alpha", item.Titre);
|
||||
Assert.Equal("alpha", _service.LastTitre);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FiltreStatut()
|
||||
{
|
||||
_parametreMetier.DossierStatuts = [new() { Label = "Ouvert" }, new() { Label = "Clôturé" }];
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "A", statut: "Ouvert"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "B", statut: "Clôturé"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.SelectedStatutIndex = _viewModel.StatutOptions.ToList().IndexOf("Ouvert");
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
var item = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Equal("DOS-001", item.Reference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FiltreType()
|
||||
{
|
||||
_parametreMetier.DossierTypes = [new() { Label = "Maintenance" }, new() { Label = "Installation" }];
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "A", type: "Maintenance"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "B", type: "Installation"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.SelectedTypeIndex = _viewModel.TypeOptions.ToList().IndexOf("Installation");
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
var item = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Equal("DOS-002", item.Reference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FiltreClient_ParNomHumain()
|
||||
{
|
||||
Guid clientAlpha = Guid.NewGuid();
|
||||
Guid clientBeta = Guid.NewGuid();
|
||||
await ClientRepository.UpsertAsync(CreateClient(clientAlpha, "Client Alpha"));
|
||||
await ClientRepository.UpsertAsync(CreateClient(clientBeta, "Client Beta"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "A", clientId: clientAlpha));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "B", clientId: clientBeta));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.ClientText = "Client Alpha";
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
var item = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Equal("DOS-001", item.Reference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FiltreRefInterne()
|
||||
{
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "A", refInterne: "INT-42"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "B", refInterne: "INT-7"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.RefInterneText = "42";
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
var item = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Equal("DOS-001", item.Reference);
|
||||
Assert.Equal("42", _service.LastRefInterne);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FiltreRefExterne()
|
||||
{
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "A", refExterne: "EXT-42"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "B", refExterne: "EXT-7"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.RefExterneText = "42";
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
var item = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Equal("DOS-001", item.Reference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reset_RetourneEtatInitialSansResultat()
|
||||
{
|
||||
_parametreMetier.DossierStatuts = [new() { Label = "Ouvert" }, new() { Label = "Clôturé" }];
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "Alpha", statut: "Ouvert"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "Beta", statut: "Clôturé"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.TitreText = "Alpha";
|
||||
_viewModel.SelectedStatutIndex = _viewModel.StatutOptions.ToList().IndexOf("Ouvert");
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
Assert.Single(_viewModel.Dossiers);
|
||||
Assert.True(_viewModel.HasActiveFilters);
|
||||
|
||||
_viewModel.ResetFiltersCommand.Execute(null);
|
||||
|
||||
Assert.Empty(_viewModel.Dossiers);
|
||||
Assert.False(_viewModel.HasSearched);
|
||||
Assert.True(_viewModel.IsSearchNotStarted);
|
||||
Assert.False(_viewModel.HasActiveFilters);
|
||||
Assert.Empty(_viewModel.TitreText);
|
||||
Assert.Equal(0, _viewModel.SelectedStatutIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pin_Persiste_EtFiltreEpingles()
|
||||
{
|
||||
Guid id = Guid.NewGuid();
|
||||
await DossierRepository.UpsertAsync(CreateDossier(id, "DOS-001", "A"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "B"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
// Épingler DOS-001.
|
||||
var item = _viewModel.Dossiers.First(d => d.Reference == "DOS-001");
|
||||
await _viewModel.TogglePinnedCommand.ExecuteAsync(item);
|
||||
|
||||
// L'état épinglé est persisté (relecture depuis le repository local).
|
||||
var pinned = await DossierStateRepository.GetPinnedIdsAsync();
|
||||
Assert.Contains(id, pinned);
|
||||
|
||||
// Onglet « Épinglés » : seule DOS-001 reste.
|
||||
_viewModel.ShowPinnedCommand.Execute(null);
|
||||
var only = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Equal("DOS-001", only.Reference);
|
||||
|
||||
// Retour « Récentes » : les résultats serveur (2) sont restaurés.
|
||||
_viewModel.ShowRecentCommand.Execute(null);
|
||||
Assert.Equal(2, _viewModel.Dossiers.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TogglePinned_AppliqueImmediatement_SansRechargerLaListe()
|
||||
{
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "Alpha"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "Beta"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
// La collection affichée sert de témoin : un rechargement la remplacerait.
|
||||
var collection = _viewModel.Dossiers;
|
||||
var item = _viewModel.Dossiers[0];
|
||||
|
||||
await _viewModel.TogglePinnedCommand.ExecuteAsync(item);
|
||||
|
||||
Assert.True(item.IsPinned);
|
||||
Assert.Same(collection, _viewModel.Dossiers);
|
||||
Assert.Equal(2, _viewModel.Dossiers.Count);
|
||||
|
||||
// Et l'état est bien persisté (relecture depuis le repository local).
|
||||
Assert.Contains(item.DossierGuid, await DossierStateRepository.GetPinnedIdsAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EtatVide_SansDonneesEtSansResultat_AfficheAucunResultat()
|
||||
{
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
Assert.True(_viewModel.IsSearchNotStarted);
|
||||
Assert.False(_viewModel.HasNoResults);
|
||||
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
Assert.True(_viewModel.HasSearched);
|
||||
Assert.True(_viewModel.HasNoResults);
|
||||
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "A"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
_viewModel.TitreText = "introuvable";
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.True(_viewModel.HasSearched);
|
||||
Assert.True(_viewModel.HasNoResults);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChangementCritereSansRecherche_NeModifiePasLaListe()
|
||||
{
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "Alpha"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "Beta"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.TitreText = "Alpha";
|
||||
|
||||
Assert.False(_viewModel.HasSearched);
|
||||
Assert.Empty(_viewModel.Dossiers);
|
||||
Assert.True(_viewModel.IsSearchNotStarted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RechercheVide_AfficheTousLesNonArchives()
|
||||
{
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-001", "Alpha"));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "Beta"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.True(_viewModel.HasSearched);
|
||||
Assert.Equal(2, _viewModel.Dossiers.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EpinglesSansRecherche_AfficheFavorisPuisRetourEtatInitial()
|
||||
{
|
||||
Guid id = Guid.NewGuid();
|
||||
await DossierRepository.UpsertAsync(CreateDossier(id, "DOS-001", "Alpha"));
|
||||
await DossierStateRepository.SetPinnedAsync(
|
||||
id,
|
||||
true,
|
||||
JsonSerializer.Serialize(new DossierListItem { DossierGuid = id, Reference = "DOS-001", Titre = "Alpha" }));
|
||||
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), "DOS-002", "Beta"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
_viewModel.ShowPinnedCommand.Execute(null);
|
||||
|
||||
var only = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Equal("DOS-001", only.Reference);
|
||||
|
||||
_viewModel.ShowRecentCommand.Execute(null);
|
||||
|
||||
Assert.Empty(_viewModel.Dossiers);
|
||||
Assert.False(_viewModel.HasSearched);
|
||||
Assert.True(_viewModel.IsSearchNotStarted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CardMapping_TypeAbsentMasqueEtReferencesPropagees()
|
||||
{
|
||||
await DossierRepository.UpsertAsync(CreateDossier(
|
||||
Guid.NewGuid(),
|
||||
"DOS-001",
|
||||
"",
|
||||
type: "Aucun Type de dossier",
|
||||
refInterne: "INT-42",
|
||||
refExterne: "EXT-42"));
|
||||
await _viewModel.LoadCommand.ExecuteAsync(null);
|
||||
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
|
||||
var item = Assert.Single(_viewModel.Dossiers);
|
||||
Assert.Null(item.Type);
|
||||
Assert.Null(item.Titre);
|
||||
Assert.Equal("INT-42", item.RefInterne);
|
||||
Assert.Equal("EXT-42", item.RefExterne);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake de service : délègue au service local réel (lecture SQLite) mais
|
||||
/// implémente <see cref="SearchDossiersAsync"/> par un filtre en mémoire des
|
||||
/// données locales, simulant la requête SQL LIKE côté serveur.
|
||||
/// </summary>
|
||||
private sealed class FakeDossierService : IDossierLocalService
|
||||
{
|
||||
private readonly IDossierLocalService _inner;
|
||||
|
||||
public string? LastRefInterne { get; private set; }
|
||||
public string? LastTitre { get; private set; }
|
||||
public string? LastClient { get; private set; }
|
||||
public string? LastStatut { get; private set; }
|
||||
public string? LastType { get; private set; }
|
||||
public string? LastRefExterne { get; private set; }
|
||||
|
||||
public FakeDossierService(IDossierLocalService inner) => _inner = inner;
|
||||
|
||||
public Task<IReadOnlyList<DossierListItem>> GetDossiersAsync(CancellationToken ct = default)
|
||||
=> _inner.GetDossiersAsync(ct);
|
||||
|
||||
/// <summary>Simule un échec de la recherche serveur (réseau ou métier).</summary>
|
||||
public Exception? SearchException { get; set; }
|
||||
|
||||
public async Task<IReadOnlyList<DossierListItem>> SearchDossiersAsync(
|
||||
string? refInterne,
|
||||
string? titre,
|
||||
string? client,
|
||||
string? statut,
|
||||
string? type,
|
||||
string? refExterne,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (SearchException is not null)
|
||||
{
|
||||
throw SearchException;
|
||||
}
|
||||
|
||||
LastRefInterne = refInterne;
|
||||
LastTitre = titre;
|
||||
LastClient = client;
|
||||
LastStatut = statut;
|
||||
LastType = type;
|
||||
LastRefExterne = refExterne;
|
||||
|
||||
IReadOnlyList<DossierListItem> all = await _inner.GetDossiersAsync(ct);
|
||||
IEnumerable<DossierListItem> filtered = all;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(refInterne))
|
||||
{
|
||||
filtered = filtered.Where(d => Contains(d.RefInterne, refInterne));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(titre))
|
||||
{
|
||||
filtered = filtered.Where(d => Contains(d.Titre, titre));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(client))
|
||||
{
|
||||
filtered = filtered.Where(d => Contains(d.ClientNom, client));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(statut))
|
||||
{
|
||||
filtered = filtered.Where(d => string.Equals(d.Statut, statut, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(type))
|
||||
{
|
||||
filtered = filtered.Where(d => string.Equals(d.Type, type, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(refExterne))
|
||||
{
|
||||
filtered = filtered.Where(d => Contains(d.RefExterne, refExterne));
|
||||
}
|
||||
|
||||
return filtered.ToList();
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DossierListItem>> GetLastSearchAsync(CancellationToken ct = default)
|
||||
=> _inner.GetLastSearchAsync(ct);
|
||||
|
||||
public Task ClearLastSearchAsync(CancellationToken ct = default)
|
||||
=> _inner.ClearLastSearchAsync(ct);
|
||||
|
||||
private static bool Contains(string? value, string query)
|
||||
=> value is not null && value.Contains(query, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public Task<DossierDetail?> GetDossierByIdAsync(Guid dossierGuid, CancellationToken ct = default)
|
||||
=> _inner.GetDossierByIdAsync(dossierGuid, ct);
|
||||
|
||||
public Task SetPinnedAsync(Guid dossierGuid, bool isPinned, DossierListItem? item, CancellationToken ct = default)
|
||||
=> _inner.SetPinnedAsync(dossierGuid, isPinned, item, ct);
|
||||
|
||||
public Task<IReadOnlyList<DossierListItem>> GetPinnedAsync(CancellationToken ct = default)
|
||||
=> _inner.GetPinnedAsync(ct);
|
||||
|
||||
public Task<IReadOnlyList<DossierIntervention>> GetDossierInterventionsAsync(Guid dossierGuid, CancellationToken ct = default)
|
||||
=> _inner.GetDossierInterventionsAsync(dossierGuid, ct);
|
||||
|
||||
public Task<IReadOnlyList<DossierIntervention>> RefreshDossierInterventionsAsync(Guid dossierGuid, CancellationToken ct = default)
|
||||
=> _inner.RefreshDossierInterventionsAsync(dossierGuid, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ticket de production du 14/10 (« Connection failure ») : l'échec réseau de la
|
||||
/// recherche n'est pas un incident. L'événement part en Warning/P2, l'écran n'entre
|
||||
/// dans aucun groupe d'erreurs du Diagnostic Center, et l'utilisateur reçoit le
|
||||
/// libellé français stable plutôt qu'un message technique anglais.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Search_EchecReseau_SignaleEnWarningSansIncident()
|
||||
{
|
||||
_service.SearchException = new HttpRequestException("Connection failure");
|
||||
|
||||
var telemetry = new CapturingTelemetry();
|
||||
ExceptionTelemetry.Initialize(telemetry, null);
|
||||
try
|
||||
{
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ExceptionTelemetry.Initialize(null, null);
|
||||
}
|
||||
|
||||
Assert.Equal("Hors ligne — impossible de contacter le serveur.", _viewModel.ErrorMessage);
|
||||
|
||||
DiagnosticEvent evt = EvenementDossiers(telemetry);
|
||||
Assert.Equal(TelemetryLevel.Warning, evt.Level);
|
||||
Assert.Equal("HttpRequestException", evt.ErrorCode);
|
||||
AssertAucunIncident(telemetry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contre-épreuve : une erreur métier (hors réseau) reste un incident Error/P1.
|
||||
/// Le classement ne dégrade pas les vraies erreurs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Search_ErreurMetier_ResteUnIncident()
|
||||
{
|
||||
_service.SearchException = new InvalidOperationException("échec de recherche");
|
||||
|
||||
var telemetry = new CapturingTelemetry();
|
||||
ExceptionTelemetry.Initialize(telemetry, null);
|
||||
try
|
||||
{
|
||||
await _viewModel.SearchCommand.ExecuteAsync(null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ExceptionTelemetry.Initialize(null, null);
|
||||
}
|
||||
|
||||
DiagnosticEvent evt = EvenementDossiers(telemetry);
|
||||
Assert.Equal(TelemetryLevel.Error, evt.Level);
|
||||
Assert.Equal("InvalidOperationException", evt.ErrorCode);
|
||||
}
|
||||
|
||||
/// <summary>Seul événement de diagnostic attribué à ce ViewModel (l'usage est un TelemetryEvent).</summary>
|
||||
private static DiagnosticEvent EvenementDossiers(CapturingTelemetry telemetry) =>
|
||||
Assert.Single(
|
||||
telemetry.Events.OfType<DiagnosticEvent>(),
|
||||
e => e.Component == nameof(DossiersViewModel));
|
||||
|
||||
/// <summary>Aucun événement de diagnostic de cet écran en Error/Critical.</summary>
|
||||
private static void AssertAucunIncident(CapturingTelemetry telemetry) =>
|
||||
Assert.DoesNotContain(
|
||||
telemetry.Events.OfType<DiagnosticEvent>(),
|
||||
e => e.Component == nameof(DossiersViewModel) && e.Level == TelemetryLevel.Error);
|
||||
|
||||
/// <summary>Capture des événements de la télémétrie statique globale.</summary>
|
||||
private sealed class CapturingTelemetry : IOmikaTelemetry
|
||||
{
|
||||
public List<OmikaEvent> Events { get; } = [];
|
||||
|
||||
public void Track(TelemetryEvent telemetryEvent) => Events.Add(telemetryEvent);
|
||||
|
||||
public void Log(DiagnosticEvent diagnosticEvent) => Events.Add(diagnosticEvent);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user