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

515 lines
21 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Omika.App.Local.Abstractions;
using Omika.App.Local.Models;
using Omika.App.Navigation;
using Omika.App.Services;
using Omika.App.ViewModels;
using Omika.Core.Contracts.Documents;
using Omika.Core.Contracts.Mobile.Sync;
using Xunit;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Messagerie EXTERNE dans la création d'un courriel : « Envoyer » ouvre la
/// messagerie du téléphone avec le courriel pré-rempli (aucun envoi par Omika),
/// puis la réponse de l'utilisateur détermine l'état enregistré :
/// <list type="bullet">
/// <item>« Oui » → courriel ENVOYÉ (<c>ME_EnvoyerMob</c> côté serveur) ;</item>
/// <item>« Non » (ou retour sans confirmation) → BROUILLON.</item>
/// </list>
/// La messagerie Back-Office (interne) n'est pas affectée : l'envoi SMTP par le
/// serveur reste le seul chemin.
/// </summary>
public class NouveauEmailExterneTests
{
// =====================================================================
// Construction
// =====================================================================
private static NouveauEmailViewModel Creer(
FakeInterventionService intervention,
FakeEmailLauncher launcher,
IAppRetourService? retour,
FakePermissionService permissions,
FakeDocumentService? documents = null)
{
var viewModel = new NouveauEmailViewModel(
intervention,
new FakeContactsService(),
documents ?? new FakeDocumentService(),
new NavigationService(),
new FakeSyncEngine(),
Omika.Core.Observability.NullTelemetryService.Instance,
launcher,
retour,
permissions);
viewModel.InterventionGuid = Guid.NewGuid();
viewModel.Sujet = "Intervention du 12";
viewModel.Corps = "<p>Bonjour,</p><p>Compte rendu ci-joint.</p>";
viewModel.Destinataires = "client@exemple.fr";
viewModel.Cc = "copie@exemple.fr";
return viewModel;
}
private static (NouveauEmailViewModel Vm, FakeInterventionService Intervention, FakeEmailLauncher Launcher) CreerExterne(
IAppRetourService? retour = null,
FakeDocumentService? documents = null)
{
var intervention = new FakeInterventionService();
var launcher = new FakeEmailLauncher();
NouveauEmailViewModel viewModel = Creer(
intervention,
launcher,
retour ?? new FakeAppRetourService(),
new FakePermissionService { MessagerieExterneValue = true },
documents);
return (viewModel, intervention, launcher);
}
// =====================================================================
// Messagerie interne : rien ne change
// =====================================================================
/// <summary>Back-Office : l'envoi passe par le SERVEUR (SMTP), aucune messagerie externe.</summary>
[Fact]
public async Task Envoyer_BackOffice_EnvoieParLeServeurSansMessagerieExterne()
{
var intervention = new FakeInterventionService();
var launcher = new FakeEmailLauncher();
NouveauEmailViewModel vm = Creer(
intervention,
launcher,
new FakeAppRetourService(),
new FakePermissionService { MessagerieExterneValue = false });
vm.ConfigurationSmtpSelectionnee = new SmtpConfigurationDto { Id = "cfg-1", Email = "moi@exemple.fr" };
await vm.EnvoyerCommand.ExecuteAsync(null);
Assert.Equal(1, intervention.EnvoisInternes);
Assert.Equal(0, launcher.Appels);
Assert.False(vm.AfficherQuestionEnvoiExterne);
}
/// <summary>Back-Office : la configuration SMTP reste proposée (comportement inchangé).</summary>
[Fact]
public void BackOffice_AfficheLaConfigurationSmtp()
{
var intervention = new FakeInterventionService();
NouveauEmailViewModel vm = Creer(
intervention,
new FakeEmailLauncher(),
new FakeAppRetourService(),
new FakePermissionService { MessagerieExterneValue = false });
Assert.True(vm.AfficherConfigurationSmtp);
Assert.False(vm.MessagerieExterne);
}
// =====================================================================
// Messagerie externe : ouverture + question
// =====================================================================
/// <summary>
/// Externe : « Envoyer » ouvre la messagerie avec le courriel pré-rempli,
/// attend le retour de l'utilisateur puis pose la question. Rien n'est encore
/// enregistré à ce stade.
/// </summary>
[Fact]
public async Task Envoyer_Externe_OuvreLaMessageriePuisDemandeConfirmation()
{
(NouveauEmailViewModel vm, FakeInterventionService intervention, FakeEmailLauncher launcher) = CreerExterne();
await vm.EnvoyerCommand.ExecuteAsync(null);
Assert.Equal(1, launcher.Appels);
Assert.NotNull(launcher.DerniereDemande);
Assert.Equal(new[] { "client@exemple.fr" }, launcher.DerniereDemande!.Destinataires);
Assert.Equal(new[] { "copie@exemple.fr" }, launcher.DerniereDemande.Cc);
Assert.Equal("Intervention du 12", launcher.DerniereDemande.Sujet);
// Le client email ne rend pas le HTML : le corps transmis est du TEXTE.
Assert.DoesNotContain('<', launcher.DerniereDemande.CorpsTexte);
Assert.Contains("Compte rendu ci-joint.", launcher.DerniereDemande.CorpsTexte);
Assert.True(vm.AfficherQuestionEnvoiExterne);
Assert.False(vm.AfficherConfigurationSmtp);
Assert.Equal(0, intervention.EnvoisInternes);
Assert.Equal(0, intervention.EnregistrementsEnvoyes);
Assert.Equal(0, intervention.EnregistrementsBrouillon);
}
/// <summary>« Oui » : le courriel est enregistré ENVOYÉ (ME_EnvoyerMob = vrai côté serveur).</summary>
[Fact]
public async Task RepondreOui_EnregistreLeCourrielEnvoye()
{
(NouveauEmailViewModel vm, FakeInterventionService intervention, _) = CreerExterne();
await vm.EnvoyerCommand.ExecuteAsync(null);
await vm.RepondreEnvoiExterneOuiCommand.ExecuteAsync(null);
Assert.Equal(1, intervention.EnregistrementsEnvoyes);
Assert.Equal(0, intervention.EnregistrementsBrouillon);
Assert.Equal(0, intervention.EnvoisInternes);
Assert.False(vm.AfficherQuestionEnvoiExterne);
CreateEmailInterventionRequest request = Assert.Single(intervention.DemandesEnvoyees);
Assert.Equal("Intervention du 12", request.Objet);
// Le corps enregistré reste le HTML d'origine (seul l'aperçu est aplati).
Assert.Equal(vm.Corps, request.Message);
}
/// <summary>« Non » : le courriel est enregistré en BROUILLON, jamais comme envoyé.</summary>
[Fact]
public async Task RepondreNon_EnregistreUnBrouillon()
{
(NouveauEmailViewModel vm, FakeInterventionService intervention, _) = CreerExterne();
await vm.EnvoyerCommand.ExecuteAsync(null);
await vm.RepondreEnvoiExterneNonCommand.ExecuteAsync(null);
Assert.Equal(0, intervention.EnregistrementsEnvoyes);
Assert.Equal(1, intervention.EnregistrementsBrouillon);
Assert.False(vm.AfficherQuestionEnvoiExterne);
}
/// <summary>
/// Aucune application de messagerie : un message clair, et surtout RIEN n'est
/// enregistré (ni envoyé, ni brouillon).
/// </summary>
[Fact]
public async Task Envoyer_Externe_SansMessagerieDisponible_RienNEstEnregistre()
{
var intervention = new FakeInterventionService();
var launcher = new FakeEmailLauncher { Resultat = EmailExterneResultat.Echec };
NouveauEmailViewModel vm = Creer(
intervention,
launcher,
new FakeAppRetourService(),
new FakePermissionService { MessagerieExterneValue = true });
await vm.EnvoyerCommand.ExecuteAsync(null);
Assert.NotNull(vm.ErrorMessage);
Assert.False(vm.AfficherQuestionEnvoiExterne);
Assert.Equal(0, intervention.EnregistrementsEnvoyes);
Assert.Equal(0, intervention.EnregistrementsBrouillon);
}
/// <summary>
/// Composeur système qui CONNAÎT le résultat (iOS avec pièces jointes) :
/// envoyé → enregistrement direct, sans question ; annulé → brouillon.
/// </summary>
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ComposeurSysteme_ResultatConnu_EnregistreSansQuestion(bool envoye)
{
var intervention = new FakeInterventionService();
var launcher = new FakeEmailLauncher { Resultat = EmailExterneResultat.Confirme(envoye) };
NouveauEmailViewModel vm = Creer(
intervention,
launcher,
new FakeAppRetourService(),
new FakePermissionService { MessagerieExterneValue = true });
await vm.EnvoyerCommand.ExecuteAsync(null);
Assert.False(vm.AfficherQuestionEnvoiExterne);
Assert.Equal(envoye ? 1 : 0, intervention.EnregistrementsEnvoyes);
Assert.Equal(envoye ? 0 : 1, intervention.EnregistrementsBrouillon);
}
/// <summary>
/// Pièces jointes cochées : leurs chemins LOCAUX (téléchargés au besoin) sont
/// transmis à la messagerie ; un document introuvable est simplement omis.
/// </summary>
[Fact]
public async Task Envoyer_Externe_TransmetLesCheminsDesPiecesJointes()
{
string chemin = Path.Combine(Path.GetTempPath(), $"omika-pj-{Guid.NewGuid():N}.pdf");
await File.WriteAllTextAsync(chemin, "pdf");
try
{
var documents = new FakeDocumentService { CheminLocal = chemin };
(NouveauEmailViewModel vm, _, FakeEmailLauncher launcher) = CreerExterne(documents: documents);
Guid documentGuid = Guid.NewGuid();
vm.PiecesJointes.Add(new PieceJointeOption(documentGuid.ToString(), "Rapport", estPhoto: false)
{
EstSelectionne = true
});
await vm.EnvoyerCommand.ExecuteAsync(null);
Assert.NotNull(launcher.DerniereDemande);
Assert.Equal(new[] { chemin }, launcher.DerniereDemande!.CheminsPiecesJointes);
}
finally
{
File.Delete(chemin);
}
}
/// <summary>
/// Sans moyen d'observer le retour (plateforme de test), la question est posée
/// immédiatement : aucun enregistrement silencieux dans un état incertain.
/// </summary>
[Fact]
public async Task Envoyer_Externe_SansObservationDuRetour_PoseLaQuestion()
{
var intervention = new FakeInterventionService();
var launcher = new FakeEmailLauncher();
NouveauEmailViewModel vm = Creer(
intervention,
launcher,
retour: null,
new FakePermissionService { MessagerieExterneValue = true });
await vm.EnvoyerCommand.ExecuteAsync(null);
Assert.True(vm.AfficherQuestionEnvoiExterne);
Assert.Equal(0, intervention.EnregistrementsEnvoyes);
Assert.Equal(0, intervention.EnregistrementsBrouillon);
}
/// <summary>Édition d'un brouillon existant : « Oui » met CE brouillon à jour en envoyé.</summary>
[Fact]
public async Task Edition_Externe_RepondreOui_MarqueLeBrouillonEnvoye()
{
var intervention = new FakeInterventionService();
var launcher = new FakeEmailLauncher();
NouveauEmailViewModel vm = Creer(
intervention,
launcher,
new FakeAppRetourService(),
new FakePermissionService { MessagerieExterneValue = true });
var brouillon = new EmailInterventionDto
{
Id = "email-42",
Objet = "Objet",
Message = "Corps",
Brouillon = true
};
await vm.LoadEditionCommand.ExecuteAsync(
new EmailEditionNavigation(vm.InterventionGuid, brouillon));
vm.Sujet = "Objet modifié";
vm.Corps = "Corps modifié";
vm.Destinataires = "client@exemple.fr";
await vm.EnvoyerCommand.ExecuteAsync(null);
await vm.RepondreEnvoiExterneOuiCommand.ExecuteAsync(null);
Assert.Equal(1, intervention.EnregistrementsEnvoyes);
Assert.Equal(0, intervention.EnregistrementsBrouillon);
Assert.Equal("email-42", intervention.DernierEmailId);
}
// =====================================================================
// Fakes
// =====================================================================
private sealed class FakeEmailLauncher : IEmailLauncher
{
public int Appels { get; private set; }
public EmailExterneDemande? DerniereDemande { get; private set; }
public EmailExterneResultat Resultat { get; set; } = EmailExterneResultat.LanceSansConfirmation;
public Task<bool> ComposeAsync(string email) => Task.FromResult(true);
public Task<EmailExterneResultat> ComposerEmailExterneAsync(EmailExterneDemande demande)
{
Appels++;
DerniereDemande = demande;
return Task.FromResult(Resultat);
}
}
/// <summary>Retour au premier plan simulé : le retour est immédiat.</summary>
private sealed class FakeAppRetourService : IAppRetourService
{
public IAppRetourAttente Armer() => new RetourImmediat();
private sealed class RetourImmediat : IAppRetourAttente
{
public Task AttendreAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public void Dispose()
{
}
}
}
private sealed class FakePermissionService : IPermissionService
{
public bool MessagerieExterneValue { get; init; }
public IReadOnlyDictionary<string, bool> Rights => new Dictionary<string, bool>();
public void SetRights(IReadOnlyDictionary<string, bool> rights) { }
public bool HasRight(string key) => true;
public bool MessagerieExterne => MessagerieExterneValue;
public void SetMessagerieExterne(bool externe) { }
public bool PeutRechercherInterventions => true;
public bool VoirRdvSansRestriction => true;
public bool PeutVoirAgendaEquipe => true;
public bool PeutEnvoyerEmail => !MessagerieExterneValue;
public bool PeutComposerEmail => true;
public bool PeutCreerRapport => true;
public bool PeutGererLicence => true;
public bool PeutSaisirTempsAutrui => true;
public bool PeutVoirNotesAdmin => true;
public bool PeutVoirClients => true;
public bool PeutVoirSites => true;
public bool PeutVoirDossiers => true;
}
private sealed class FakeInterventionService : IInterventionLocalService
{
public int EnvoisInternes { get; private set; }
public int EnregistrementsEnvoyes { get; private set; }
public int EnregistrementsBrouillon { get; private set; }
/// <summary>Identifiant du dernier courriel touché (édition).</summary>
public string? DernierEmailId { get; private set; }
public List<CreateEmailInterventionRequest> DemandesEnvoyees { get; } = [];
public Task<EmailInterventionDto> SendEmailAsync(
Guid interventionGuid,
string? emailId,
CreateEmailInterventionRequest request,
CancellationToken cancellationToken = default)
{
EnvoisInternes++;
return Task.FromResult(new EmailInterventionDto { Id = Guid.NewGuid().ToString() });
}
public Task<EmailInterventionDto> AddEmailAsync(
Guid interventionGuid,
CreateEmailInterventionRequest request,
CancellationToken cancellationToken = default)
{
EnregistrementsBrouillon++;
return Task.FromResult(new EmailInterventionDto { Id = Guid.NewGuid().ToString(), Brouillon = true });
}
public Task<EmailInterventionDto> UpdateEmailAsync(
Guid interventionGuid,
string emailId,
CreateEmailInterventionRequest request,
CancellationToken cancellationToken = default)
{
EnregistrementsBrouillon++;
return Task.FromResult(new EmailInterventionDto { Id = emailId, Brouillon = true });
}
public Task<EmailInterventionDto> AddEmailEnvoyeExterneAsync(
Guid interventionGuid,
CreateEmailInterventionRequest request,
CancellationToken cancellationToken = default)
{
EnregistrementsEnvoyes++;
DemandesEnvoyees.Add(request);
return Task.FromResult(new EmailInterventionDto { Id = Guid.NewGuid().ToString() });
}
public Task<EmailInterventionDto> UpdateEmailEnvoyeExterneAsync(
Guid interventionGuid,
string emailId,
CreateEmailInterventionRequest request,
CancellationToken cancellationToken = default)
{
EnregistrementsEnvoyes++;
DernierEmailId = emailId;
DemandesEnvoyees.Add(request);
return Task.FromResult(new EmailInterventionDto { Id = emailId });
}
public Task<Guid> CreateAsync(InterventionSyncPayload payload, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Guid> UpdateAsync(Guid interventionGuid, InterventionSyncPayload payload, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Guid> ArchiveAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDetail?> GetInterventionByIdAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<InterventionDetail?>(null);
public Task<IReadOnlyList<InterventionListItem>> GetInterventionsAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<InterventionListItem>>([]);
}
private sealed class FakeContactsService : IInterventionContactsService
{
public Task<IReadOnlyList<InterventionContact>> GetContactsForInterventionAsync(
Guid interventionGuid, CancellationToken ct = default) => Task.FromResult<IReadOnlyList<InterventionContact>>([]);
public Task<InterventionContact?> GetContactByIdAsync(
Guid contactGuid, CancellationToken ct = default) => Task.FromResult<InterventionContact?>(null);
public Task<IReadOnlyList<InterventionContact>> GetContactsDisponiblesAsync(
Guid interventionGuid, CancellationToken ct = default) => Task.FromResult<IReadOnlyList<InterventionContact>>([]);
}
private sealed class FakeDocumentService : IDocumentLocalService
{
public string? CheminLocal { get; init; }
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(
Guid interventionGuid, CancellationToken ct = default) => Task.FromResult<IReadOnlyList<InterventionDocumentItem>>([]);
public Task<bool> EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default) => Task.FromResult(true);
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken ct = default) => Task.FromResult(true);
public Task<string?> GetCachedFilePathAsync(Guid documentGuid, CancellationToken ct = default)
=> Task.FromResult(CheminLocal);
public Task<IReadOnlyList<DocumentNoteItem>> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DocumentNoteItem>>([]);
public Task<DocumentNoteItem> AddNoteAsync(Guid documentGuid, string contenu, CancellationToken ct = default)
=> Task.FromResult(new DocumentNoteItem());
public Task<InterventionDocumentItem?> AddPhotoAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult(new AddPhotosResult());
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<InterventionDocumentItem?> AddAnnotatedPhotoAsync(
Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
}
private sealed class FakeSyncEngine : ISyncEngine
{
public Task<SyncCycleResult> SyncAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new SyncCycleResult());
public Task<SyncCycleResult> PushAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new SyncCycleResult());
}
}