562 lines
23 KiB
C#
562 lines
23 KiB
C#
using System.Net.Http;
|
|
using Omika.App.Local.Abstractions;
|
|
using Omika.App.Local.Models;
|
|
using Omika.App.Navigation;
|
|
using Omika.App.ViewModels;
|
|
using Omika.Core.Contracts.Common;
|
|
using Omika.Core.Contracts.Configuration;
|
|
using Omika.Core.Contracts.Interventions;
|
|
using Omika.Core.Contracts.Organisation;
|
|
using Omika.Core.Observability;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests du CompteRenduCreateViewModel : vérifie que la création transmet
|
|
/// l'identité du collaborateur connecté (audit CreationUser/ModifUser) et que les
|
|
/// échecs réseau de cet écran — dont celui de l'anti-doublon survenu en production
|
|
/// le 09/10 (« Connection failure ») — sont classés en Warning/P2, jamais en
|
|
/// incident Error/P1.
|
|
/// </summary>
|
|
[Collection("TelemetrieStatique")]
|
|
public class CompteRenduCreateViewModelTests
|
|
{
|
|
private sealed class FakeInterventionLocalService : IInterventionLocalService
|
|
{
|
|
public CreateCompteRenduRequest? LastRequest { get; private set; }
|
|
|
|
public Task<Guid> CreateAsync(Omika.Core.Contracts.Mobile.Sync.InterventionSyncPayload payload, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<Guid> UpdateAsync(Guid interventionGuid, Omika.Core.Contracts.Mobile.Sync.InterventionSyncPayload payload, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<Guid> ArchiveAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<InterventionDetail?> GetInterventionByIdAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<InterventionDetail?>(null);
|
|
|
|
public Task<IReadOnlyList<InterventionListItem>> GetInterventionsAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionListItem>>([]);
|
|
|
|
public Task<CompteRenduDto> CreateCompteRenduAsync(
|
|
Guid rendezVousGuid,
|
|
CreateCompteRenduRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
LastRequest = request;
|
|
return Task.FromResult(new CompteRenduDto { Id = Guid.NewGuid().ToString() });
|
|
}
|
|
}
|
|
|
|
private sealed class FakeInterventionContactsService : IInterventionContactsService
|
|
{
|
|
public Task<IReadOnlyList<InterventionContact>> GetContactsForInterventionAsync(
|
|
Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionContact>>([]);
|
|
|
|
public Task<InterventionContact?> GetContactByIdAsync(
|
|
Guid contactGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<InterventionContact?>(null);
|
|
|
|
public Task<IReadOnlyList<InterventionContact>> GetContactsDisponiblesAsync(
|
|
Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionContact>>([]);
|
|
}
|
|
|
|
private sealed class FakeParametreMetierService : IParametreMetierLocalService
|
|
{
|
|
/// <summary>Référentiel « type de signataire » exposé par le bootstrap local.</summary>
|
|
public IReadOnlyList<ReferenceValueDto> TypesSignataire { get; set; } = [];
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetTypeTempsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<TypeTempsParametresDto?> GetTypeTempsParametresAsync(string typeTempsKey, CancellationToken ct = default)
|
|
=> Task.FromResult<TypeTempsParametresDto?>(null);
|
|
|
|
public Task<TypeRdvParametresDto?> GetTypeRdvParametresAsync(string typeRdvKey, CancellationToken ct = default)
|
|
=> Task.FromResult<TypeRdvParametresDto?>(null);
|
|
|
|
public Task<StatutRdvParametresDto?> GetStatutRdvParametresAsync(string statutRdvKey, CancellationToken ct = default)
|
|
=> Task.FromResult<StatutRdvParametresDto?>(null);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetInterventionTypesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetInterventionEtatsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetDossierTypesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetDossierStatutsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetClientTypesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetClientStatutsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetSiteTypesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetTypeSignatairesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult(TypesSignataire);
|
|
}
|
|
|
|
private sealed class FakeCompteRenduScopeService : ICompteRenduScopeService
|
|
{
|
|
/// <summary>Exception levée par la lecture du compte rendu (chargement de l'écran).</summary>
|
|
public Exception? ExceptionOnGet { get; set; }
|
|
|
|
/// <summary>Exception levée par la vérification d'existence (anti-doublon du bouton Créer).</summary>
|
|
public Exception? ExceptionOnExists { get; set; }
|
|
|
|
public CompteRenduScope ResolveScope(AgendaItem rendezVous)
|
|
=> new(rendezVous.RendezVousGuid, DateOnly.FromDateTime(rendezVous.Debut), rendezVous.InterventionGuid);
|
|
|
|
public Task<CompteRenduDto?> GetCompteRenduAsync(AgendaItem rendezVous, CancellationToken ct = default)
|
|
=> ExceptionOnGet is { } ex
|
|
? Task.FromException<CompteRenduDto?>(ex)
|
|
: Task.FromResult<CompteRenduDto?>(null);
|
|
|
|
public Task<bool> CompteRenduExistsAsync(AgendaItem rendezVous, CancellationToken ct = default)
|
|
=> ExceptionOnExists is { } ex
|
|
? Task.FromException<bool>(ex)
|
|
: Task.FromResult(false);
|
|
|
|
public Task<bool> CompteRenduIsValidatedAsync(AgendaItem rendezVous, CancellationToken ct = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<bool> CanCreateCompteRenduAsync(AgendaItem rendezVous, CancellationToken ct = default)
|
|
=> Task.FromResult(true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Save_TransmetLeCollaborateurConnectePourAudit()
|
|
{
|
|
// Arrange : un collaborateur connecté (bootstrap) et un rendez-vous passé.
|
|
Guid collaborateurGuid = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
|
var session = new StubSessionService
|
|
{
|
|
Bootstrap = new LocalBootstrapState
|
|
{
|
|
Collaborateur = new CollaborateurDetailsDto
|
|
{
|
|
Id = collaborateurGuid.ToString("D"),
|
|
NomAffiche = "Technicien"
|
|
}
|
|
}
|
|
};
|
|
var intervention = new FakeInterventionLocalService();
|
|
var vm = new CompteRenduCreateViewModel(
|
|
intervention,
|
|
new FakeInterventionContactsService(),
|
|
session,
|
|
new FakeParametreMetierService(),
|
|
new NavigationService(),
|
|
new FakeCompteRenduScopeService());
|
|
|
|
DateTime passe = DateTime.Now.AddDays(-1);
|
|
var rdv = new AgendaItem
|
|
{
|
|
RendezVousGuid = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
|
Titre = "RDV",
|
|
Debut = passe.AddHours(10),
|
|
Fin = passe.AddHours(12),
|
|
InterventionGuid = Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd")
|
|
};
|
|
|
|
// Act : chargement puis enregistrement (création du compte rendu).
|
|
await vm.LoadCommand.ExecuteAsync(rdv);
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Assert : l'identité du collaborateur est transmise (jamais null/0).
|
|
Assert.NotNull(intervention.LastRequest);
|
|
Assert.Equal(collaborateurGuid.ToString("D"), intervention.LastRequest!.CollaborateurId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Le référentiel « type de signataire » est proposé dans la liste déroulante
|
|
/// du compte rendu (source unique de CR_CleTypeSignataire).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Load_ExposeLeReferentielDesTypesDeSignataire()
|
|
{
|
|
FakeParametreMetierService parametres = TypesSignataireParDefaut();
|
|
var vm = CreateViewModel(parametres);
|
|
|
|
await vm.LoadCommand.ExecuteAsync(CreateRendezVous());
|
|
|
|
Assert.Equal(2, vm.TypesSignataire.Count);
|
|
Assert.False(vm.AucunTypeSignataire);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Quand le rôle de la personne correspond au référentiel, le type est
|
|
/// pré-sélectionné ET transmis à la création (colonne renseignée).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Save_TypeDeSignataireDeduit_EstTransmis()
|
|
{
|
|
var intervention = new FakeInterventionLocalService();
|
|
var vm = CreateViewModel(TypesSignataireParDefaut(), intervention);
|
|
|
|
await vm.LoadCommand.ExecuteAsync(CreateRendezVous());
|
|
|
|
// Le client est assimilé au « Propriétaire » du référentiel local.
|
|
Assert.NotNull(vm.SelectedTypeSignataire);
|
|
Assert.Equal("T.SIGN.PROP", vm.SelectedTypeSignataire!.Key);
|
|
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
Assert.NotNull(intervention.LastRequest);
|
|
Assert.Equal("T.SIGN.PROP", intervention.LastRequest!.TypeSignataire?.Key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cas du bug constaté : aucun rôle ne correspond au référentiel (le type
|
|
/// n'est pas déductible). L'utilisateur choisit alors explicitement le type
|
|
/// dans la liste déroulante, et cette valeur est transmise.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Save_TypeDeSignataireNonDeduit_ObligeLeChoixPuisLeTransmet()
|
|
{
|
|
var intervention = new FakeInterventionLocalService();
|
|
var parametres = new FakeParametreMetierService
|
|
{
|
|
// Aucun libellé ne correspond au rôle « Client » : rien n'est déduit.
|
|
TypesSignataire =
|
|
[
|
|
new ReferenceValueDto { Key = "T.SIGN.MAND", Label = "Mandataire" },
|
|
new ReferenceValueDto { Key = "T.SIGN.OCC", Label = "Occupant" }
|
|
]
|
|
};
|
|
|
|
var vm = CreateViewModel(parametres, intervention);
|
|
|
|
await vm.LoadCommand.ExecuteAsync(CreateRendezVous());
|
|
|
|
Assert.Null(vm.SelectedTypeSignataire);
|
|
Assert.True(vm.TypeSignataireManquant);
|
|
|
|
// Sans choix, la création est bloquée avec un message explicite : le
|
|
// compte rendu ne doit pas partir avec un type de signataire vide.
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
Assert.Null(intervention.LastRequest);
|
|
Assert.Equal("Veuillez sélectionner le type de signataire.", vm.ErrorMessage);
|
|
|
|
// Choix explicite dans la liste déroulante.
|
|
vm.SelectedTypeSignataire = vm.TypesSignataire[1];
|
|
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
Assert.NotNull(intervention.LastRequest);
|
|
Assert.Equal("T.SIGN.OCC", intervention.LastRequest!.TypeSignataire?.Key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Référentiel vide (paramétrage absent du bootstrap) : la création n'est pas
|
|
/// bloquée — aucune valeur n'est inventée, mais l'utilisateur est informé.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Save_ReferentielVide_NeBloquePasLaCreation()
|
|
{
|
|
var intervention = new FakeInterventionLocalService();
|
|
var vm = CreateViewModel(new FakeParametreMetierService(), intervention);
|
|
|
|
await vm.LoadCommand.ExecuteAsync(CreateRendezVous());
|
|
|
|
Assert.True(vm.AucunTypeSignataire);
|
|
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
Assert.NotNull(intervention.LastRequest);
|
|
Assert.Null(intervention.LastRequest!.TypeSignataire);
|
|
}
|
|
|
|
private static FakeParametreMetierService TypesSignataireParDefaut()
|
|
=> new()
|
|
{
|
|
TypesSignataire =
|
|
[
|
|
new ReferenceValueDto { Key = "T.SIGN.PROP", Label = "Propriétaire" },
|
|
new ReferenceValueDto { Key = "T.SIGN.LOC", Label = "Locataire" }
|
|
]
|
|
};
|
|
|
|
private static AgendaItem CreateRendezVous()
|
|
{
|
|
DateTime passe = DateTime.Now.AddDays(-1);
|
|
return new AgendaItem
|
|
{
|
|
RendezVousGuid = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
|
Titre = "RDV",
|
|
Debut = passe.AddHours(10),
|
|
Fin = passe.AddHours(12),
|
|
// Un compte rendu documente TOUJOURS une intervention : les rendez-vous
|
|
// de ces tests en portent une, comme en production.
|
|
InterventionGuid = Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
|
|
ClientNom = "Client Test"
|
|
};
|
|
}
|
|
|
|
private static CompteRenduCreateViewModel CreateViewModel(
|
|
FakeParametreMetierService parametres,
|
|
FakeInterventionLocalService? intervention = null,
|
|
FakeCompteRenduScopeService? scope = null)
|
|
{
|
|
var session = new StubSessionService
|
|
{
|
|
Bootstrap = new LocalBootstrapState
|
|
{
|
|
Collaborateur = new CollaborateurDetailsDto
|
|
{
|
|
Id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
|
NomAffiche = "Technicien"
|
|
}
|
|
}
|
|
};
|
|
|
|
return new CompteRenduCreateViewModel(
|
|
intervention ?? new FakeInterventionLocalService(),
|
|
new FakeInterventionContactsService(),
|
|
session,
|
|
parametres,
|
|
new NavigationService(),
|
|
scope ?? new FakeCompteRenduScopeService());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Règle métier : le compte rendu ne se saisit qu'APRÈS le rendez-vous. Un
|
|
/// rendez-vous futur n'ouvre aucune saisie (aucun champ affiché, aucune
|
|
/// création possible) : la page explique la règle et propose le retour.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Load_RendezVousFutur_AucuneSaisiePossible()
|
|
{
|
|
// Arrange : rendez-vous planifié demain.
|
|
DateTime demain = DateTime.Now.AddDays(1);
|
|
var rendezVous = new AgendaItem
|
|
{
|
|
RendezVousGuid = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
|
Titre = "RDV futur",
|
|
Debut = demain,
|
|
Fin = demain.AddHours(1),
|
|
InterventionGuid = Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
|
|
ClientNom = "Client Test"
|
|
};
|
|
var intervention = new FakeInterventionLocalService();
|
|
CompteRenduCreateViewModel vm = CreateViewModel(TypesSignataireParDefaut(), intervention: intervention);
|
|
|
|
// Act
|
|
await vm.LoadCommand.ExecuteAsync(rendezVous);
|
|
|
|
// Assert : formulaire masqué, explication datée.
|
|
Assert.False(vm.SaisieAutorisee);
|
|
Assert.True(vm.SaisieIndisponible);
|
|
Assert.False(vm.FormulaireVisible);
|
|
Assert.Contains("Rendez-vous prévu le", vm.MessageSaisieIndisponible);
|
|
Assert.Null(vm.ErrorMessage);
|
|
|
|
// Act : enregistrement demandé malgré tout (appel indirect de la commande).
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Assert : aucune création n'est transmise au serveur.
|
|
Assert.Null(intervention.LastRequest);
|
|
Assert.False(vm.IsBusy);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Règle métier : un compte rendu documente une intervention (travaux
|
|
/// réalisés, réserves, signature du client). Un rendez-vous lié à AUCUNE
|
|
/// intervention ne peut donc pas en porter un : la page n'ouvre aucune saisie,
|
|
/// explique la règle et ne transmet jamais de création.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Load_SansIntervention_SaisieFermee()
|
|
{
|
|
// Arrange : rendez-vous passé, sans intervention liée.
|
|
DateTime passe = DateTime.Now.AddDays(-1);
|
|
var rendezVous = new AgendaItem
|
|
{
|
|
RendezVousGuid = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
|
Titre = "RDV sans intervention",
|
|
Debut = passe.AddHours(10),
|
|
Fin = passe.AddHours(12),
|
|
ClientNom = "Client Test"
|
|
};
|
|
var intervention = new FakeInterventionLocalService();
|
|
CompteRenduCreateViewModel vm = CreateViewModel(
|
|
TypesSignataireParDefaut(), intervention: intervention);
|
|
|
|
// Act
|
|
await vm.LoadCommand.ExecuteAsync(rendezVous);
|
|
|
|
// Assert : formulaire masqué, motif dédié (ce n'est pas la règle « après le
|
|
// rendez-vous » : le rendez-vous est passé).
|
|
Assert.False(vm.SaisieAutorisee);
|
|
Assert.True(vm.SaisieIndisponible);
|
|
Assert.False(vm.FormulaireVisible);
|
|
Assert.Contains("intervention", vm.ExplicationSaisieIndisponible);
|
|
Assert.Null(vm.MessageSaisieIndisponible);
|
|
|
|
// Act : enregistrement demandé malgré tout (appel indirect de la commande).
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Assert : aucune création n'est transmise au serveur.
|
|
Assert.Null(intervention.LastRequest);
|
|
Assert.False(vm.IsBusy);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contre-épreuve de la règle : un rendez-vous passé ouvre la saisie
|
|
/// normalement (aucune explication, formulaire affiché).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Load_RendezVousPasse_SaisieOuverte()
|
|
{
|
|
CompteRenduCreateViewModel vm = CreateViewModel(TypesSignataireParDefaut());
|
|
|
|
await vm.LoadCommand.ExecuteAsync(CreateRendezVous());
|
|
|
|
Assert.True(vm.SaisieAutorisee);
|
|
Assert.True(vm.FormulaireVisible);
|
|
Assert.False(vm.SaisieIndisponible);
|
|
Assert.Null(vm.MessageSaisieIndisponible);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Échec réseau au chargement de l'écran : l'utilisateur reçoit le libellé français
|
|
/// « hors ligne », et l'événement part en Warning/P2 — aucune entrée en Error/P1,
|
|
/// donc aucun alimentation des groupes d'erreurs du Diagnostic Center.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Load_EchecReseau_EstSignaleEnWarningSansIncident()
|
|
{
|
|
var scope = new FakeCompteRenduScopeService
|
|
{
|
|
ExceptionOnGet = new HttpRequestException("Connection failure")
|
|
};
|
|
CompteRenduCreateViewModel vm = CreateViewModel(TypesSignataireParDefaut(), scope: scope);
|
|
|
|
var telemetry = new CapturingTelemetry();
|
|
ExceptionTelemetry.Initialize(telemetry, null);
|
|
try
|
|
{
|
|
await vm.LoadCommand.ExecuteAsync(CreateRendezVous());
|
|
}
|
|
finally
|
|
{
|
|
ExceptionTelemetry.Initialize(null, null);
|
|
}
|
|
|
|
Assert.Equal("Hors ligne — impossible de contacter le serveur.", vm.ErrorMessage);
|
|
AssertAucunIncident(telemetry);
|
|
DiagnosticEvent evt = EvenementCompteRendu(telemetry);
|
|
Assert.Equal(TelemetryLevel.Warning, evt.Level);
|
|
Assert.Equal("HttpRequestException", evt.ErrorCode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Régression du ticket de production du 09/10 : l'échec réseau de la vérification
|
|
/// d'existence (anti-doublon du bouton Créer) remontait en Error/P1. Il doit
|
|
/// désormais être classé en Warning/P2, à sa vraie gravité.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Save_EchecReseauDeLAntiDoublon_NEstPasUnIncident()
|
|
{
|
|
var scope = new FakeCompteRenduScopeService
|
|
{
|
|
ExceptionOnExists = new HttpRequestException("Connection failure")
|
|
};
|
|
CompteRenduCreateViewModel vm = CreateViewModel(TypesSignataireParDefaut(), scope: scope);
|
|
|
|
await vm.LoadCommand.ExecuteAsync(CreateRendezVous());
|
|
|
|
var telemetry = new CapturingTelemetry();
|
|
ExceptionTelemetry.Initialize(telemetry, null);
|
|
try
|
|
{
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
}
|
|
finally
|
|
{
|
|
ExceptionTelemetry.Initialize(null, null);
|
|
}
|
|
|
|
AssertAucunIncident(telemetry);
|
|
DiagnosticEvent evt = EvenementCompteRendu(telemetry);
|
|
Assert.Equal(TelemetryLevel.Warning, evt.Level);
|
|
Assert.Equal("HttpRequestException", evt.ErrorCode);
|
|
Assert.Equal("Hors ligne — impossible de contacter le serveur.", vm.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contre-épreuve : une erreur métier (hors réseau) reste un incident Error/P1 avec
|
|
/// son message — le classement préalable ne dilue pas les vraies erreurs.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Save_ErreurMetier_ResteUnIncident()
|
|
{
|
|
var scope = new FakeCompteRenduScopeService
|
|
{
|
|
ExceptionOnExists = new InvalidOperationException("boom")
|
|
};
|
|
CompteRenduCreateViewModel vm = CreateViewModel(TypesSignataireParDefaut(), scope: scope);
|
|
|
|
await vm.LoadCommand.ExecuteAsync(CreateRendezVous());
|
|
|
|
var telemetry = new CapturingTelemetry();
|
|
ExceptionTelemetry.Initialize(telemetry, null);
|
|
try
|
|
{
|
|
await vm.SaveCommand.ExecuteAsync(null);
|
|
}
|
|
finally
|
|
{
|
|
ExceptionTelemetry.Initialize(null, null);
|
|
}
|
|
|
|
Assert.Equal("boom", vm.ErrorMessage);
|
|
DiagnosticEvent evt = EvenementCompteRendu(telemetry);
|
|
Assert.Equal(TelemetryLevel.Error, evt.Level);
|
|
Assert.Equal("InvalidOperationException", evt.ErrorCode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seul événement de diagnostic attribué à ce ViewModel : la télémétrie d'usage
|
|
/// (fonctionnalités) est un <see cref="TelemetryEvent"/>, pas un
|
|
/// <see cref="DiagnosticEvent"/>. Le filtre porte sur le composant, la télémétrie
|
|
/// statique globale étant partagée avec les classes exécutées en parallèle.
|
|
/// </summary>
|
|
private static DiagnosticEvent EvenementCompteRendu(CapturingTelemetry telemetry) =>
|
|
Assert.Single(
|
|
telemetry.Events.OfType<DiagnosticEvent>(),
|
|
e => e.Component == nameof(CompteRenduCreateViewModel));
|
|
|
|
/// <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(CompteRenduCreateViewModel) && e.Level == TelemetryLevel.Error);
|
|
|
|
/// <summary>
|
|
/// Capture en mémoire des événements de la télémétrie statique globale
|
|
/// (<see cref="ExceptionTelemetry.Initialize"/>), rétablie à <c>null</c> après
|
|
/// chaque test — la classe appartient à la collection « TelemetrieStatique ».
|
|
/// </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);
|
|
}
|
|
}
|