using System.Reflection; using Omika.Core.Compatibility; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Documents; using Omika.Core.Contracts.Interventions; using Omika.Core.Contracts.Mobile.Sync; using Omika.Core.Domain.Commands; using Omika.Core.Domain.Entities.Interventions; using Omika.Core.Domain.ValueObjects; using Omika.Core.Interfaces; using Omika.Core.Interfaces.Mobile; using Omika.Core.Services; using Omika.Core.Domain; namespace Omika.Tests.Services; /// /// Tests unitaires de avec un fake /// en mémoire. Aucune base réelle. /// public class InterventionServiceTests { private static readonly IMobileEntityIdentityResolver IdentityResolver = new FakeIdentityResolver(); // ========================================================================= // SEARCH // ========================================================================= [Fact] public async Task SearchAsync_CallsRepositoryOnce() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest()); Assert.Equal(1, repo.SearchCallCount); } [Fact] public async Task SearchAsync_TransmitsPage() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { Page = 3 }); Assert.Equal(3, repo.LastSearchCriteria?.Page); } [Fact] public async Task SearchAsync_TransmitsPageSize() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { PageSize = 50 }); Assert.Equal(50, repo.LastSearchCriteria?.PageSize); } [Fact] public async Task SearchAsync_TransmitsSearch() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { Search = "test" }); Assert.Equal("test", repo.LastSearchCriteria?.Search); } [Fact] public async Task SearchAsync_TransmitsTitre() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { Titre = "Réparation" }); Assert.Equal("Réparation", repo.LastSearchCriteria?.Titre); } [Fact] public async Task SearchAsync_TransmitsReference() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { Reference = "INT-001" }); Assert.Equal("INT-001", repo.LastSearchCriteria?.Reference); } [Fact] public async Task SearchAsync_TransmitsType() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { Type = "Maintenance" }); Assert.Equal("Maintenance", repo.LastSearchCriteria?.Type); } [Fact] public async Task SearchAsync_TransmitsEtat() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { Etat = "En cours" }); Assert.Equal("En cours", repo.LastSearchCriteria?.Etat); } [Fact] public async Task SearchAsync_TransmitsEstArchive() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { EstArchive = true }); Assert.True(repo.LastSearchCriteria?.EstArchive); } [Fact] public async Task SearchAsync_TransmitsSortBy() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { SortBy = "Date" }); Assert.Equal("Date", repo.LastSearchCriteria?.SortBy); } [Fact] public async Task SearchAsync_TransmitsSortDescending() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchAsync(new SearchInterventionsRequest { SortDescending = true }); Assert.True(repo.LastSearchCriteria?.SortDescending); } [Fact] public async Task SearchAsync_TransmitsCancellationToken() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.SearchAsync(new SearchInterventionsRequest(), cts.Token); Assert.Equal(cts.Token, repo.LastCancellationToken); } [Fact] public async Task SearchAsync_EmptyResult_ReturnsEmptyPage() { var repo = new FakeInterventionRepository { SearchResult = new PagedResult([], 1, 20, 0, 0) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchInterventionsRequest()); Assert.Empty(result.Items); Assert.Equal(0, result.TotalCount); Assert.Equal(0, result.TotalPages); Assert.Equal(1, result.Page); Assert.Equal(20, result.PageSize); } [Fact] public async Task SearchAsync_SingleIntervention_MapsToSummaryDto() { var intervention = new Intervention { Reference = "INT-001", Titre = "Réparation", Type = new ReferenceValue("Maintenance") { Key = "MAINT" }, Etat = new ReferenceValue("En cours") { Key = "ENCOURS" }, DatePrevueDebut = new DateTime(2026, 1, 15) }; var repo = new FakeInterventionRepository { SearchResult = new PagedResult([intervention], 1, 20, 1, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchInterventionsRequest()); InterventionSummaryDto dto = Assert.Single(result.Items); Assert.Equal("INT-001", dto.Reference); Assert.Equal("Réparation", dto.Titre); Assert.Equal("Maintenance", dto.Type); Assert.Equal("En cours", dto.Etat); Assert.Equal(new DateTime(2026, 1, 15), dto.DatePrevueDebut); } [Fact] public async Task SearchAsync_MultipleInterventions_MapsAllToDtos() { var interventions = new[] { new Intervention { Titre = "Alpha", Reference = "A" }, new Intervention { Titre = "Beta", Reference = "B" }, new Intervention { Titre = "Gamma", Reference = "C" } }; var repo = new FakeInterventionRepository { SearchResult = new PagedResult(interventions, 1, 20, 3, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchInterventionsRequest()); Assert.Equal(3, result.Items.Count); Assert.Equal(3, result.TotalCount); Assert.Equal("Alpha", result.Items[0].Titre); Assert.Equal("Beta", result.Items[1].Titre); Assert.Equal("Gamma", result.Items[2].Titre); } [Fact] public async Task SearchAsync_PreservesTotalCount() { var repo = new FakeInterventionRepository { SearchResult = new PagedResult([], 2, 10, 42, 5) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchInterventionsRequest()); Assert.Equal(42, result.TotalCount); Assert.Equal(2, result.Page); Assert.Equal(10, result.PageSize); Assert.Equal(5, result.TotalPages); } [Fact] public async Task SearchAsync_IdComesFromInterventionContractMapper() { var intervention = new Intervention { Titre = "Test" }; var expectedId = intervention.Id.ToString(); var repo = new FakeInterventionRepository { SearchResult = new PagedResult([intervention], 1, 20, 1, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchInterventionsRequest()); Assert.Equal(expectedId, result.Items[0].Id); } [Fact] public async Task SearchAsync_ServiceDoesNotRePaginate() { var repo = new FakeInterventionRepository { SearchResult = new PagedResult( [new Intervention { Titre = "A" }, new Intervention { Titre = "B" }], 3, 15, 100, 7) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchInterventionsRequest { Page = 1, PageSize = 5 }); Assert.Equal(2, result.Items.Count); Assert.Equal(100, result.TotalCount); Assert.Equal(7, result.TotalPages); } // ========================================================================= // SEARCH DETAILS (BULK) // ========================================================================= [Fact] public async Task SearchDetailsAsync_CallsRepositoryOnce() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest()); Assert.Equal(1, repo.SearchCallCount); } [Fact] public async Task SearchDetailsAsync_TransmitsPage() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { Page = 3 }); Assert.Equal(3, repo.LastSearchCriteria?.Page); } [Fact] public async Task SearchDetailsAsync_TransmitsPageSize() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { PageSize = 50 }); Assert.Equal(50, repo.LastSearchCriteria?.PageSize); } [Fact] public async Task SearchDetailsAsync_TransmitsSearch() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { Search = "test" }); Assert.Equal("test", repo.LastSearchCriteria?.Search); } [Fact] public async Task SearchDetailsAsync_TransmitsTitre() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { Titre = "Réparation" }); Assert.Equal("Réparation", repo.LastSearchCriteria?.Titre); } [Fact] public async Task SearchDetailsAsync_TransmitsReference() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { Reference = "INT-001" }); Assert.Equal("INT-001", repo.LastSearchCriteria?.Reference); } [Fact] public async Task SearchDetailsAsync_TransmitsType() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { Type = "Maintenance" }); Assert.Equal("Maintenance", repo.LastSearchCriteria?.Type); } [Fact] public async Task SearchDetailsAsync_TransmitsEtat() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { Etat = "En cours" }); Assert.Equal("En cours", repo.LastSearchCriteria?.Etat); } [Fact] public async Task SearchDetailsAsync_TransmitsEstArchive() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { EstArchive = true }); Assert.True(repo.LastSearchCriteria?.EstArchive); } [Fact] public async Task SearchDetailsAsync_TransmitsSortBy() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { SortBy = "Date" }); Assert.Equal("Date", repo.LastSearchCriteria?.SortBy); } [Fact] public async Task SearchDetailsAsync_TransmitsSortDescending() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.SearchDetailsAsync(new SearchInterventionsRequest { SortDescending = true }); Assert.True(repo.LastSearchCriteria?.SortDescending); } [Fact] public async Task SearchDetailsAsync_TransmitsCancellationToken() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.SearchDetailsAsync(new SearchInterventionsRequest(), cts.Token); Assert.Equal(cts.Token, repo.LastCancellationToken); } [Fact] public async Task SearchDetailsAsync_EmptyResult_ReturnsEmptyPage() { var repo = new FakeInterventionRepository { SearchResult = new PagedResult([], 1, 20, 0, 0) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); Assert.Empty(result.Items); Assert.Equal(0, result.TotalCount); Assert.Equal(0, result.TotalPages); Assert.Equal(1, result.Page); Assert.Equal(20, result.PageSize); } [Fact] public async Task SearchDetailsAsync_MultipleInterventions_MapsAllToDetailsDtos() { var interventions = new[] { new Intervention { Titre = "Alpha", Reference = "A" }, new Intervention { Titre = "Beta", Reference = "B" }, new Intervention { Titre = "Gamma", Reference = "C" } }; var repo = new FakeInterventionRepository { SearchResult = new PagedResult(interventions, 1, 20, 3, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); Assert.Equal(3, result.Items.Count); Assert.Equal(3, result.TotalCount); Assert.Equal("Alpha", result.Items[0].Titre); Assert.Equal("Beta", result.Items[1].Titre); Assert.Equal("Gamma", result.Items[2].Titre); } [Fact] public async Task SearchDetailsAsync_PreservesTotalCount() { var repo = new FakeInterventionRepository { SearchResult = new PagedResult([], 2, 10, 42, 5) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); Assert.Equal(42, result.TotalCount); Assert.Equal(2, result.Page); Assert.Equal(10, result.PageSize); Assert.Equal(5, result.TotalPages); } [Fact] public async Task SearchDetailsAsync_SourceReference_Preserved() { var intervention = new Intervention { Titre = "Test", SourceReference = LegacyEntityReference.FromLegacyId(42) }; var repo = new FakeInterventionRepository { SearchResult = new PagedResult([intervention], 1, 20, 1, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); InterventionDetailsDto dto = Assert.Single(result.Items); Assert.NotNull(dto.SourceReference); Assert.Equal(42, dto.SourceReference.NumericId); } [Fact] public async Task SearchDetailsAsync_ClientSourceReference_Preserved() { var intervention = new Intervention { Titre = "Test", ClientSourceReference = LegacyEntityReference.FromLegacyId(7) }; var repo = new FakeInterventionRepository { SearchResult = new PagedResult([intervention], 1, 20, 1, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); InterventionDetailsDto dto = Assert.Single(result.Items); Assert.NotNull(dto.ClientSourceReference); Assert.Equal(7, dto.ClientSourceReference.NumericId); } [Fact] public async Task SearchDetailsAsync_SiteSourceReference_Preserved() { var intervention = new Intervention { Titre = "Test", SiteSourceReference = LegacyEntityReference.FromLegacyId(99) }; var repo = new FakeInterventionRepository { SearchResult = new PagedResult([intervention], 1, 20, 1, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); InterventionDetailsDto dto = Assert.Single(result.Items); Assert.NotNull(dto.SiteSourceReference); Assert.Equal(99, dto.SiteSourceReference.NumericId); } [Fact] public async Task SearchDetailsAsync_DossierSourceReference_Preserved() { var intervention = new Intervention { Titre = "Test", DossierSourceReference = LegacyEntityReference.FromLegacyId(123) }; var repo = new FakeInterventionRepository { SearchResult = new PagedResult([intervention], 1, 20, 1, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); InterventionDetailsDto dto = Assert.Single(result.Items); Assert.NotNull(dto.DossierSourceReference); Assert.Equal(123, dto.DossierSourceReference.NumericId); } [Fact] public async Task SearchDetailsAsync_ActionsMapped() { var intervention = new Intervention { Titre = "Test" }; intervention.AddAction(new ActionIntervention { Description = "Action 1" }); var repo = new FakeInterventionRepository { SearchResult = new PagedResult([intervention], 1, 20, 1, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); InterventionDetailsDto dto = Assert.Single(result.Items); Assert.Single(dto.Actions); Assert.Equal("Action 1", dto.Actions[0].Description); } [Fact] public async Task SearchDetailsAsync_DoesNotCallGetById() { // Le service SearchDetailsAsync doit appeler UNIQUEMENT le repository // bulk (SearchDetailsAsync) et JAMAIS GetDetailsAsync par intervention. var repo = new FakeInterventionRepository { SearchResult = new PagedResult( [new Intervention { Titre = "A" }, new Intervention { Titre = "B" }], 1, 20, 2, 1) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest()); Assert.Equal(2, result.Items.Count); Assert.Equal(0, repo.DetailsCallCount); } [Fact] public async Task SearchDetailsAsync_ServiceDoesNotRePaginate() { var repo = new FakeInterventionRepository { SearchResult = new PagedResult( [new Intervention { Titre = "A" }, new Intervention { Titre = "B" }], 3, 15, 100, 7) }; var service = new InterventionService(repo, IdentityResolver); PagedResult result = await service.SearchDetailsAsync( new SearchInterventionsRequest { Page = 1, PageSize = 5 }); Assert.Equal(2, result.Items.Count); Assert.Equal(100, result.TotalCount); Assert.Equal(7, result.TotalPages); } // ========================================================================= // DETAIL // ========================================================================= [Fact] public async Task GetByIdAsync_TransmitsExactId() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.GetByIdAsync("test-id-123"); Assert.Equal("test-id-123", repo.LastDetailsId); } [Fact] public async Task GetByIdAsync_TransmitsCancellationToken() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.GetByIdAsync("42", cts.Token); Assert.Equal(cts.Token, repo.LastCancellationToken); } [Fact] public async Task GetByIdAsync_RepositoryReturnsNull_ReturnsNull() { var repo = new FakeInterventionRepository { DetailsResult = null }; var service = new InterventionService(repo, IdentityResolver); InterventionDetailsDto? result = await service.GetByIdAsync("999"); Assert.Null(result); } [Fact] public async Task GetByIdAsync_MapsToDetailsDto() { var intervention = new Intervention { Reference = "INT-001", Titre = "Réparation", Type = new ReferenceValue("Maintenance") { Key = "MAINT" }, Etat = new ReferenceValue("En cours") { Key = "ENCOURS" } }; var repo = new FakeInterventionRepository { DetailsResult = intervention }; var service = new InterventionService(repo, IdentityResolver); InterventionDetailsDto? result = await service.GetByIdAsync("42"); Assert.NotNull(result); Assert.Equal("INT-001", result.Reference); Assert.Equal("Réparation", result.Titre); Assert.Equal("Maintenance", result.Type?.Label); Assert.Equal("En cours", result.Etat?.Label); } [Fact] public async Task GetByIdAsync_ActionsMapped() { var intervention = new Intervention { Titre = "Test" }; intervention.AddAction(new ActionIntervention { Description = "Action 1" }); var repo = new FakeInterventionRepository { DetailsResult = intervention }; var service = new InterventionService(repo, IdentityResolver); InterventionDetailsDto? result = await service.GetByIdAsync("42"); Assert.NotNull(result); Assert.Single(result.Actions); Assert.Equal("Action 1", result.Actions[0].Description); } [Fact] public async Task GetByIdAsync_IdComesFromMapper() { var intervention = new Intervention { Titre = "Test" }; var expectedId = intervention.Id.ToString(); var repo = new FakeInterventionRepository { DetailsResult = intervention }; var service = new InterventionService(repo, IdentityResolver); InterventionDetailsDto? result = await service.GetByIdAsync("42"); Assert.NotNull(result); Assert.Equal(expectedId, result.Id); } [Fact] public async Task GetByIdAsync_NoGuidParsingInService() { var repo = new FakeInterventionRepository(); var service = new InterventionService(repo, IdentityResolver); await service.GetByIdAsync("not-a-guid-nor-int"); Assert.Equal("not-a-guid-nor-int", repo.LastDetailsId); } [Fact] public async Task GetByIdAsync_RepositoryException_Propagated() { var repo = new FakeInterventionRepository { ThrowOnDetails = new InvalidOperationException("Infrastructure error") }; var service = new InterventionService(repo, IdentityResolver); var ex = await Assert.ThrowsAsync( () => service.GetByIdAsync("42")); Assert.Equal("Infrastructure error", ex.Message); } // ========================================================================= // CREATE // ========================================================================= [Fact] public async Task CreateAsync_CallsRepositoryOnce() { var repo = new FakeInterventionRepository { CreateResult = new Intervention { Titre = "Nouveau", Reference = "INT-001" } }; var service = new InterventionService(repo, IdentityResolver); await service.CreateAsync(new CreateInterventionRequest { Titre = "Nouveau" }); Assert.Equal(1, repo.CreateCallCount); } [Fact] public async Task CreateAsync_MapsAllProperties() { var repo = new FakeInterventionRepository { CreateResult = new Intervention { Titre = "Nouveau" } }; var service = new InterventionService(repo, IdentityResolver); await service.CreateAsync(new CreateInterventionRequest { Reference = "INT-001", Titre = "Nouveau", Description = "Description", Type = new ReferenceValueDto { Key = "MAINT" }, Priorite = new ReferenceValueDto { Key = "HAUTE" }, DatePrevueDebut = new DateTime(2026, 1, 15), DatePrevueFin = new DateTime(2026, 1, 20) }); Assert.Equal("INT-001", repo.LastCreateCommand!.Reference); Assert.Equal("Nouveau", repo.LastCreateCommand!.Titre); Assert.Equal("Description", repo.LastCreateCommand!.Description); Assert.Equal("MAINT", repo.LastCreateCommand!.TypeKey); Assert.Equal("HAUTE", repo.LastCreateCommand!.PrioriteKey); Assert.Equal(new DateTime(2026, 1, 15), repo.LastCreateCommand!.DatePrevueDebut); Assert.Equal(new DateTime(2026, 1, 20), repo.LastCreateCommand!.DatePrevueFin); } [Fact] public async Task CreateAsync_ResultMappedToDetails() { var repo = new FakeInterventionRepository { CreateResult = new Intervention { Titre = "Nouveau", Reference = "INT-001" } }; var service = new InterventionService(repo, IdentityResolver); InterventionDetailsDto result = await service.CreateAsync(new CreateInterventionRequest { Titre = "Nouveau" }); Assert.Equal("Nouveau", result.Titre); Assert.Equal("INT-001", result.Reference); } [Fact] public async Task CreateAsync_CancellationTokenTransmitted() { var repo = new FakeInterventionRepository { CreateResult = new Intervention { Titre = "Nouveau" } }; var service = new InterventionService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.CreateAsync(new CreateInterventionRequest { Titre = "Nouveau" }, cts.Token); Assert.Equal(cts.Token, repo.LastCreateCancellationToken); } [Fact] public async Task CreateAsync_ExceptionPropagated() { var repo = new FakeInterventionRepository { ThrowOnCreate = new InvalidOperationException("Panne") }; var service = new InterventionService(repo, IdentityResolver); var ex = await Assert.ThrowsAsync( () => service.CreateAsync(new CreateInterventionRequest { Titre = "Nouveau" })); Assert.Equal("Panne", ex.Message); } // ========================================================================= // UPDATE // ========================================================================= [Fact] public async Task UpdateAsync_MapsAllProperties() { var repo = new FakeInterventionRepository { UpdateResult = new Intervention { Titre = "Updated", Reference = "INT-002" } }; var service = new InterventionService(repo, IdentityResolver); await service.UpdateAsync("42", new UpdateInterventionRequest { Reference = "INT-002", Titre = "Updated", Type = new ReferenceValueDto { Key = "MAINT" }, Etat = new ReferenceValueDto { Key = "ENCOURS" }, Priorite = new ReferenceValueDto { Key = "HAUTE" } }); Assert.Equal(1, repo.UpdateCallCount); Assert.Equal("42", repo.LastUpdateId); Assert.Equal("INT-002", repo.LastUpdateCommand!.Reference); Assert.Equal("Updated", repo.LastUpdateCommand!.Titre); Assert.Equal("MAINT", repo.LastUpdateCommand!.TypeKey); Assert.Equal("ENCOURS", repo.LastUpdateCommand!.EtatKey); Assert.Equal("HAUTE", repo.LastUpdateCommand!.PrioriteKey); } [Fact] public async Task UpdateAsync_NullsPreserved() { var repo = new FakeInterventionRepository { UpdateResult = new Intervention { Titre = "X" } }; var service = new InterventionService(repo, IdentityResolver); await service.UpdateAsync("42", new UpdateInterventionRequest()); Assert.Null(repo.LastUpdateCommand!.TypeKey); Assert.Null(repo.LastUpdateCommand!.EtatKey); Assert.Null(repo.LastUpdateCommand!.PrioriteKey); } [Fact] public async Task UpdateAsync_RepoReturnsNull_ReturnsNull() { var repo = new FakeInterventionRepository { UpdateResult = null }; var service = new InterventionService(repo, IdentityResolver); InterventionDetailsDto? result = await service.UpdateAsync("42", new UpdateInterventionRequest()); Assert.Null(result); } [Fact] public async Task UpdateAsync_IdTransmittedExactly() { var repo = new FakeInterventionRepository { UpdateResult = new Intervention { Titre = "XY" } }; var service = new InterventionService(repo, IdentityResolver); await service.UpdateAsync("my-guid-123", new UpdateInterventionRequest()); Assert.Equal("my-guid-123", repo.LastUpdateId); } [Fact] public async Task UpdateAsync_CancellationTokenTransmitted() { var repo = new FakeInterventionRepository { UpdateResult = new Intervention { Titre = "X" } }; var service = new InterventionService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.UpdateAsync("42", new UpdateInterventionRequest(), cts.Token); Assert.Equal(cts.Token, repo.LastUpdateCancellationToken); } [Fact] public async Task UpdateAsync_ExceptionPropagated() { var repo = new FakeInterventionRepository { ThrowOnUpdate = new InvalidOperationException("Panne") }; var service = new InterventionService(repo, IdentityResolver); var ex = await Assert.ThrowsAsync( () => service.UpdateAsync("42", new UpdateInterventionRequest())); Assert.Equal("Panne", ex.Message); } // ========================================================================= // ARCHIVE // ========================================================================= [Fact] public async Task ArchiveAsync_TruePropagated() { var repo = new FakeInterventionRepository { ArchiveResult = true }; var service = new InterventionService(repo, IdentityResolver); bool result = await service.ArchiveAsync("42"); Assert.True(result); Assert.Equal(1, repo.ArchiveCallCount); } [Fact] public async Task ArchiveAsync_FalsePropagated() { var repo = new FakeInterventionRepository { ArchiveResult = false }; var service = new InterventionService(repo, IdentityResolver); bool result = await service.ArchiveAsync("42"); Assert.False(result); } [Fact] public async Task ArchiveAsync_IdTransmittedExactly() { var repo = new FakeInterventionRepository { ArchiveResult = true }; var service = new InterventionService(repo, IdentityResolver); await service.ArchiveAsync("my-guid-123"); Assert.Equal("my-guid-123", repo.LastArchiveId); } [Fact] public async Task ArchiveAsync_CancellationTokenTransmitted() { var repo = new FakeInterventionRepository { ArchiveResult = true }; var service = new InterventionService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.ArchiveAsync("42", cts.Token); Assert.Equal(cts.Token, repo.LastArchiveCancellationToken); } [Fact] public async Task ArchiveAsync_ExceptionPropagated() { var repo = new FakeInterventionRepository { ThrowOnArchive = new InvalidOperationException("Panne") }; var service = new InterventionService(repo, IdentityResolver); var ex = await Assert.ThrowsAsync( () => service.ArchiveAsync("42")); Assert.Equal("Panne", ex.Message); } // ========================================================================= // ARCHITECTURE // ========================================================================= [Fact] public void Service_ImplementsIInterventionService() { Assert.True(typeof(IInterventionService).IsAssignableFrom(typeof(InterventionService))); } [Fact] public void Service_DependsOnIInterventionRepository() { var type = typeof(InterventionService); bool hasRepoParam = type.GetConstructors() .Any(c => c.GetParameters() .Any(p => p.ParameterType == typeof(IInterventionRepository))); Assert.True(hasRepoParam, "InterventionService doit dépendre de IInterventionRepository."); } [Fact] public void Service_HasNoOmikaDataReferences() { var type = typeof(InterventionService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Namespace is not null && t.Namespace.StartsWith("Omika.Data")); } [Fact] public void Service_HasNoBatikReferences() { var type = typeof(InterventionService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Name.Contains("Batik", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Service_HasNoHfsqlReferences() { var type = typeof(InterventionService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Name.Contains("Row", StringComparison.OrdinalIgnoreCase) && t.Namespace is not null && (t.Namespace.Contains("HfSql", StringComparison.OrdinalIgnoreCase) || t.Namespace.Contains("Models.Omika", StringComparison.OrdinalIgnoreCase))); } [Fact] public void Service_HasNoSqlStatements() { string source = ReadSourceFile(typeof(InterventionService)); Assert.DoesNotContain("SELECT ", source, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("FROM ", source, StringComparison.OrdinalIgnoreCase); } [Fact] public void Service_HasNoApplicationRuntimeContext() { var type = typeof(InterventionService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Name.Contains("ApplicationRuntimeContext", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Service_HasNoDatabaseSchemaVersion() { var type = typeof(InterventionService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Name.Contains("DatabaseSchemaVersion", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Service_HasNoOdbcReferences() { var type = typeof(InterventionService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Namespace is not null && t.Namespace.StartsWith("System.Data.Odbc", StringComparison.Ordinal)); } // ========================================================================= // HELPERS // ========================================================================= private static IEnumerable GetReferencedTypes(Type type) { var members = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) .Select(m => m.DeclaringType) .Where(t => t is not null) .Cast(); var parameters = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .SelectMany(m => m.GetParameters()) .Select(p => p.ParameterType); var returnTypes = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(m => m.ReturnType); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(p => p.PropertyType); var ctorParams = type.GetConstructors() .SelectMany(c => c.GetParameters()) .Select(p => p.ParameterType); return members.Concat(parameters).Concat(returnTypes).Concat(properties).Concat(ctorParams); } private static string ReadSourceFile(Type type) { string baseDir = AppDomain.CurrentDomain.BaseDirectory; var dir = new DirectoryInfo(baseDir); while (dir != null && !File.Exists(Path.Combine(dir.FullName, "Omika.slnx"))) dir = dir.Parent; string root = dir?.FullName ?? baseDir; return File.ReadAllText(Path.Combine(root, "src", "Omika.Core", "Services", type.Name + ".cs")); } // ========================================================================= // FAKE REPOSITORY // ========================================================================= private sealed class FakeInterventionRepository : IInterventionRepository { public int SearchCallCount { get; private set; } public int DetailsCallCount { get; private set; } public int UpdateCallCount { get; private set; } public int ArchiveCallCount { get; private set; } public int CreateCallCount { get; private set; } public InterventionSearchCriteria? LastSearchCriteria { get; private set; } public string? LastDetailsId { get; private set; } public string? LastUpdateId { get; private set; } public string? LastArchiveId { get; private set; } public InterventionUpdateCommand? LastUpdateCommand { get; private set; } public InterventionCreateCommand? LastCreateCommand { get; private set; } public CancellationToken LastUpdateCancellationToken { get; private set; } public CancellationToken LastArchiveCancellationToken { get; private set; } public CancellationToken LastCreateCancellationToken { get; private set; } public CancellationToken LastCancellationToken { get; private set; } public PagedResult? SearchResult { get; set; } public Intervention? DetailsResult { get; set; } public Intervention? UpdateResult { get; set; } public Intervention? CreateResult { get; set; } public bool ArchiveResult { get; set; } public Exception? ThrowOnSearch { get; set; } public Exception? ThrowOnDetails { get; set; } public Exception? ThrowOnUpdate { get; set; } public Exception? ThrowOnArchive { get; set; } public Exception? ThrowOnCreate { get; set; } public Task CreateAsync( InterventionCreateCommand command, CancellationToken cancellationToken = default) { CreateCallCount++; LastCreateCommand = command; LastCreateCancellationToken = cancellationToken; if (ThrowOnCreate is not null) throw ThrowOnCreate; return Task.FromResult(CreateResult ?? new Intervention { Titre = command.Titre }); } public Task> SearchAsync( InterventionSearchCriteria criteria, CancellationToken cancellationToken = default) { SearchCallCount++; LastSearchCriteria = criteria; LastCancellationToken = cancellationToken; if (ThrowOnSearch is not null) throw ThrowOnSearch; return Task.FromResult(SearchResult ?? new PagedResult([], 1, 20, 0, 0)); } public Task> SearchDetailsAsync( InterventionSearchCriteria criteria, CancellationToken cancellationToken = default) { SearchCallCount++; LastSearchCriteria = criteria; LastCancellationToken = cancellationToken; if (ThrowOnSearch is not null) throw ThrowOnSearch; return Task.FromResult(SearchResult ?? new PagedResult([], 1, 20, 0, 0)); } public Task GetDetailsAsync( string id, CancellationToken cancellationToken = default) { DetailsCallCount++; LastDetailsId = id; LastCancellationToken = cancellationToken; if (ThrowOnDetails is not null) throw ThrowOnDetails; return Task.FromResult(DetailsResult); } public Task UpdateAsync( string id, InterventionUpdateCommand command, CancellationToken cancellationToken = default) { UpdateCallCount++; LastUpdateId = id; LastUpdateCommand = command; LastUpdateCancellationToken = cancellationToken; if (ThrowOnUpdate is not null) throw ThrowOnUpdate; return Task.FromResult(UpdateResult); } public Task ArchiveAsync( string id, CancellationToken cancellationToken = default) { ArchiveCallCount++; LastArchiveId = id; LastArchiveCancellationToken = cancellationToken; if (ThrowOnArchive is not null) throw ThrowOnArchive; return Task.FromResult(ArchiveResult); } public Task> GetNotesAsync( long numericInterventionId, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task> GetNotesAdministrativesAsync(long numericInterventionId, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task> GetNotesForInterventionsAsync(IReadOnlyList numericInterventionIds, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task> GetEmailsForInterventionsAsync(IReadOnlyList numericInterventionIds, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task AddNoteAsync( long numericInterventionId, string contenu, long creationUser, CancellationToken cancellationToken = default) => Task.FromResult(new NoteInterneDto()); public Task> GetEmailsAsync( long numericInterventionId, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task AddEmailAsync( long numericInterventionId, string objet, string message, string? adrDestinataires, string? adrCC, string? adrCCI, IReadOnlyList piecesJointes, bool brouillon, long creationUser, CancellationToken cancellationToken = default) => Task.FromResult(new EmailInterventionDto { PiecesJointes = piecesJointes }); public Task ResolveCreationUserAsync( AuditActor? auteur, CancellationToken cancellationToken = default) => Task.FromResult(0L); } 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; } }