327 lines
14 KiB
C#
327 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Omika.App.Local.Abstractions;
|
|
using Omika.App.Local.Models;
|
|
using Omika.App.Navigation;
|
|
using Omika.App.ViewModels;
|
|
using Omika.Core.Contracts.Documents;
|
|
using Omika.Core.Contracts.Mobile.Sync;
|
|
using Xunit;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Liste des destinataires proposée à la composition d'un courriel : elle contient
|
|
/// UNIQUEMENT les contacts de l'intervention, puis les contacts du carnet
|
|
/// d'adresses du téléphone. Chaque entrée porte son ORIGINE (« Intervention » /
|
|
/// « Téléphone ») pour qu'un contact personnel ne soit jamais confondu avec un
|
|
/// contact de l'intervention, même à nom identique.
|
|
/// </summary>
|
|
public class NouveauEmailContactsDisponiblesTests
|
|
{
|
|
// =====================================================================
|
|
// Helpers
|
|
// =====================================================================
|
|
|
|
private static NouveauEmailViewModel Creer(
|
|
IInterventionContactsService contacts,
|
|
IContactsTelephoneService? contactsTelephone)
|
|
{
|
|
var viewModel = new NouveauEmailViewModel(
|
|
new FakeInterventionService(),
|
|
contacts,
|
|
new FakeDocumentService(),
|
|
new NavigationService(),
|
|
new FakeSyncEngine(),
|
|
Omika.Core.Observability.NullTelemetryService.Instance,
|
|
emailLauncher: null,
|
|
appRetour: null,
|
|
permissions: null,
|
|
contactsTelephone: contactsTelephone);
|
|
|
|
viewModel.InterventionGuid = Guid.NewGuid();
|
|
|
|
// « Email vierge » : aucun appel au serveur pour l'aperçu du modèle, mais la
|
|
// liste des destinataires est bien construite (comme en composition réelle).
|
|
viewModel.ModeleSelectionne = new ModeleMailDto { Id = string.Empty, Nom = "Email vierge" };
|
|
return viewModel;
|
|
}
|
|
|
|
private static InterventionContact ContactIntervention(string nom, string? email)
|
|
=> new() { ContactGuid = Guid.NewGuid(), DisplayName = nom, Email = email };
|
|
|
|
// =====================================================================
|
|
// Sources et origines
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Les contacts de l'intervention viennent en premier, puis ceux du téléphone,
|
|
/// chacun identifié par son origine.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Liste_InterventionPuisTelephone_AvecOrigineIdentifiee()
|
|
{
|
|
var contacts = new FakeContactsService
|
|
{
|
|
Disponibles =
|
|
[
|
|
ContactIntervention("Alice Dupont", "alice@test.fr"),
|
|
ContactIntervention("Bruno Petit", "bruno@test.fr")
|
|
]
|
|
};
|
|
|
|
var telephone = new FakeContactsTelephoneService
|
|
{
|
|
Contacts =
|
|
[
|
|
new ContactTelephone("Zoé Martin", "zoe@perso.fr"),
|
|
new ContactTelephone("Benoit Roux", "benoit@perso.fr")
|
|
]
|
|
};
|
|
|
|
NouveauEmailViewModel vm = Creer(contacts, telephone);
|
|
await vm.ContinuerCommand.ExecuteAsync(null);
|
|
|
|
// Assert : 4 propositions, intervention d'abord, puis téléphone trié par nom.
|
|
Assert.Equal(4, vm.ContactsDisponibles.Count);
|
|
|
|
Assert.Equal(
|
|
new[] { "Alice Dupont — alice@test.fr", "Bruno Petit — bruno@test.fr", "Benoit Roux — benoit@perso.fr", "Zoé Martin — zoe@perso.fr" },
|
|
vm.ContactsDisponibles.Select(c => c.Libelle));
|
|
|
|
// L'origine est explicite pour chaque entrée.
|
|
Assert.Equal(
|
|
new[] { OrigineContactEmail.Intervention, OrigineContactEmail.Intervention, OrigineContactEmail.Telephone, OrigineContactEmail.Telephone },
|
|
vm.ContactsDisponibles.Select(c => c.Origine));
|
|
|
|
Assert.Equal(
|
|
new[] { "Intervention", "Intervention", "Téléphone", "Téléphone" },
|
|
vm.ContactsDisponibles.Select(c => c.OrigineLibelle));
|
|
|
|
Assert.Equal(new[] { true, true, false, false }, vm.ContactsDisponibles.Select(c => c.EstContactIntervention));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Une adresse déjà proposée par l'intervention n'est pas dupliquée depuis le
|
|
/// carnet du téléphone : l'origine « Intervention » est conservée.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Telephone_MemeAdresse_FusionneeEnFaveurDeLIntervention()
|
|
{
|
|
var contacts = new FakeContactsService
|
|
{
|
|
Disponibles = [ContactIntervention("Alice Dupont", "alice@test.fr")]
|
|
};
|
|
|
|
var telephone = new FakeContactsTelephoneService
|
|
{
|
|
Contacts = [new ContactTelephone("Alice D.", "ALICE@test.fr")]
|
|
};
|
|
|
|
NouveauEmailViewModel vm = Creer(contacts, telephone);
|
|
await vm.ContinuerCommand.ExecuteAsync(null);
|
|
|
|
EmailContactOption option = Assert.Single(vm.ContactsDisponibles);
|
|
Assert.Equal("alice@test.fr", option.Email);
|
|
Assert.Equal(OrigineContactEmail.Intervention, option.Origine);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Une entrée sans adresse email valide n'est jamais proposée, quelle que soit
|
|
/// sa source.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ContactSansAdresseValide_NEstPasPropose()
|
|
{
|
|
var contacts = new FakeContactsService
|
|
{
|
|
Disponibles =
|
|
[
|
|
ContactIntervention("Alice Dupont", "alice@test.fr"),
|
|
ContactIntervention("Sans adresse", null),
|
|
ContactIntervention("Adresse vide", " ")
|
|
]
|
|
};
|
|
|
|
var telephone = new FakeContactsTelephoneService
|
|
{
|
|
Contacts =
|
|
[
|
|
new ContactTelephone("Carnet invalide", "pas-une-adresse"),
|
|
new ContactTelephone("Carnet vide", ""),
|
|
new ContactTelephone("Carnet valide", "carnet@perso.fr")
|
|
]
|
|
};
|
|
|
|
NouveauEmailViewModel vm = Creer(contacts, telephone);
|
|
await vm.ContinuerCommand.ExecuteAsync(null);
|
|
|
|
Assert.Equal(
|
|
new[] { "Alice Dupont — alice@test.fr", "Carnet valide — carnet@perso.fr" },
|
|
vm.ContactsDisponibles.Select(c => c.Libelle));
|
|
}
|
|
|
|
// =====================================================================
|
|
// Sources optionnelles : la composition n'échoue jamais
|
|
// =====================================================================
|
|
|
|
/// <summary>Plateforme sans carnet d'adresses : seuls les contacts de l'intervention sont proposés.</summary>
|
|
[Fact]
|
|
public async Task SansCarnetTelephone_ListeLimiteeALIntervention()
|
|
{
|
|
var contacts = new FakeContactsService
|
|
{
|
|
Disponibles = [ContactIntervention("Alice Dupont", "alice@test.fr")]
|
|
};
|
|
|
|
NouveauEmailViewModel vm = Creer(contacts, contactsTelephone: null);
|
|
await vm.ContinuerCommand.ExecuteAsync(null);
|
|
|
|
EmailContactOption option = Assert.Single(vm.ContactsDisponibles);
|
|
Assert.Equal(OrigineContactEmail.Intervention, option.Origine);
|
|
}
|
|
|
|
/// <summary>Autorisation refusée / lecture du carnet en erreur : la composition reste possible.</summary>
|
|
[Fact]
|
|
public async Task CarnetEnErreur_NAffectePasLaComposition()
|
|
{
|
|
var contacts = new FakeContactsService
|
|
{
|
|
Disponibles = [ContactIntervention("Alice Dupont", "alice@test.fr")]
|
|
};
|
|
|
|
NouveauEmailViewModel vm = Creer(contacts, new FakeContactsTelephoneService { Echoue = true });
|
|
await vm.ContinuerCommand.ExecuteAsync(null);
|
|
|
|
Assert.Single(vm.ContactsDisponibles);
|
|
Assert.Null(vm.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>Liste des contacts de l'intervention indisponible : les contacts du téléphone restent proposés.</summary>
|
|
[Fact]
|
|
public async Task InterventionEnErreur_GardeLesContactsDuTelephone()
|
|
{
|
|
var contacts = new FakeContactsService { Echoue = true };
|
|
var telephone = new FakeContactsTelephoneService
|
|
{
|
|
Contacts = [new ContactTelephone("Zoé Martin", "zoe@perso.fr")]
|
|
};
|
|
|
|
NouveauEmailViewModel vm = Creer(contacts, telephone);
|
|
await vm.ContinuerCommand.ExecuteAsync(null);
|
|
|
|
EmailContactOption option = Assert.Single(vm.ContactsDisponibles);
|
|
Assert.Equal(OrigineContactEmail.Telephone, option.Origine);
|
|
Assert.Equal("Téléphone", option.OrigineLibelle);
|
|
}
|
|
|
|
// =====================================================================
|
|
// Fakes
|
|
// =====================================================================
|
|
|
|
private sealed class FakeContactsService : IInterventionContactsService
|
|
{
|
|
public IReadOnlyList<InterventionContact> Disponibles { get; set; } = [];
|
|
|
|
public bool Echoue { get; set; }
|
|
|
|
public Task<IReadOnlyList<InterventionContact>> GetContactsForInterventionAsync(
|
|
Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult(Disponibles);
|
|
|
|
public Task<InterventionContact?> GetContactByIdAsync(
|
|
Guid contactGuid, CancellationToken ct = default) => Task.FromResult<InterventionContact?>(null);
|
|
|
|
public Task<IReadOnlyList<InterventionContact>> GetContactsDisponiblesAsync(
|
|
Guid interventionGuid, CancellationToken ct = default)
|
|
=> Echoue
|
|
? throw new InvalidOperationException("Contacts de l'intervention indisponibles.")
|
|
: Task.FromResult(Disponibles);
|
|
}
|
|
|
|
private sealed class FakeContactsTelephoneService : IContactsTelephoneService
|
|
{
|
|
public IReadOnlyList<ContactTelephone> Contacts { get; set; } = [];
|
|
|
|
public bool Echoue { get; set; }
|
|
|
|
public Task<IReadOnlyList<ContactTelephone>> GetContactsAvecEmailAsync(CancellationToken cancellationToken = default)
|
|
=> Echoue
|
|
? throw new InvalidOperationException("Carnet d'adresses indisponible (autorisation refusée).")
|
|
: Task.FromResult(Contacts);
|
|
}
|
|
|
|
private sealed class FakeInterventionService : IInterventionLocalService
|
|
{
|
|
public Task<IReadOnlyList<RapportChampDto>> GetMailChampsAsync(
|
|
GetRapportChampsRequest request, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<RapportChampDto>>([]);
|
|
|
|
public Task<EmailModeleApercuDto?> GetEmailModeleApercuAsync(
|
|
CreateEmailModeleRequest request, CancellationToken ct = default)
|
|
=> Task.FromResult<EmailModeleApercuDto?>(null);
|
|
|
|
public Task<IReadOnlyList<SmtpConfigurationDto>> GetSmtpConfigurationsAsync(
|
|
CancellationToken ct = default) => Task.FromResult<IReadOnlyList<SmtpConfigurationDto>>([]);
|
|
|
|
public Task<EmailInterventionDto> AddEmailAsync(
|
|
Guid interventionGuid, CreateEmailInterventionRequest request, CancellationToken ct = default)
|
|
=> Task.FromResult(new EmailInterventionDto { Id = Guid.NewGuid().ToString() });
|
|
|
|
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 FakeDocumentService : IDocumentLocalService
|
|
{
|
|
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<string?>(null);
|
|
|
|
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());
|
|
}
|
|
}
|