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

726 lines
27 KiB
C#

using System.Text.Json;
using Omika.App.Local.Abstractions;
using Omika.App.Local.Models;
using Omika.App.Local.Repositories;
using Omika.App.Local.Services;
using Omika.App.Navigation;
using Omika.App.Services;
using Omika.App.ViewModels;
using Omika.Core.Contracts.Clients;
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Documents;
using Omika.Core.Contracts.Interventions;
using Omika.Core.Contracts.Planning;
using Omika.Core.Contracts.Sites;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Tests du ViewModel Fiche Intervention : chargement, état introuvable,
/// indicateurs de relations et navigation vers un rendez-vous lié.
/// </summary>
public class InterventionDetailViewModelTests : LocalTestBase
{
private readonly InterventionDetailViewModel _viewModel;
private readonly NavigationService _navigation;
private readonly FakePhoneLauncher _phoneLauncher = new();
private readonly FakeEmailLauncher _emailLauncher = new();
private readonly PermissionService _permissions = new();
private sealed class FakeContactsService : 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 FakePhoneLauncher : IPhoneLauncher
{
public string? LastNumber { get; private set; }
public Task<bool> DialAsync(string phoneNumber)
{
LastNumber = phoneNumber;
return Task.FromResult(true);
}
}
private sealed class FakeEmailLauncher : IEmailLauncher
{
public string? LastEmail { get; private set; }
public Task<bool> ComposeAsync(string email)
{
LastEmail = email;
return Task.FromResult(true);
}
}
private sealed class FakeDocumentService : IDocumentLocalService
{
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<InterventionDocumentItem>>([]);
public Task<bool> EnsureDownloadedAsync(Guid documentGuid, CancellationToken cancellationToken = default)
=> Task.FromResult(false);
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken cancellationToken = default)
=> Task.FromResult(false);
public Task<string?> GetCachedFilePathAsync(Guid documentGuid, CancellationToken cancellationToken = default)
=> Task.FromResult<string?>(null);
public Task<IReadOnlyList<DocumentNoteItem>> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<DocumentNoteItem>>([]);
public Task<DocumentNoteItem> AddNoteAsync(Guid documentGuid, string contenu, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<InterventionDocumentItem?> AddPhotoAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
=> Task.FromResult(new AddPhotosResult());
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
public Task<InterventionDocumentItem?> AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken cancellationToken = default)
=> Task.FromResult<InterventionDocumentItem?>(null);
}
private sealed class FakeMapLauncher : IMapLauncher
{
public Task<bool> OpenItineraireAsync(string address) => Task.FromResult(true);
}
public InterventionDetailViewModelTests()
{
IInterventionLocalService interventionService = new InterventionLocalService(
InterventionRepository, ClientRepository, SiteRepository, DossierRepository,
RendezVousRepository, DocumentMetadataRepository, CollaborateurRepository,
OutboxService, ReconciliationService, ApiClient, InterventionCacheRepository, ContextInterventionCacheRepository, Database);
var clientService = new ClientLocalService(
ClientRepository, DossierRepository, ContextInterventionCacheRepository, ClientStateRepository, ApiClient);
_navigation = new NavigationService();
_viewModel = new InterventionDetailViewModel(
interventionService, OutboxRepository, new FakeContactsService(), clientService, new FakeDocumentService(), _phoneLauncher, _emailLauncher, _navigation, _permissions,
new RapportBrouillonRepository(Database), new FakeMapLauncher(), Network);
}
private static LocalClient CreateClient(
Guid id,
string nom,
string? code = null,
string? phone = null,
string? mobile = null,
string? email = null)
{
bool aDesCoordonnees = phone is not null || mobile is not null || email is not null;
var dto = new ClientDetailsDto
{
Id = id.ToString(),
Nom = nom,
Code = code,
Coordonnees = aDesCoordonnees
? new ContactDetailsDto { Phone = phone, Mobile = mobile, Email = email }
: null
};
return new LocalClient
{
Id = id,
PayloadJson = JsonSerializer.Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalIntervention CreateIntervention(Guid id, string reference, Guid? clientId = null)
{
var dto = new InterventionDetailsDto
{
Id = id.ToString(),
Reference = reference,
Titre = "Intervention",
ClientId = clientId?.ToString(),
Etat = new ReferenceValueDto { Label = "En cours" }
};
return new LocalIntervention
{
Id = id,
ClientId = clientId,
PayloadJson = JsonSerializer.Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalSite CreateSite(Guid id, string nom)
{
var dto = new SiteDetailsDto { Id = id.ToString(), Nom = nom };
return new LocalSite
{
Id = id,
PayloadJson = JsonSerializer.Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalRendezVous CreateRendezVous(Guid id, Guid interventionId, string titre)
{
var dto = new RendezVousDetailsDto
{
Id = id.ToString(),
Titre = titre,
Debut = new DateTime(2026, 8, 12, 9, 0, 0),
Fin = new DateTime(2026, 8, 12, 10, 0, 0),
InterventionId = interventionId.ToString(),
Statut = new ReferenceValueDto { Label = "Planifié" }
};
return new LocalRendezVous
{
Id = id,
InterventionId = interventionId,
PayloadJson = JsonSerializer.Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
[Fact]
public async Task Load_InterventionTrouvee_PeutLaFiche()
{
// Arrange
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert
Assert.False(_viewModel.IsLoading);
Assert.Null(_viewModel.ErrorMessage);
Assert.NotNull(_viewModel.Intervention);
Assert.Equal("INT-001", _viewModel.Intervention.Reference);
}
[Fact]
public async Task Load_InterventionIntrouvable_MessageErreur()
{
// Act
await _viewModel.LoadCommand.ExecuteAsync(Guid.NewGuid());
// Assert
Assert.False(_viewModel.IsLoading);
Assert.NotNull(_viewModel.ErrorMessage);
Assert.Null(_viewModel.Intervention);
}
[Fact]
public async Task Load_AvecClient_HasClientVrai()
{
// Arrange
Guid clientId = Guid.NewGuid();
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert
Assert.True(_viewModel.HasClient);
}
[Fact]
public async Task Load_SansClient_HasClientFaux()
{
// Arrange
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert
Assert.False(_viewModel.HasClient);
Assert.False(_viewModel.HasSite);
Assert.False(_viewModel.HasDossier);
Assert.False(_viewModel.HasRendezVous);
Assert.False(_viewModel.HasDocuments);
}
[Fact]
public async Task Load_AvecSuiviPar_ExposeAppelEtEmailPersonnel()
{
// Arrange
Guid interventionId = Guid.NewGuid();
var dto = new InterventionDetailsDto
{
Id = interventionId.ToString(),
Reference = "INT-001",
Titre = "Intervention",
SuiviParNom = "Dupont",
SuiviParMobile = "0612345678",
SuiviParEmail = "dupont@example.com"
};
await InterventionRepository.UpsertAsync(new LocalIntervention
{
Id = interventionId,
PayloadJson = JsonSerializer.Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
});
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert : les boutons sont visibles et déclenchent le composeur système
// (appel vers le mobile + email via la messagerie personnelle).
Assert.True(_viewModel.HasSuiviParPhone);
Assert.True(_viewModel.HasSuiviParEmail);
await _viewModel.CallSuiviParCommand.ExecuteAsync(null);
Assert.Equal("0612345678", _phoneLauncher.LastNumber);
await _viewModel.EmailSuiviParCommand.ExecuteAsync(null);
Assert.Equal("dupont@example.com", _emailLauncher.LastEmail);
}
[Fact]
public async Task Load_AvecRendezVous_HasRendezVousVrai()
{
// Arrange
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await RendezVousRepository.UpsertAsync(CreateRendezVous(Guid.NewGuid(), interventionId, "RDV"));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert
Assert.True(_viewModel.HasRendezVous);
Assert.Single(_viewModel.Intervention!.RendezVous);
}
[Fact]
public async Task OpenRendezVous_NavigueVersDetail()
{
// Arrange
Guid interventionId = Guid.NewGuid();
Guid rdvId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await RendezVousRepository.UpsertAsync(CreateRendezVous(rdvId, interventionId, "RDV"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
_viewModel.OpenRendezVousCommand.Execute(_viewModel.Intervention!.RendezVous[0]);
// Assert : navigation vers le détail du rendez-vous avec le GUID public et le retour vers la fiche Intervention.
Assert.Equal(AppPage.RendezVousDetail, _navigation.CurrentPage);
var nav = Assert.IsType<DetailNavigation>(_navigation.CurrentParameter);
Assert.Equal(rdvId, nav.Id);
Assert.Equal(AppPage.InterventionDetail, nav.ReturnContext.Page);
Assert.Equal(interventionId, nav.ReturnContext.Parameter);
}
[Fact]
public async Task OpenRendezVous_Null_NeNaviguePas()
{
// Act
_viewModel.OpenRendezVousCommand.Execute(null);
// Assert
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
}
[Fact]
public async Task Back_RetourneALAgenda()
{
// Act
_viewModel.BackCommand.Execute(null);
// Assert
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
}
[Fact]
public async Task OpenDocuments_NavigueVersDocuments()
{
// Arrange
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
_viewModel.OpenDocumentsCommand.Execute(null);
// Assert : navigation vers la page « Contenus associés », onglet Documents.
Assert.Equal(AppPage.InterventionResources, _navigation.CurrentPage);
var nav = Assert.IsType<InterventionResourceNavigation>(_navigation.CurrentParameter);
Assert.Equal(interventionId, nav.InterventionGuid);
Assert.Equal(1, nav.TabIndex);
}
[Fact]
public async Task OpenContacts_NavigueVersContacts()
{
// Arrange
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
_viewModel.OpenContactsCommand.Execute(null);
// Assert : navigation vers la page Contacts de l'intervention avec le GUID public.
Assert.Equal(AppPage.InterventionContacts, _navigation.CurrentPage);
Assert.Equal(interventionId, _navigation.CurrentParameter);
}
[Fact]
public async Task OpenContacts_InterventionNull_NeNaviguePas()
{
// Act
_viewModel.OpenContactsCommand.Execute(null);
// Assert
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
}
[Fact]
public async Task Load_EnLigne_CartesCourrielsEtRapportsAccessibles()
{
// Arrange
Network.Online = true;
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert : en ligne, les deux cartes restent utilisables.
Assert.False(_viewModel.IsOffline);
Assert.True(_viewModel.PeutOuvrirCourriels);
Assert.True(_viewModel.PeutOuvrirRapports);
}
[Fact]
public async Task Load_HorsLigne_DesactiveLesCartesCourrielsEtRapports()
{
// Arrange
Network.Online = false;
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert : hors ligne les cartes restent VISIBLES (aucun masquage) mais
// sont désactivées : courriels et rapports passent par le serveur.
Assert.True(_viewModel.IsOffline);
Assert.False(_viewModel.PeutOuvrirCourriels);
Assert.False(_viewModel.PeutOuvrirRapports);
}
/// <summary>
/// Messagerie EXTERNE : l'action « écrire un courriel » à un contact ouvre
/// DIRECTEMENT l'application de messagerie du téléphone, sans passer par le
/// composeur interne (le serveur ne lui expose d'ailleurs aucune configuration
/// SMTP).
/// </summary>
[Fact]
public async Task EmailContact_MessagerieExterne_OuvreLApplicationDeMessagerie()
{
// Arrange : droit d'envoi présent, mais messagerie externe.
_permissions.SetRights(new Dictionary<string, bool>
{
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
});
_permissions.SetMessagerieExterne(true);
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
var contact = new InterventionContact { Email = "contact@exemple.test", DisplayName = "Contact" };
// Act
await _viewModel.EmailContactCommand.ExecuteAsync(contact);
// Assert : composeur externe utilisé, aucune navigation vers l'écran interne.
Assert.Equal("contact@exemple.test", _emailLauncher.LastEmail);
Assert.NotEqual(AppPage.NouveauEmail, _navigation.CurrentPage);
Assert.False(_viewModel.PeutEnvoyerEmail);
Assert.True(_viewModel.PeutEcrireCourriel);
}
[Fact]
public async Task OpenEmails_EnLigne_NavigueVersLesCourriels() {
// Arrange
Network.Online = true;
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.OpenEmailsCommand.ExecuteAsync(null);
// Assert
Assert.Equal(AppPage.InterventionEmails, _navigation.CurrentPage);
Assert.Equal(interventionId, _navigation.CurrentParameter);
}
[Fact]
public async Task OpenEmails_CoupureApresChargement_NeNaviguePas()
{
// Arrange : la fiche est chargée en ligne puis le réseau est perdu.
Network.Online = true;
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
Network.Online = false;
// Act
await _viewModel.OpenEmailsCommand.ExecuteAsync(null);
// Assert : aucun écran inutilisable ouvert, l'état hors ligne est affiché
// (bandeau + cartes désactivées).
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
Assert.True(_viewModel.IsOffline);
Assert.False(_viewModel.PeutOuvrirCourriels);
}
[Fact]
public async Task OpenRapport_EnLigne_NavigueVersLesRapports()
{
// Arrange
Network.Online = true;
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.OpenRapportCommand.ExecuteAsync(null);
// Assert
Assert.Equal(AppPage.RapportChamps, _navigation.CurrentPage);
Assert.Equal(interventionId, _navigation.CurrentParameter);
}
[Fact]
public async Task OpenRapport_CoupureApresChargement_NeNaviguePas()
{
// Arrange
Network.Online = true;
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
Network.Online = false;
// Act
await _viewModel.OpenRapportCommand.ExecuteAsync(null);
// Assert
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
Assert.True(_viewModel.IsOffline);
Assert.False(_viewModel.PeutOuvrirRapports);
}
[Fact]
public async Task Load_AvecClient_AfficheLeBlocClient()
{
// Arrange : client lié présent dans le snapshot local.
Guid clientId = Guid.NewGuid();
Guid interventionId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Boulangerie Martin", "CLI-042", "0240000000"));
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert : le bloc Client est affiché et porte les informations de base.
Assert.True(_viewModel.HasClient);
Assert.NotNull(_viewModel.Client);
Assert.Equal("Boulangerie Martin", _viewModel.ClientNomAffiche);
Assert.Equal("CLI-042", _viewModel.Client.Code);
Assert.True(_viewModel.Client.HasPhone);
}
[Fact]
public async Task Load_SansClient_BlocClientMasque()
{
// Arrange
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert : aucun bloc Client (ni bouton) sans client lié.
Assert.False(_viewModel.HasClient);
Assert.Null(_viewModel.Client);
Assert.Null(_viewModel.ClientNomAffiche);
Assert.False(_viewModel.PeutOuvrirClient);
}
[Fact]
public async Task OpenClient_NavigueVersLaFicheClient()
{
// Arrange
Guid clientId = Guid.NewGuid();
Guid interventionId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Boulangerie Martin"));
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
_viewModel.OpenClientCommand.Execute(null);
// Assert : navigation vers la fiche du client lié (GUID public).
Assert.Equal(AppPage.ClientDetail, _navigation.CurrentPage);
Assert.Equal(clientId, _navigation.CurrentParameter);
}
[Fact]
public async Task OpenClient_SansClient_NeNaviguePas()
{
// Arrange
Guid interventionId = Guid.NewGuid();
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
_viewModel.OpenClientCommand.Execute(null);
// Assert
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
}
[Fact]
public async Task Load_ClientLie_SansDroitVoirClients_BoutonFicheClientMasque()
{
// Arrange : client lié mais aucun droit A.CLIENT.VOIR.
_permissions.SetRights(new Dictionary<string, bool>
{
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
});
Guid clientId = Guid.NewGuid();
Guid interventionId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Boulangerie Martin"));
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
// Act
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Assert : le bloc reste visible (nom du client) mais l'ouverture est masquée,
// comme pour la fiche du lieu.
Assert.True(_viewModel.HasClient);
Assert.False(_viewModel.PeutOuvrirClient);
}
/// <summary>
/// Les boutons de la ligne client se comportent comme ceux d'une ligne de
/// contact : appel vers le mobile (prioritaire sur le fixe), courriel via le
/// composeur interne ou la messagerie du téléphone.
/// </summary>
[Fact]
public async Task CallClient_AppelleLeMobileDuClient()
{
// Arrange : le mobile est prioritaire sur le fixe.
Guid clientId = Guid.NewGuid();
Guid interventionId = Guid.NewGuid();
await ClientRepository.UpsertAsync(
CreateClient(clientId, "Boulangerie Martin", phone: "0240000000", mobile: "0612345678"));
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.CallClientCommand.ExecuteAsync(null);
// Assert
Assert.Equal("0612345678", _phoneLauncher.LastNumber);
}
[Fact]
public async Task CallClient_SansNumero_NAppellePas()
{
// Arrange : client lié mais sans aucun numéro.
Guid clientId = Guid.NewGuid();
Guid interventionId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Boulangerie Martin"));
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.CallClientCommand.ExecuteAsync(null);
// Assert : aucun appel composé et aucune erreur affichée.
Assert.Null(_phoneLauncher.LastNumber);
Assert.Null(_viewModel.ErrorMessage);
}
[Fact]
public async Task EmailClient_MessagerieInterne_OuvreLeComposeurAvecLAdresseDuClient()
{
// Arrange : messagerie interne + droit d'envoi.
_permissions.SetRights(new Dictionary<string, bool>
{
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
});
Guid clientId = Guid.NewGuid();
Guid interventionId = Guid.NewGuid();
await ClientRepository.UpsertAsync(
CreateClient(clientId, "Boulangerie Martin", email: "contact@boulangerie.test"));
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.EmailClientCommand.ExecuteAsync(null);
// Assert : composeur interne pré-rempli avec l'adresse du client.
Assert.Equal(AppPage.NouveauEmail, _navigation.CurrentPage);
var nav = Assert.IsType<NouveauEmailContactNavigation>(_navigation.CurrentParameter);
Assert.Equal(interventionId, nav.InterventionGuid);
Assert.Equal("contact@boulangerie.test", nav.DestinataireEmail);
Assert.Null(_emailLauncher.LastEmail);
}
[Fact]
public async Task EmailClient_MessagerieExterne_OuvreLApplicationDeMessagerie()
{
// Arrange : messagerie EXTERNE (application du téléphone).
_permissions.SetRights(new Dictionary<string, bool>
{
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
});
_permissions.SetMessagerieExterne(true);
Guid clientId = Guid.NewGuid();
Guid interventionId = Guid.NewGuid();
await ClientRepository.UpsertAsync(
CreateClient(clientId, "Boulangerie Martin", email: "contact@boulangerie.test"));
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
// Act
await _viewModel.EmailClientCommand.ExecuteAsync(null);
// Assert : ouverture directe de la messagerie, sans écran interne.
Assert.Equal("contact@boulangerie.test", _emailLauncher.LastEmail);
Assert.NotEqual(AppPage.NouveauEmail, _navigation.CurrentPage);
}
}