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

1576 lines
65 KiB
C#

using Omika.Core.Compatibility;
using Omika.Core.Contracts.Clients;
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Configuration;
using Omika.Core.Contracts.Documents;
using Omika.Core.Contracts.Interventions;
using Omika.Core.Contracts.Mobile.Documents;
using Omika.Core.Contracts.Mobile.Snapshot;
using Omika.Core.Contracts.Organisation;
using Omika.Core.Contracts.Planning;
using Omika.Core.Contracts.Mobile.Sync;
using Microsoft.Extensions.Logging.Abstractions;
using Omika.Core.Contracts.Sites;
using Omika.Core.Interfaces;
using Omika.Core.Interfaces.Mobile;
using Omika.Core.Services.Mobile;
using Omika.Core.Domain;
namespace Omika.Tests.Mobile;
/// <summary>
/// Tests du service de snapshot mobile.
/// </summary>
public class MobileSnapshotServiceTests
{
private static MobileSnapshotService CreateService(
IClientService? clients = null,
ICollaborateurService? collaborateurs = null,
ISiteService? sites = null,
IInterventionService? interventions = null,
IDossierService? dossiers = null,
IRendezVousService? rendezVous = null,
IMobileDocumentService? documents = null,
ISuiviTempsService? suivisTemps = null,
IParametreMetierRepository? parametres = null)
{
return new MobileSnapshotService(
clients ?? new FakeClientService(),
collaborateurs ?? new FakeCollaborateurService(),
sites ?? new FakeSiteService(),
interventions ?? new FakeInterventionService(),
dossiers ?? new FakeDossierService(),
rendezVous ?? new FakeRendezVousService(),
documents ?? new FakeMobileDocumentService(),
suivisTemps ?? new FakeSuiviTempsService(),
parametres ?? new FakeParametreMetierRepository(),
new FakeIdentityResolver(),
NullLogger<MobileSnapshotService>.Instance);
}
[Fact]
public async Task Snapshot_Returns_Complete_Contract()
{
// Arrange
var service = CreateService();
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1");
// Assert : données obligatoires présentes.
Assert.NotEqual(default, result.GeneratedAtUtc);
Assert.NotNull(result.Clients);
Assert.NotNull(result.Personnes);
Assert.NotNull(result.Sites);
Assert.NotNull(result.Documents);
Assert.NotNull(result.Interventions);
Assert.NotNull(result.Dossiers);
Assert.NotNull(result.RendezVous);
Assert.NotNull(result.Collaborateurs);
}
[Fact]
public async Task Snapshot_Contains_No_Document_Buffer()
{
// Arrange
var service = CreateService();
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1");
// Assert : le snapshot ne contient que des métadonnées de documents.
// DocumentSummaryDto ne porte aucun contenu binaire (pas de buffer).
Assert.All(result.Documents, d => Assert.Equal(0, d.TailleOctets));
}
[Fact]
public async Task Snapshot_Is_Identical_For_Batik_And_Omika()
{
// Arrange
var service = CreateService();
// Act
var batik = await service.GetSnapshotAsync("user-1", "device-1");
var omika = await service.GetSnapshotAsync("user-1", "device-1");
// Assert : le contrat public est identique quelle que soit la génération.
Assert.IsType<MobileSnapshotResponse>(batik);
Assert.IsType<MobileSnapshotResponse>(omika);
Assert.Equal(batik.Clients.Count, omika.Clients.Count);
Assert.Equal(batik.Documents.Count, omika.Documents.Count);
}
[Fact]
public async Task Snapshot_Respects_CancellationToken()
{
// Arrange
var service = CreateService();
using var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert : l'annulation est propagée.
await Assert.ThrowsAnyAsync<OperationCanceledException>(
() => service.GetSnapshotAsync("user-1", "device-1", null, cancellationToken: cts.Token));
}
[Fact]
public async Task Snapshot_Empty_UserId_Throws()
{
// Arrange
var service = CreateService();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => service.GetSnapshotAsync("", "device-1"));
}
[Fact]
public async Task Snapshot_Empty_DeviceId_Throws()
{
// Arrange
var service = CreateService();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => service.GetSnapshotAsync("user-1", ""));
}
[Fact]
public async Task Snapshot_No_SuivisTemps_Returns_Empty_Collection()
{
// Arrange : aucun SuiviTemps (fake par défaut vide).
var service = CreateService();
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1");
// Assert : collection vide (pas null) et autres sections inchangées.
Assert.NotNull(result.SuivisTemps);
Assert.Empty(result.SuivisTemps);
Assert.NotNull(result.Clients);
Assert.NotNull(result.Interventions);
Assert.NotNull(result.RendezVous);
Assert.NotNull(result.Collaborateurs);
}
[Fact]
public async Task Snapshot_Does_Not_Load_SuivisTemps()
{
// Le snapshot ne charge plus les saisies de temps en bloc : elles sont
// récupérées à la demande via GET /api/suivis-temps.
var fake = new FakeSuiviTempsService(new[]
{
new SuiviTempsDto { Id = Guid.NewGuid().ToString("D"), DureeMinutes = 45 }
});
var service = CreateService(suivisTemps: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.SuivisTemps);
}
[Fact]
public async Task Snapshot_SuiviTemps_Not_Exposed()
{
// Les saisies de temps ne transitent plus par le snapshot : identité
// physique ou synthétique, aucune n'est embarquée.
var fake = new FakeSuiviTempsService(new[]
{
new SuiviTempsDto { Id = Guid.NewGuid().ToString("D") }
});
var service = CreateService(suivisTemps: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.SuivisTemps);
}
[Fact]
public async Task Snapshot_SuiviTemps_Exposes_No_HfSql_Columns()
{
// Arrange
var fake = new FakeSuiviTempsService(new[]
{
new SuiviTempsDto
{
Id = Guid.NewGuid().ToString("D"),
CollaborateurId = "aaaaaaaa-0000-0000-0000-000000000001",
InterventionId = "aaaaaaaa-0000-0000-0000-000000000002",
RendezVousId = "aaaaaaaa-0000-0000-0000-000000000003",
Date = DateTime.UtcNow,
DureeMinutes = 30,
TypeTemps = new ReferenceValueDto { Key = "SAISIE", Label = "Saisie" },
Description = "Test",
Valide = false
}
});
var service = CreateService(suivisTemps: fake);
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1");
string json = System.Text.Json.JsonSerializer.Serialize(result);
// Assert : aucune colonne physique HFSQL n'est exposée dans le contrat public.
Assert.DoesNotContain("SDT_PKID", json);
Assert.DoesNotContain("SDT_IDCollaborateur", json);
Assert.DoesNotContain("SDT_IDIntervention", json);
Assert.DoesNotContain("SDT_IDRdv", json);
Assert.DoesNotContain("SDT_NbMinutes", json);
}
[Fact]
public async Task Snapshot_Does_Not_Load_Clients()
{
// Le snapshot ne charge plus les clients en bloc : ils sont récupérés à la
// demande (liste via GET /clients, détail via GET /clients/{id}).
var fake = new FakeClientService(
details:
[
new ClientDetailsDto { Id = Guid.NewGuid().ToString("D"), Nom = "Client X" }
]);
var service = CreateService(clients: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Clients);
}
[Fact]
public async Task Snapshot_Does_Not_Load_Sites()
{
// Le snapshot ne charge plus les sites en bloc : ils sont récupérés à la
// demande (liste via GET /sites, détail via GET /sites/{id}).
var fake = new FakeSiteService(
details:
[
new SiteDetailsDto { Id = Guid.NewGuid().ToString("D"), Nom = "Site X" }
]);
var service = CreateService(sites: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Sites);
}
[Fact]
public async Task Snapshot_Does_Not_Load_Site_Details()
{
// Les détails de sites ne sont plus embarqués dans le snapshot.
var fake = new FakeSiteService(
details:
[
new SiteDetailsDto { Id = Guid.NewGuid().ToString("D"), Nom = "Site 42" }
]);
var service = CreateService(sites: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Sites);
}
[Fact]
public async Task Snapshot_RendezVous_Does_Not_Call_GetById_Per_RendezVous()
{
// Arrange : 100 summaries. Le snapshot doit charger les rendez-vous
// COMPLETS via SearchDetailsAsync (bulk) et ne JAMAIS appeler
// GetByIdAsync par rendez-vous. C'est le test anti-N+1 : il échoue avec
// l'ancien pattern (SearchAsync + GetByIdAsync par summary).
const int count = 100;
const string collaborateurId = "aaaaaaaa-0000-0000-0000-0000000000ff";
var summaries = Enumerable.Range(1, count)
.Select(i => new RendezVousSummaryDto
{
Id = Guid.NewGuid().ToString("D"),
SourceReference = LegacyEntityReference.FromLegacyId(i)
})
.ToList();
var details = summaries
.Select(s => new RendezVousDetailsDto
{
Id = s.Id,
SourceReference = s.SourceReference,
TypeActivite = MobileEnabledTypeRdv(),
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = collaborateurId,
PeriodeDebut = DateTime.Today.AddHours(9),
PeriodeFin = DateTime.Today.AddHours(10)
}
]
})
.ToList();
var getByIdCalls = 0;
var fake = new FakeRendezVousService(
summaries,
details,
id =>
{
getByIdCalls++;
return null;
});
var service = CreateService(rendezVous: fake);
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", collaborateurId);
// Assert : tous les rendez-vous sont présents (chargés en bulk) et aucun
// GetByIdAsync n'a été appelé. Le N+1 est éliminé.
Assert.Equal(count, result.RendezVous.Count);
Assert.Equal(0, getByIdCalls);
}
[Fact]
public async Task Snapshot_Does_Not_Load_Collaborateurs()
{
// Le snapshot ne charge plus les collaborateurs en bloc : ils sont
// récupérés à la demande.
var fake = new FakeCollaborateurService(
details:
[
new CollaborateurDetailsDto { Id = Guid.NewGuid().ToString("D"), Nom = "Doe" }
]);
var service = CreateService(collaborateurs: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Collaborateurs);
}
[Fact]
public async Task Snapshot_Does_Not_Load_Collaborateur_Details()
{
// Les détails de collaborateurs ne sont plus embarqués dans le snapshot.
var fake = new FakeCollaborateurService(
details:
[
new CollaborateurDetailsDto { Id = Guid.NewGuid().ToString("D"), Nom = "Doe" }
]);
var service = CreateService(collaborateurs: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Collaborateurs);
}
[Fact]
public async Task Snapshot_Does_Not_Load_Dossiers()
{
// Le snapshot ne charge plus les dossiers en bloc : ils sont récupérés à
// la demande (recherche via GET /dossiers, détail via GET /dossiers/{id}).
var fake = new FakeDossierService(
details:
[
new DossierDetailsDto { Id = Guid.NewGuid().ToString("D"), Titre = "Dossier X" }
]);
var service = CreateService(dossiers: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Dossiers);
}
[Fact]
public async Task Snapshot_Does_Not_Load_Dossier_Details()
{
// Les détails de dossiers ne sont plus embarqués dans le snapshot.
var fake = new FakeDossierService(
details:
[
new DossierDetailsDto { Id = Guid.NewGuid().ToString("D"), Titre = "Dossier 42" }
]);
var service = CreateService(dossiers: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Dossiers);
}
[Fact]
public async Task Snapshot_Interventions_Empty_NoGlobalLoad()
{
// Règle « strictement à la demande » : sans rendez-vous, le snapshot ne
// charge aucune intervention (aucun SearchDetailsAsync, aucune collection
// globale). Les interventions ne sont chargées que si des rendez-vous les
// référencent (voir Snapshot_Loads_Interventions_For_Agenda_RendezVous).
var fake = new FakeInterventionService(details:
[
new InterventionDetailsDto { Id = Guid.NewGuid().ToString("D"), Titre = "X" }
]);
var service = CreateService(interventions: fake);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Interventions);
Assert.Equal(0, fake.SearchDetailsCallCount);
}
[Fact]
public async Task Snapshot_Loads_Interventions_For_Agenda_RendezVous()
{
// Arrange : un rendez-vous lié à une intervention (PK numérique + GUID
// physique Omika, comme le mapper OmikaRendezVousMapper les produit).
const long interventionPk = 123L;
Guid interventionGuid = Guid.Parse("22222222-1111-0000-0000-000000000001");
const string collaborateurId = "aaaaaaaa-0000-0000-0000-0000000000ff";
var details = new[]
{
new RendezVousDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Titre = "RDV intervention",
TypeActivite = MobileEnabledTypeRdv(),
InterventionSourceReference = new LegacyEntityReference(
DatabaseSchemaVersion.Omika, NumericId: interventionPk, EntityGuid: interventionGuid),
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = collaborateurId,
PeriodeDebut = DateTime.Today.AddHours(9),
PeriodeFin = DateTime.Today.AddHours(10)
}
]
}
};
var interventionFake = new FakeInterventionService(details:
[
new InterventionDetailsDto
{
Id = interventionGuid.ToString("D"),
SourceReference = new LegacyEntityReference(
DatabaseSchemaVersion.Omika, NumericId: interventionPk, EntityGuid: interventionGuid),
Reference = "INT-001",
NumeroDevis = "DEV-42",
ReferenceExterne = "EXT-7"
}
]);
var service = CreateService(
rendezVous: new FakeRendezVousService(details: details),
interventions: interventionFake);
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", collaborateurId);
// Assert : l'intervention liée est chargée en bulk ciblé (IN_PKID IN (...)),
// jamais via un chargement global ni un GetById par intervention.
var intervention = Assert.Single(result.Interventions);
Assert.Equal(interventionGuid.ToString("D"), intervention.Id);
Assert.Equal("INT-001", intervention.Reference);
Assert.Equal("DEV-42", intervention.NumeroDevis);
Assert.Equal("EXT-7", intervention.ReferenceExterne);
Assert.Equal(1, interventionFake.SearchDetailsCallCount);
Assert.NotNull(interventionFake.LastSearchRequest);
Assert.NotNull(interventionFake.LastSearchRequest.Ids);
Assert.Contains(interventionFake.LastSearchRequest.Ids, id => id == "123");
}
[Fact]
public async Task Snapshot_Loads_Notes_Emails_Documents_For_Agenda_Interventions()
{
// Arrange : un rendez-vous lié à une intervention (PK numérique + GUID
// physique). Le snapshot doit charger en bulk les notes (internes +
// administratives), les courriels et les métadonnées de documents.
const long interventionPk = 123L;
Guid interventionGuid = Guid.Parse("22222222-1111-0000-0000-000000000001");
const string collaborateurId = "aaaaaaaa-0000-0000-0000-0000000000ff";
var details = new[]
{
new RendezVousDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Titre = "RDV intervention",
TypeActivite = MobileEnabledTypeRdv(),
InterventionSourceReference = new LegacyEntityReference(
DatabaseSchemaVersion.Omika, NumericId: interventionPk, EntityGuid: interventionGuid),
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = collaborateurId,
PeriodeDebut = DateTime.Today.AddHours(9),
PeriodeFin = DateTime.Today.AddHours(10)
}
]
}
};
var interventionFake = new FakeInterventionService(
details:
[
new InterventionDetailsDto
{
Id = interventionGuid.ToString("D"),
SourceReference = new LegacyEntityReference(
DatabaseSchemaVersion.Omika, NumericId: interventionPk, EntityGuid: interventionGuid),
Reference = "INT-001"
}
],
bulkNotes:
[
new InterventionNoteBulkRow
{
InterventionNumericId = interventionPk,
EstAdministrative = false,
Note = new NoteInterneDto { Id = Guid.NewGuid().ToString("D"), Contenu = "Note interne", DateCreation = DateTime.Today }
},
new InterventionNoteBulkRow
{
InterventionNumericId = interventionPk,
EstAdministrative = true,
Note = new NoteInterneDto { Id = Guid.NewGuid().ToString("D"), Contenu = "Note admin", DateCreation = DateTime.Today }
}
],
bulkEmails:
[
new InterventionEmailBulkRow
{
InterventionNumericId = interventionPk,
Email = new EmailInterventionDto { Id = Guid.NewGuid().ToString("D"), Objet = "Sujet", DateCreation = DateTime.Today }
}
]);
var documentId = Guid.NewGuid().ToString("D");
var documentFake = new FakeMobileDocumentService(
[
new DocumentSummaryDto { Id = documentId, InterventionId = interventionGuid.ToString("D"), Nom = "fichier.pdf" }
]);
var service = CreateService(
rendezVous: new FakeRendezVousService(details: details),
interventions: interventionFake,
documents: documentFake);
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", collaborateurId, includeNotesAdministratives: true);
// Assert : notes internes + administratives, courriel et document chargés.
Assert.Equal(2, result.Notes.Count);
Assert.Contains(result.Notes, n => n.EstAdministrative && n.Contenu == "Note admin" && n.InterventionId == interventionGuid.ToString("D"));
Assert.Contains(result.Notes, n => !n.EstAdministrative && n.Contenu == "Note interne");
var email = Assert.Single(result.Emails);
Assert.Equal("Sujet", email.Objet);
Assert.Equal(interventionGuid.ToString("D"), email.InterventionId);
var document = Assert.Single(result.Documents);
Assert.Equal(documentId, document.Id);
Assert.Equal(interventionGuid.ToString("D"), document.InterventionId);
}
[Fact]
public async Task Snapshot_Excludes_Administrative_Notes_Without_Permission()
{
// Arrange : notes interne + administrative, mais le droit notes admin
// n'est pas accordé → la note administrative est exclue du snapshot.
const long interventionPk = 123L;
Guid interventionGuid = Guid.Parse("22222222-1111-0000-0000-000000000001");
const string collaborateurId = "aaaaaaaa-0000-0000-0000-0000000000ff";
var details = new[]
{
new RendezVousDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Titre = "RDV",
TypeActivite = MobileEnabledTypeRdv(),
InterventionSourceReference = new LegacyEntityReference(
DatabaseSchemaVersion.Omika, NumericId: interventionPk, EntityGuid: interventionGuid),
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = collaborateurId,
PeriodeDebut = DateTime.Today.AddHours(9),
PeriodeFin = DateTime.Today.AddHours(10)
}
]
}
};
var interventionFake = new FakeInterventionService(
details:
[
new InterventionDetailsDto
{
Id = interventionGuid.ToString("D"),
SourceReference = new LegacyEntityReference(
DatabaseSchemaVersion.Omika, NumericId: interventionPk, EntityGuid: interventionGuid)
}
],
bulkNotes:
[
new InterventionNoteBulkRow
{
InterventionNumericId = interventionPk,
EstAdministrative = false,
Note = new NoteInterneDto { Id = Guid.NewGuid().ToString("D"), Contenu = "Note interne" }
},
new InterventionNoteBulkRow
{
InterventionNumericId = interventionPk,
EstAdministrative = true,
Note = new NoteInterneDto { Id = Guid.NewGuid().ToString("D"), Contenu = "Note admin" }
}
]);
var service = CreateService(
rendezVous: new FakeRendezVousService(details: details),
interventions: interventionFake);
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", collaborateurId, includeNotesAdministratives: false);
// Assert : seule la note interne est conservée.
var note = Assert.Single(result.Notes);
Assert.False(note.EstAdministrative);
Assert.Equal("Note interne", note.Contenu);
}
[Fact]
public async Task Snapshot_Does_Not_Attach_Dossier_Counts()
{
// Le compteur d'interventions par dossier n'est plus calculé dans le
// snapshot : les dossiers sont chargés à la demande (le compteur, s'il est
// nécessaire, est résolu par l'endpoint de détail/recherche de dossier).
var dossiers = new FakeDossierService(details:
[
new DossierDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
SourceReference = LegacyEntityReference.FromLegacyId(123),
Titre = "Dossier A"
}
])
{
Counts = new Dictionary<long, int> { [123] = 2 }
};
var service = CreateService(dossiers: dossiers);
var result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Dossiers);
}
[Fact]
public async Task Snapshot_RendezVous_Details_Preserve_Contract_Properties()
{
// Arrange : un rendez-vous détail complet avec les propriétés
// contractuelles attendues (SourceReference, ClientSourceReference,
// SiteSourceReference, DossierSourceReference, InterventionSourceReference).
var sourceRef = LegacyEntityReference.FromLegacyId(42);
var clientRef = LegacyEntityReference.FromLegacyId(7);
var siteRef = LegacyEntityReference.FromLegacyId(9);
var dossierRef = LegacyEntityReference.FromLegacyId(11);
var interventionRef = LegacyEntityReference.FromLegacyId(13);
const string collaborateurId = "aaaaaaaa-0000-0000-0000-0000000000ff";
var details = new[]
{
new RendezVousDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
TypeActivite = MobileEnabledTypeRdv(),
SourceReference = sourceRef,
ClientSourceReference = clientRef,
SiteSourceReference = siteRef,
DossierSourceReference = dossierRef,
InterventionSourceReference = interventionRef,
Titre = "Réunion client",
Description = "Point d'avancement",
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = collaborateurId,
PeriodeDebut = DateTime.Today.AddHours(9),
PeriodeFin = DateTime.Today.AddHours(10)
}
]
}
};
var fake = new FakeRendezVousService(details: details);
var service = CreateService(rendezVous: fake);
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", collaborateurId);
// Assert : le rendez-vous détail conserve toutes les propriétés
// contractuelles attendues après le mapping d'identité du snapshot.
var rdv = Assert.Single(result.RendezVous);
Assert.Equal("Réunion client", rdv.Titre);
Assert.Equal("Point d'avancement", rdv.Description);
Assert.NotNull(rdv.SourceReference);
Assert.Equal(42, rdv.SourceReference.NumericId);
Assert.NotNull(rdv.ClientSourceReference);
Assert.Equal(7, rdv.ClientSourceReference.NumericId);
Assert.NotNull(rdv.SiteSourceReference);
Assert.Equal(9, rdv.SiteSourceReference.NumericId);
Assert.NotNull(rdv.DossierSourceReference);
Assert.Equal(11, rdv.DossierSourceReference.NumericId);
Assert.NotNull(rdv.InterventionSourceReference);
Assert.Equal(13, rdv.InterventionSourceReference.NumericId);
}
[Fact]
public async Task Snapshot_Does_Not_Derive_Personnes_From_Clients()
{
// Les contacts de clients ne sont plus embarqués dans le snapshot : ils
// sont portés par le détail client récupéré à la demande. Sans
// intervention, aucune Personne n'est émise.
var clientA = new ClientDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Nom = "Client A",
Contacts = [new ContactDto { Id = Guid.NewGuid().ToString("D"), Nom = "Dupont" }]
};
var fake = new FakeClientService(details: [clientA]);
var service = CreateService(clients: fake);
MobileSnapshotResponse result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Personnes);
}
[Fact]
public async Task Snapshot_Does_Not_Preserve_Client_Personnes()
{
// Les Personnes de clients ne sont plus embarquées (clients chargés à la
// demande). Sans intervention, la collection Personnes reste vide.
var client = new ClientDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Nom = "Client",
Contacts =
[
new ContactDto { Id = Guid.NewGuid().ToString("D"), Nom = "Dupont" },
new ContactDto { Id = Guid.NewGuid().ToString("D"), Nom = "Dupont" }
]
};
var fake = new FakeClientService(details: [client]);
var service = CreateService(clients: fake);
MobileSnapshotResponse result = await service.GetSnapshotAsync("user-1", "device-1");
Assert.Empty(result.Personnes);
}
[Fact]
public void Diagnostic_Personne_Ids_Uniqueness_Counts()
{
// Arrange : un snapshot en mémoire avec un doublon d'Id (diagnostic ciblé).
Guid uniqueA = Guid.NewGuid();
Guid uniqueB = Guid.NewGuid();
Guid duplicate = Guid.NewGuid();
var snapshot = new MobileSnapshotResponse
{
GeneratedAtUtc = DateTimeOffset.UtcNow,
Personnes =
[
new ContactDto { Id = uniqueA.ToString("D") },
new ContactDto { Id = uniqueB.ToString("D") },
new ContactDto { Id = duplicate.ToString("D") },
new ContactDto { Id = duplicate.ToString("D") }
]
};
// Act : compter les Id distincts sans exposer aucune donnée personnelle.
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
int duplicates = 0;
foreach (ContactDto personne in snapshot.Personnes)
{
if (!seen.Add(personne.Id))
{
duplicates++;
}
}
int total = snapshot.Personnes.Count;
int distinct = seen.Count;
Console.WriteLine($"PERSONNE IDS total={total} distinct={distinct} duplicates={duplicates}");
// Assert
Assert.Equal(4, total);
Assert.Equal(3, distinct);
Assert.Equal(1, duplicates);
}
// ===== Filtrage des RendezVous par Collaborateur =====
private const string CollaborateurA = "aaaaaaaa-0000-0000-0000-0000000000a1";
private const string CollaborateurB = "bbbbbbbb-0000-0000-0000-0000000000b2";
private const string TypeRdvMobileKey = "T.RDV.INTER";
private const string TypeRdvMobileLabel = "Intervention";
private static ReferenceValueDto MobileEnabledTypeRdv()
=> new() { Key = TypeRdvMobileKey, Label = TypeRdvMobileLabel };
private static ParametreMetierDto MobileEnabledTypeRdvParam()
=> new()
{
Id = TypeRdvMobileKey,
Code = "14",
Categorie = "T.RDV",
Libelle = TypeRdvMobileLabel,
Parametre = """{"Envoi_mobile":1}"""
};
private static RendezVousDetailsDto CreateRdv(string titre, params string?[] collaborateurIds)
=> new()
{
Id = Guid.NewGuid().ToString("D"),
Titre = titre,
TypeActivite = MobileEnabledTypeRdv(),
Collaborateurs = collaborateurIds
.Select(c => new AffectationCollaborateurDto
{
CollaborateurId = c,
PeriodeDebut = DateTime.Today.AddHours(9),
PeriodeFin = DateTime.Today.AddHours(10)
})
.ToList()
};
private static RendezVousDetailsDto[] FourRendezVous()
=>
[
CreateRdv("RDV1", CollaborateurA),
CreateRdv("RDV2", CollaborateurB),
CreateRdv("RDV3", CollaborateurA, CollaborateurB),
CreateRdv("RDV4")
];
[Fact]
public async Task Snapshot_RendezVous_FilteredByCollaborateurA()
{
// Arrange : RDV1 → A, RDV2 → B, RDV3 → A+B, RDV4 → aucun.
var service = CreateService(rendezVous: new FakeRendezVousService(details: FourRendezVous()));
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", CollaborateurA);
// Assert : uniquement RDV1 et RDV3.
Assert.Equal(2, result.RendezVous.Count);
Assert.Contains(result.RendezVous, r => r.Titre == "RDV1");
Assert.Contains(result.RendezVous, r => r.Titre == "RDV3");
Assert.DoesNotContain(result.RendezVous, r => r.Titre == "RDV2");
Assert.DoesNotContain(result.RendezVous, r => r.Titre == "RDV4");
}
[Fact]
public async Task Snapshot_RendezVous_FilteredByCollaborateurB()
{
// Arrange
var service = CreateService(rendezVous: new FakeRendezVousService(details: FourRendezVous()));
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", CollaborateurB);
// Assert : uniquement RDV2 et RDV3.
Assert.Equal(2, result.RendezVous.Count);
Assert.Contains(result.RendezVous, r => r.Titre == "RDV2");
Assert.Contains(result.RendezVous, r => r.Titre == "RDV3");
Assert.DoesNotContain(result.RendezVous, r => r.Titre == "RDV1");
Assert.DoesNotContain(result.RendezVous, r => r.Titre == "RDV4");
}
[Fact]
public async Task Snapshot_RendezVous_CollaborateurNull_ReturnsEmpty_OtherCollectionsIntact()
{
// Arrange : un rendez-vous et un client présents (le client n'est plus
// embarqué dans le snapshot : chargé à la demande).
var clients = new[]
{
new ClientDetailsDto { Id = Guid.NewGuid().ToString("D"), Nom = "Client X" }
};
var service = CreateService(
rendezVous: new FakeRendezVousService(details: FourRendezVous()),
clients: new FakeClientService(details: clients));
// Act : collaborateur null.
var result = await service.GetSnapshotAsync("user-1", "device-1", null);
// Assert : 0 RendezVous ; les clients ne sont pas embarqués (à la demande).
Assert.Empty(result.RendezVous);
Assert.Empty(result.Clients);
}
[Fact]
public async Task Snapshot_RendezVous_MultiCollaborateur_NoDuplicate()
{
// Arrange : RDV3 affecté à A + B. Pour A, il ne doit apparaître qu'une fois.
var service = CreateService(rendezVous: new FakeRendezVousService(details: FourRendezVous()));
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", CollaborateurA);
// Assert : RDV3 présent une seule fois (pas de doublon).
Assert.Single(result.RendezVous, r => r.Titre == "RDV3");
}
[Fact]
public async Task Snapshot_RendezVous_SansCollaborateur_Exclu()
{
// Arrange : RDV4 sans affectation + RDV1 affecté à A.
var service = CreateService(rendezVous: new FakeRendezVousService(details: FourRendezVous()));
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", CollaborateurA);
// Assert : RDV4 (sans collaborateur) est exclu.
Assert.DoesNotContain(result.RendezVous, r => r.Titre == "RDV4");
Assert.Contains(result.RendezVous, r => r.Titre == "RDV1");
}
[Fact]
public async Task Snapshot_RendezVous_CollaborateurInconnu_ReturnsEmpty()
{
// Arrange : aucun rendez-vous affecté à "collaborateur-inconnu".
var service = CreateService(rendezVous: new FakeRendezVousService(details: FourRendezVous()));
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", "cccccccc-0000-0000-0000-0000000000c3");
// Assert : aucun RendezVous (pas de retombée vers "tous").
Assert.Empty(result.RendezVous);
}
[Fact]
public async Task Snapshot_RendezVous_ResolvesNumericCollaborateurId_ToCanonicalGuid()
{
// Arrange : le token porte le PK numérique de la licence (LI_IDCollaborateur),
// alors que les affectations portent le GUID canonique. Le snapshot doit
// résoudre le PK numérique vers le GUID canonique AVANT de filtrer, sinon
// aucun rendez-vous n'est jamais remonté (bug offline « semaine suivante »).
const string collaborateurGuid = "aaaaaaaa-0000-0000-0000-000000000001";
var collaborateurs = new FakeCollaborateurService(
getById: _ => new CollaborateurDetailsDto { Id = collaborateurGuid, Nom = "Doe" });
var rdvs = new[] { CreateRdv("RDV1", collaborateurGuid) };
var service = CreateService(
collaborateurs: collaborateurs,
rendezVous: new FakeRendezVousService(details: rdvs));
// Act : le token porte "42" (PK numérique), pas le GUID.
var result = await service.GetSnapshotAsync("user-1", "device-1", "42");
// Assert : le rendez-vous est bien remonté (PK numérique → GUID canonique).
Assert.Single(result.RendezVous);
Assert.Contains(result.RendezVous, r => r.Titre == "RDV1");
}
[Fact]
public async Task Snapshot_RendezVous_DebutFin_Provient_De_LAffectation()
{
// Arrange : la racine porte une date (RV_Date) différente de la date de
// l'affectation du collaborateur (RVC_DateDeb / RVC_DateFin). Le snapshot
// doit rabattre Debut/Fin sur la date de l'affectation.
var details = new[]
{
new RendezVousDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Titre = "RDV affectation",
TypeActivite = MobileEnabledTypeRdv(),
Debut = DateTime.Today.AddDays(-30),
Fin = DateTime.Today.AddDays(-30).AddHours(1),
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = CollaborateurA,
PeriodeDebut = DateTime.Today.AddHours(9),
PeriodeFin = DateTime.Today.AddHours(10)
}
]
}
};
var service = CreateService(rendezVous: new FakeRendezVousService(details: details));
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", CollaborateurA);
// Assert : Debut/Fin proviennent de l'affectation, pas de la racine.
var rdv = Assert.Single(result.RendezVous);
Assert.Equal(DateTime.Today.AddHours(9), rdv.Debut);
Assert.Equal(DateTime.Today.AddHours(10), rdv.Fin);
}
[Fact]
public async Task Snapshot_RendezVous_FilteredByEnvoiMobile()
{
// Arrange : un type « Intervention » marqué Envoi_mobile et un type
// « Absence » non marqué. Seul le rendez-vous de type mobile est envoyé.
var parametres = new FakeParametreMetierRepository(
[
MobileEnabledTypeRdvParam(),
new ParametreMetierDto
{
Id = "T.RDV.ABSENCE",
Code = "14",
Categorie = "T.RDV",
Libelle = "Absence",
Parametre = """{"Envoi_mobile":0}"""
}
]);
var details = new[]
{
CreateRdv("RDV intervention", CollaborateurA),
new RendezVousDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Titre = "RDV absence",
TypeActivite = new ReferenceValueDto { Key = "T.RDV.ABSENCE", Label = "Absence" },
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = CollaborateurA,
PeriodeDebut = DateTime.Today.AddHours(11),
PeriodeFin = DateTime.Today.AddHours(12)
}
]
}
};
var service = CreateService(
rendezVous: new FakeRendezVousService(details: details),
parametres: parametres);
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", CollaborateurA);
// Assert : seul le RDV de type mobile est conservé.
var rdv = Assert.Single(result.RendezVous);
Assert.Equal("RDV intervention", rdv.Titre);
}
[Fact]
public async Task Snapshot_RendezVous_SansTypeActivite_Exclu()
{
// Arrange : un rendez-vous sans type d'activité ne peut pas être rattaché
// à une règle Envoi_mobile : il est exclu du mobile.
var details = new[]
{
new RendezVousDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Titre = "RDV sans type",
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = CollaborateurA,
PeriodeDebut = DateTime.Today.AddHours(9),
PeriodeFin = DateTime.Today.AddHours(10)
}
]
}
};
var service = CreateService(rendezVous: new FakeRendezVousService(details: details));
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", CollaborateurA);
// Assert
Assert.Empty(result.RendezVous);
}
[Fact]
public async Task Snapshot_RendezVousPlusieursJours_PeriodeEnveloppeDuCollaborateur()
{
// Arrange : rendez-vous groupé sur plusieurs jours. Le collaborateur A
// porte TROIS affectations (une par jour), transmises dans le désordre —
// la base ne garantit aucun ordre. La période envoyée au mobile doit être
// l'enveloppe (premier début, dernière fin), sinon le rendez-vous
// n'apparaît que sur une journée arbitraire de son intervalle.
DateTime premierJour = DateTime.Today.AddDays(-2);
DateTime dernierJour = DateTime.Today;
var rdv = new RendezVousDetailsDto
{
Id = Guid.NewGuid().ToString("D"),
Titre = "Chantier 3 jours",
TypeActivite = MobileEnabledTypeRdv(),
Collaborateurs =
[
new AffectationCollaborateurDto
{
CollaborateurId = CollaborateurA,
PeriodeDebut = dernierJour.AddHours(12),
PeriodeFin = dernierJour.AddHours(15)
},
new AffectationCollaborateurDto
{
CollaborateurId = CollaborateurB,
PeriodeDebut = premierJour.AddHours(12),
PeriodeFin = premierJour.AddHours(15)
},
new AffectationCollaborateurDto
{
CollaborateurId = CollaborateurA,
PeriodeDebut = premierJour.AddHours(12),
PeriodeFin = premierJour.AddHours(15)
},
new AffectationCollaborateurDto
{
CollaborateurId = CollaborateurA,
PeriodeDebut = premierJour.AddDays(1).AddHours(12),
PeriodeFin = premierJour.AddDays(1).AddHours(15)
}
]
};
var service = CreateService(rendezVous: new FakeRendezVousService(details: [rdv]));
// Act
var result = await service.GetSnapshotAsync("user-1", "device-1", CollaborateurA);
// Assert : premier jour 12:00 → dernier jour 15:00.
RendezVousDetailsDto envoye = Assert.Single(result.RendezVous);
Assert.Equal(premierJour.AddHours(12), envoye.Debut);
Assert.Equal(dernierJour.AddHours(15), envoye.Fin);
}
// ===== Fakes =====
private sealed class FakeIdentityResolver : IMobileEntityIdentityResolver
{
public Guid CreateCanonicalIdentity(string entityType, PhysicalEntityIdentity physical)
=> physical.Guid ?? Guid.NewGuid();
public PhysicalEntityIdentity? ResolvePhysicalIdentity(string entityType, Guid publicGuid)
=> new() { Guid = publicGuid };
public bool IsCanonical(string entityType, Guid publicGuid)
=> publicGuid != Guid.Empty;
}
private sealed class FakeClientService : IClientService
{
private readonly IReadOnlyList<ClientSummaryDto> _items;
private readonly IReadOnlyList<ClientDetailsDto> _details;
private readonly Func<string, ClientDetailsDto?>? _getById;
public FakeClientService(
IReadOnlyList<ClientSummaryDto>? items = null,
IReadOnlyList<ClientDetailsDto>? details = null,
Func<string, ClientDetailsDto?>? getById = null)
{
_items = items ?? Array.Empty<ClientSummaryDto>();
_details = details ?? Array.Empty<ClientDetailsDto>();
_getById = getById;
}
public Task<PagedResult<ClientSummaryDto>> SearchAsync(SearchClientsRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<ClientSummaryDto>(_items, 1, 1000, _items.Count, 1));
}
public Task<PagedResult<ClientDetailsDto>> SearchDetailsAsync(SearchClientsRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<ClientDetailsDto>(_details, 1, 1000, _details.Count, 1));
}
public Task<ClientDetailsDto?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult(_getById is null ? null : _getById(id));
public Task<ClientDetailsDto> CreateAsync(CreateClientRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<ClientDetailsDto?> UpdateAsync(string id, UpdateClientRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<bool> ArchiveAsync(string id, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
private sealed class FakeCollaborateurService : ICollaborateurService
{
private readonly IReadOnlyList<CollaborateurSummaryDto> _items;
private readonly IReadOnlyList<CollaborateurDetailsDto> _details;
private readonly Func<string, CollaborateurDetailsDto?>? _getById;
public FakeCollaborateurService(
IReadOnlyList<CollaborateurSummaryDto>? items = null,
IReadOnlyList<CollaborateurDetailsDto>? details = null,
Func<string, CollaborateurDetailsDto?>? getById = null)
{
_items = items ?? Array.Empty<CollaborateurSummaryDto>();
_details = details ?? Array.Empty<CollaborateurDetailsDto>();
_getById = getById;
}
public Task<PagedResult<CollaborateurSummaryDto>> SearchAsync(SearchCollaborateurRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<CollaborateurSummaryDto>(_items, 1, 1000, _items.Count, 1));
}
public Task<PagedResult<CollaborateurDetailsDto>> SearchDetailsAsync(SearchCollaborateurRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<CollaborateurDetailsDto>(_details, 1, 1000, _details.Count, 1));
}
public Task<CollaborateurDetailsDto?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult(_getById is null ? null : _getById(id));
public Task<CollaborateurDetailsDto> CreateAsync(CreateCollaborateurRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<CollaborateurDetailsDto> UpdateAsync(string id, UpdateCollaborateurRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<IReadOnlyList<RendezVousDetailsDto>> GetRendezVousAsync(string collaborateurId, DateTime debutMin, DateTime debutMax, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<RendezVousDetailsDto>>([]);
public Task<IReadOnlyList<RendezVousDetailsDto>> GetRendezVousForCollaborateursAsync(IReadOnlyList<string> collaborateurIds, DateTime debutMin, DateTime debutMax, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<RendezVousDetailsDto>>([]);
public Task<IReadOnlyList<long>> GetSocieteIdsAsync(long collaborateurId, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<long>>([]);
public Task<IReadOnlyList<long>> GetAllowedCollaborateurIdsAsync(long collaborateurId, bool includeSocietePrincipale, bool includeSocietes, bool includeEquipe, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<long>>([]);
public Task<bool> ArchiveAsync(string id, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
private sealed class FakeSiteService : ISiteService
{
private readonly IReadOnlyList<SiteSummaryDto> _items;
private readonly IReadOnlyList<SiteDetailsDto> _details;
private readonly Func<string, SiteDetailsDto?>? _getById;
public FakeSiteService(
IReadOnlyList<SiteSummaryDto>? items = null,
IReadOnlyList<SiteDetailsDto>? details = null,
Func<string, SiteDetailsDto?>? getById = null)
{
_items = items ?? Array.Empty<SiteSummaryDto>();
_details = details ?? Array.Empty<SiteDetailsDto>();
_getById = getById;
}
public Task<PagedResult<SiteSummaryDto>> SearchAsync(SearchSitesRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<SiteSummaryDto>(_items, 1, 1000, _items.Count, 1));
}
public Task<PagedResult<SiteDetailsDto>> SearchDetailsAsync(SearchSitesRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<SiteDetailsDto>(_details, 1, 1000, _details.Count, 1));
}
public Task<SiteDetailsDto?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult(_getById is null ? null : _getById(id));
public Task<IReadOnlyList<ContactDto>> GetContactsAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<ContactDto>>([]);
public Task<IReadOnlyList<RendezVousDetailsDto>> GetRendezVousAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<RendezVousDetailsDto>>([]);
public Task<IReadOnlyList<ClientSummaryDto>> GetClientsAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<ClientSummaryDto>>([]);
public Task<SiteDetailsDto> CreateAsync(CreateSiteRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<SiteDetailsDto?> UpdateAsync(string id, UpdateSiteRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<bool> ArchiveAsync(string id, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
private sealed class FakeInterventionService : IInterventionService
{
private readonly IReadOnlyList<InterventionDetailsDto> _details;
private readonly Func<string, InterventionDetailsDto?>? _getById;
private readonly IReadOnlyList<InterventionNoteBulkRow> _bulkNotes;
private readonly IReadOnlyList<InterventionEmailBulkRow> _bulkEmails;
public FakeInterventionService(
IReadOnlyList<InterventionDetailsDto>? details = null,
Func<string, InterventionDetailsDto?>? getById = null,
IReadOnlyList<InterventionNoteBulkRow>? bulkNotes = null,
IReadOnlyList<InterventionEmailBulkRow>? bulkEmails = null)
{
_details = details ?? Array.Empty<InterventionDetailsDto>();
_getById = getById;
_bulkNotes = bulkNotes ?? Array.Empty<InterventionNoteBulkRow>();
_bulkEmails = bulkEmails ?? Array.Empty<InterventionEmailBulkRow>();
}
/// <summary>Nombre d'appels au chargement bulk d'interventions (SearchDetailsAsync).</summary>
public int SearchDetailsCallCount { get; private set; }
/// <summary>Dernière requête reçue par SearchDetailsAsync (pour vérifier le filtre ciblé).</summary>
public SearchInterventionsRequest? LastSearchRequest { get; private set; }
public Task<PagedResult<InterventionSummaryDto>> SearchAsync(SearchInterventionsRequest request, CancellationToken cancellationToken = default)
=> Task.FromResult(new PagedResult<InterventionSummaryDto>(Array.Empty<InterventionSummaryDto>(), 1, 1000, 0, 1));
public Task<PagedResult<InterventionDetailsDto>> SearchDetailsAsync(SearchInterventionsRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
SearchDetailsCallCount++;
LastSearchRequest = request;
return Task.FromResult(new PagedResult<InterventionDetailsDto>(_details, 1, 1000, _details.Count, 1));
}
public Task<InterventionDetailsDto?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult(_getById?.Invoke(id));
public Task<InterventionDetailsDto> CreateAsync(CreateInterventionRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<InterventionDetailsDto?> UpdateAsync(string id, UpdateInterventionRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<bool> ArchiveAsync(string id, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<IReadOnlyList<NoteInterneDto>> GetNotesAsync(string interventionId, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<NoteInterneDto>>([]);
public Task<IReadOnlyList<NoteInterneDto>> GetNotesAdministrativesAsync(string interventionId, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<NoteInterneDto>>([]);
public Task<NoteInterneDto> AddNoteAsync(string interventionId, string contenu, AuditActor? auteur, CancellationToken cancellationToken = default)
=> Task.FromResult(new NoteInterneDto());
public Task<IReadOnlyList<EmailInterventionDto>> GetEmailsAsync(string interventionId, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<EmailInterventionDto>>([]);
public Task<IReadOnlyList<InterventionNoteBulkRow>> GetNotesForInterventionsAsync(
IReadOnlyList<long> numericInterventionIds, CancellationToken cancellationToken = default)
=> Task.FromResult(_bulkNotes);
public Task<IReadOnlyList<InterventionEmailBulkRow>> GetEmailsForInterventionsAsync(
IReadOnlyList<long> numericInterventionIds, CancellationToken cancellationToken = default)
=> Task.FromResult(_bulkEmails);
public Task<EmailInterventionDto> AddEmailAsync(string interventionId, CreateEmailInterventionRequest request, AuditActor? auteur, CancellationToken cancellationToken = default)
=> Task.FromResult(new EmailInterventionDto());
}
private sealed class FakeDossierService : IDossierService
{
private readonly IReadOnlyList<DossierSummaryDto> _items;
private readonly IReadOnlyList<DossierDetailsDto> _details;
private readonly Func<string, DossierDetailsDto?>? _getById;
public FakeDossierService(
IReadOnlyList<DossierSummaryDto>? items = null,
IReadOnlyList<DossierDetailsDto>? details = null,
Func<string, DossierDetailsDto?>? getById = null)
{
_items = items ?? Array.Empty<DossierSummaryDto>();
_details = details ?? Array.Empty<DossierDetailsDto>();
_getById = getById;
}
/// <summary>Compteurs d'interventions non archivées (PK numérique → count).</summary>
public IReadOnlyDictionary<long, int> Counts { get; set; } = new Dictionary<long, int>();
public Task<PagedResult<DossierSummaryDto>> SearchAsync(SearchDossiersRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<DossierSummaryDto>(_items, 1, 1000, _items.Count, 1));
}
public Task<PagedResult<DossierDetailsDto>> SearchDetailsAsync(SearchDossiersRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<DossierDetailsDto>(_details, 1, 1000, _details.Count, 1));
}
public Task<DossierDetailsDto?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult(_getById is null ? null : _getById(id));
public Task<DossierDetailsDto> CreateAsync(CreateDossierRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<DossierDetailsDto?> UpdateAsync(string id, UpdateDossierRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<bool> ArchiveAsync(string id, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<IReadOnlyDictionary<long, int>> GetNonArchivedInterventionCountsAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(Counts);
}
private sealed class FakeParametreMetierRepository : IParametreMetierRepository
{
private readonly IReadOnlyList<ParametreMetierDto> _items;
public FakeParametreMetierRepository(IReadOnlyList<ParametreMetierDto>? items = null)
=> _items = items ?? [MobileEnabledTypeRdvParam()];
public Task<IReadOnlyList<ParametreMetierDto>> GetAllAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(_items);
}
}
private sealed class FakeRendezVousService : IRendezVousService
{
private readonly IReadOnlyList<RendezVousSummaryDto> _items;
private readonly IReadOnlyList<RendezVousDetailsDto> _details;
private readonly Func<string, RendezVousDetailsDto?>? _getById;
public FakeRendezVousService(
IReadOnlyList<RendezVousSummaryDto>? items = null,
IReadOnlyList<RendezVousDetailsDto>? details = null,
Func<string, RendezVousDetailsDto?>? getById = null)
{
_items = items ?? Array.Empty<RendezVousSummaryDto>();
_details = details ?? Array.Empty<RendezVousDetailsDto>();
_getById = getById;
}
public Task<PagedResult<RendezVousSummaryDto>> SearchAsync(SearchRendezVousRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<RendezVousSummaryDto>(_items, 1, 1000, _items.Count, 1));
}
public Task<PagedResult<RendezVousDetailsDto>> SearchDetailsAsync(SearchRendezVousRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<RendezVousDetailsDto>(_details, 1, 1000, _details.Count, 1));
}
public Task<RendezVousDetailsDto?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult(_getById is null ? null : _getById(id));
public Task<RendezVousDetailsDto> CreateAsync(CreateRendezVousRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task<RendezVousDetailsDto> UpdateAsync(string id, UpdateRendezVousRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> throw new NotSupportedException();
public Task CancelAsync(string id, AnnulationRendezVousDto annulation, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> Task.CompletedTask;
public Task ArchiveAsync(string id, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> Task.CompletedTask;
public Task<bool> SetTempsValideAsync(string rendezVousId, string collaborateurId, bool tempsValide, CancellationToken cancellationToken = default, AuditActor? auteur = null)
=> Task.FromResult(true);
}
private sealed class FakeMobileDocumentService : IMobileDocumentService
{
private readonly IReadOnlyList<DocumentSummaryDto> _documents;
public FakeMobileDocumentService(IReadOnlyList<DocumentSummaryDto>? documents = null)
=> _documents = documents ?? Array.Empty<DocumentSummaryDto>();
public Task<IReadOnlyList<DocumentSummaryDto>> GetByInterventionsAsync(
IReadOnlyList<long> numericInterventionIds,
IReadOnlyCollection<string>? allowedCategories,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<IReadOnlyList<DocumentSummaryDto>>(_documents);
}
public Task<IReadOnlyList<DocumentSummaryDto>> GetByInterventionAsync(
string interventionId,
IReadOnlyCollection<string>? allowedCategories,
CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<DocumentSummaryDto>>(_documents);
public Task<DocumentDownloadResponse?> DownloadAsync(Guid documentGuid, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<long> ResolveDocumentPkAsync(Guid documentGuid, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<string?> ResolveDocumentFullPathAsync(Guid documentGuid, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<DocumentUploadResponse> UploadAsync(DocumentUploadRequest request, string categorieCle, AuditActor? auteur, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<IReadOnlyList<NoteInterneDto>> GetNotesAsync(Guid documentGuid, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<NoteInterneDto> AddNoteAsync(Guid documentGuid, string contenu, AuditActor? auteur, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
private sealed class FakeSuiviTempsService : ISuiviTempsService
{
private readonly IReadOnlyList<SuiviTempsDto> _items;
public FakeSuiviTempsService(IReadOnlyList<SuiviTempsDto>? items = null)
=> _items = items ?? Array.Empty<SuiviTempsDto>();
public Task<PagedResult<SuiviTempsDto>> SearchAsync(SuiviTempsSearchCriteria criteria, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new PagedResult<SuiviTempsDto>(_items, 1, 1000, _items.Count, 1));
}
public Task<SuiviTempsDto?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
=> Task.FromResult(_items.FirstOrDefault(i => i.Id == id));
public Task<SuiviTempsDto> CreateAsync(CreateSuiviTempsRequest request, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
}