using System.Reflection; using Omika.Core.Contracts.Clients; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Mobile.Sync; using Omika.Core.Domain.Commands; using Omika.Core.Domain.Entities.Clients; using Omika.Core.Domain.ValueObjects; using Omika.Core.Interfaces; using Omika.Core.Interfaces.Mobile; using Omika.Core.Mapping; using Omika.Core.Services; namespace Omika.Tests.Services; /// /// Tests unitaires de avec un fake /// en mémoire. Aucune base réelle. /// public class ClientServiceTests { private static readonly IMobileEntityIdentityResolver IdentityResolver = new FakeIdentityResolver(); // ========================================================================= // SEARCH // ========================================================================= [Fact] public async Task SearchAsync_CallsRepositoryOnce() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest()); Assert.Equal(1, repo.SearchCallCount); } [Fact] public async Task SearchAsync_TransmitsPage() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { Page = 3 }); Assert.Equal(3, repo.LastSearchCriteria?.Page); } [Fact] public async Task SearchAsync_TransmitsPageSize() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { PageSize = 50 }); Assert.Equal(50, repo.LastSearchCriteria?.PageSize); } [Fact] public async Task SearchAsync_TransmitsSearch() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { Search = "test" }); Assert.Equal("test", repo.LastSearchCriteria?.Search); } [Fact] public async Task SearchAsync_TransmitsNom() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { Nom = "Dupont" }); Assert.Equal("Dupont", repo.LastSearchCriteria?.Nom); } [Fact] public async Task SearchAsync_TransmitsCode() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { Code = "C001" }); Assert.Equal("C001", repo.LastSearchCriteria?.Code); } [Fact] public async Task SearchAsync_TransmitsVille() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { Ville = "Paris" }); Assert.Equal("Paris", repo.LastSearchCriteria?.Ville); } [Fact] public async Task SearchAsync_TransmitsType() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { Type = "Entreprise" }); Assert.Equal("Entreprise", repo.LastSearchCriteria?.Type); } [Fact] public async Task SearchAsync_TransmitsStatut() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { Statut = "Actif" }); Assert.Equal("Actif", repo.LastSearchCriteria?.Statut); } [Fact] public async Task SearchAsync_TransmitsEstArchive() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { EstArchive = true }); Assert.True(repo.LastSearchCriteria?.EstArchive); } [Fact] public async Task SearchAsync_TransmitsEstBloque() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { EstBloque = true }); Assert.True(repo.LastSearchCriteria?.EstBloque); } [Fact] public async Task SearchAsync_TransmitsSortBy() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { SortBy = "Code" }); Assert.Equal("Code", repo.LastSearchCriteria?.SortBy); } [Fact] public async Task SearchAsync_TransmitsSortDescending() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.SearchAsync(new SearchClientsRequest { SortDescending = true }); Assert.True(repo.LastSearchCriteria?.SortDescending); } [Fact] public async Task SearchAsync_TransmitsCancellationToken() { var repo = new FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.SearchAsync(new SearchClientsRequest(), cts.Token); Assert.Equal(cts.Token, repo.LastCancellationToken); } [Fact] public async Task SearchAsync_EmptyResult_ReturnsEmptyPage() { var repo = new FakeClientRepository { SearchResult = new PagedResult([], 1, 20, 0, 0) }; var service = new ClientService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchClientsRequest()); 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_SingleClient_MapsToSummaryDto() { var client = new Client { Code = "C001", Nom = "Dupont", Type = new ReferenceValue("Entreprise") { Key = "ENTREPRISE" }, Statut = new ReferenceValue("Actif") { Key = "ACTIF" }, AdressePrincipale = new Adresse { AdressePostale = new PostalAddress(City: "Paris") }, Coordonnees = new ContactDetails(Phone: "0102030405"), EstBloque = false, EstArchive = false }; var repo = new FakeClientRepository { SearchResult = new PagedResult([client], 1, 20, 1, 1) }; var service = new ClientService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchClientsRequest()); ClientSummaryDto dto = Assert.Single(result.Items); Assert.Equal("C001", dto.Code); Assert.Equal("Dupont", dto.Nom); Assert.Equal("Entreprise", dto.Type); Assert.Equal("Actif", dto.Statut); Assert.Equal("Paris", dto.Ville); Assert.Equal("0102030405", dto.Telephone); Assert.False(dto.EstBloque); Assert.False(dto.EstArchive); } [Fact] public async Task SearchAsync_MultipleClients_MapsAllToDtos() { var clients = new[] { new Client { Nom = "Alpha", Code = "A" }, new Client { Nom = "Beta", Code = "B" }, new Client { Nom = "Gamma", Code = "C" } }; var repo = new FakeClientRepository { SearchResult = new PagedResult(clients, 1, 20, 3, 1) }; var service = new ClientService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchClientsRequest()); Assert.Equal(3, result.Items.Count); Assert.Equal(3, result.TotalCount); Assert.Equal("Alpha", result.Items[0].Nom); Assert.Equal("Beta", result.Items[1].Nom); Assert.Equal("Gamma", result.Items[2].Nom); } [Fact] public async Task SearchAsync_PreservesTotalCount() { var repo = new FakeClientRepository { SearchResult = new PagedResult([], 2, 10, 42, 5) }; var service = new ClientService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchClientsRequest()); 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_IdComesFromClientContractMapper() { var client = new Client { Nom = "Test" }; var expectedId = client.Id.ToString(); var repo = new FakeClientRepository { SearchResult = new PagedResult([client], 1, 20, 1, 1) }; var service = new ClientService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchClientsRequest()); Assert.Equal(expectedId, result.Items[0].Id); } [Fact] public async Task SearchAsync_ServiceDoesNotRePaginate() { // Le service ne doit pas altérer la pagination du repository. var repo = new FakeClientRepository { SearchResult = new PagedResult( [new Client { Nom = "A" }, new Client { Nom = "B" }], 3, 15, 100, 7) }; var service = new ClientService(repo, IdentityResolver); PagedResult result = await service.SearchAsync( new SearchClientsRequest { Page = 1, PageSize = 5 }); // Le repo reçoit la demande mais retourne ce qu'il veut. // Le service doit préserver le résultat du repo, pas refaire la pagination. 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 FakeClientRepository(); var service = new ClientService(repo, IdentityResolver); await service.GetByIdAsync("test-id-123"); Assert.Equal("test-id-123", repo.LastDetailsId); } [Fact] public async Task GetByIdAsync_TransmitsCancellationToken() { var repo = new FakeClientRepository(); var service = new ClientService(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 FakeClientRepository { DetailsResult = null }; var service = new ClientService(repo, IdentityResolver); ClientDetailsDto? result = await service.GetByIdAsync("999"); Assert.Null(result); } [Fact] public async Task GetByIdAsync_MapsToDetailsDto() { var client = new Client { Code = "C001", Nom = "Dupont", Type = new ReferenceValue("Entreprise") { Key = "ENTREPRISE" }, Statut = new ReferenceValue("Actif") { Key = "ACTIF" }, EstBloque = false, EstArchive = false }; var repo = new FakeClientRepository { DetailsResult = client }; var service = new ClientService(repo, IdentityResolver); ClientDetailsDto? result = await service.GetByIdAsync("42"); Assert.NotNull(result); Assert.Equal("C001", result.Code); Assert.Equal("Dupont", result.Nom); Assert.Equal("Entreprise", result.Type?.Label); Assert.Equal("Actif", result.Statut?.Label); } [Fact] public async Task GetByIdAsync_AdressesMapped() { var client = new Client { Nom = "Test" }; client.AddAdresse(new Adresse { Etiquette = "Siège", AdressePostale = new PostalAddress(City: "Paris"), EstPrincipale = true }); var repo = new FakeClientRepository { DetailsResult = client }; var service = new ClientService(repo, IdentityResolver); ClientDetailsDto? result = await service.GetByIdAsync("42"); Assert.NotNull(result); Assert.Single(result.Adresses); Assert.Equal("Siège", result.Adresses[0].Etiquette); Assert.Equal("Paris", result.Adresses[0].AdressePostale?.City); } [Fact] public async Task GetByIdAsync_ContactsMapped() { var client = new Client { Nom = "Test" }; client.AddContact(new Contact { Nom = "Martin", Prenom = "Jean" }); var repo = new FakeClientRepository { DetailsResult = client }; var service = new ClientService(repo, IdentityResolver); ClientDetailsDto? result = await service.GetByIdAsync("42"); Assert.NotNull(result); Assert.Single(result.Contacts); Assert.Equal("Martin", result.Contacts[0].Nom); Assert.Equal("Jean Martin", result.Contacts[0].NomAffiche); } [Fact] public async Task GetByIdAsync_SitesMapped() { var client = new Client { Nom = "Test" }; client.AddSite(new Site { Code = "S1", Nom = "Site 1" }); var repo = new FakeClientRepository { DetailsResult = client }; var service = new ClientService(repo, IdentityResolver); ClientDetailsDto? result = await service.GetByIdAsync("42"); Assert.NotNull(result); Assert.Single(result.Sites); Assert.Equal("S1", result.Sites[0].Code); } [Fact] public async Task GetByIdAsync_IdComesFromMapper() { var client = new Client { Nom = "Test" }; var expectedId = client.Id.ToString(); var repo = new FakeClientRepository { DetailsResult = client }; var service = new ClientService(repo, IdentityResolver); ClientDetailsDto? result = await service.GetByIdAsync("42"); Assert.NotNull(result); Assert.Equal(expectedId, result.Id); } [Fact] public async Task GetByIdAsync_NoGuidParsingInService() { // Le service transmet l'id tel quel, sans parsing. var repo = new FakeClientRepository(); var service = new ClientService(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 FakeClientRepository { ThrowOnDetails = new InvalidOperationException("Infrastructure error") }; var service = new ClientService(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 FakeClientRepository { CreateResult = new Client { Nom = "Nouveau", Code = "C001" } }; var service = new ClientService(repo, IdentityResolver); await service.CreateAsync(new CreateClientRequest { Nom = "Nouveau" }); Assert.Equal(1, repo.CreateCallCount); } [Fact] public async Task CreateAsync_MapsAllProperties() { var repo = new FakeClientRepository { CreateResult = new Client { Nom = "Nouveau" } }; var service = new ClientService(repo, IdentityResolver); await service.CreateAsync(new CreateClientRequest { Code = "C001", Nom = "Nouveau", Type = new ReferenceValueDto { Key = "ENT" }, Statut = new ReferenceValueDto { Key = "ACT" }, FormeJuridique = new ReferenceValueDto { Key = "SARL" }, CodeTva = "FR123", CodeComptable = "411", Siren = "123456789", Siret = "12345678900014", Pays = "FR", Coordonnees = new ContactDetailsDto { Email = "a@b.c", Phone = "0102" }, InformationsComplementaires = "Info sup", AdressePrincipale = new AddressDto { Label = "Etiquette", Line1 = "1 rue", PostalCode = "75001", City = "Paris" } }); Assert.Equal("C001", repo.LastCreateCommand!.Code); Assert.Equal("Nouveau", repo.LastCreateCommand!.Nom); Assert.Equal("ENT", repo.LastCreateCommand!.TypeKey); Assert.Equal("ACT", repo.LastCreateCommand!.StatutKey); Assert.Equal("SARL", repo.LastCreateCommand!.FormeJuridique); Assert.Equal("FR123", repo.LastCreateCommand!.CodeTva); Assert.Equal("411", repo.LastCreateCommand!.CodeComptable); Assert.Equal("123456789", repo.LastCreateCommand!.Siren); Assert.Equal("12345678900014", repo.LastCreateCommand!.Siret); Assert.Equal("FR", repo.LastCreateCommand!.Pays); Assert.Equal("a@b.c", repo.LastCreateCommand!.Email); Assert.Equal("0102", repo.LastCreateCommand!.Telephone); Assert.Equal("Info sup", repo.LastCreateCommand!.InformationsComplementaires); Assert.Equal("Etiquette", repo.LastCreateCommand!.AdresseEtiquette); Assert.Equal("1 rue", repo.LastCreateCommand!.AdresseLigne1); Assert.Equal("75001", repo.LastCreateCommand!.AdresseCodePostal); Assert.Equal("Paris", repo.LastCreateCommand!.AdresseVille); } [Fact] public async Task CreateAsync_ResultMappedToDetails() { var repo = new FakeClientRepository { CreateResult = new Client { Nom = "Nouveau", Code = "C001" } }; var service = new ClientService(repo, IdentityResolver); ClientDetailsDto result = await service.CreateAsync(new CreateClientRequest { Nom = "Nouveau" }); Assert.Equal("Nouveau", result.Nom); Assert.Equal("C001", result.Code); } [Fact] public async Task CreateAsync_CancellationTokenTransmitted() { var repo = new FakeClientRepository { CreateResult = new Client { Nom = "Nouveau" } }; var service = new ClientService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.CreateAsync(new CreateClientRequest { Nom = "Nouveau" }, cts.Token); Assert.Equal(cts.Token, repo.LastCreateCancellationToken); } [Fact] public async Task CreateAsync_ExceptionPropagated() { var repo = new FakeClientRepository { ThrowOnCreate = new InvalidOperationException("Panne") }; var service = new ClientService(repo, IdentityResolver); var ex = await Assert.ThrowsAsync( () => service.CreateAsync(new CreateClientRequest { Nom = "Nouveau" })); Assert.Equal("Panne", ex.Message); } // ========================================================================= // UPDATE // ========================================================================= [Fact] public async Task UpdateAsync_MapsAllProperties() { var repo = new FakeClientRepository { UpdateResult = new Client { Nom = "Updated", Code = "C002" } }; var service = new ClientService(repo, IdentityResolver); await service.UpdateAsync("42", new UpdateClientRequest { Code = "C002", Nom = "Updated", Type = new ReferenceValueDto { Key = "ENT" }, Statut = new ReferenceValueDto { Key = "ACT" }, FormeJuridique = new ReferenceValueDto { Key = "SARL" }, CodeTva = "FR123", CodeComptable = "411", Siren = "123456789", Siret = "12345678900014", Pays = "FR", Coordonnees = new ContactDetailsDto { Email = "a@b.c", Phone = "0102" }, InformationsComplementaires = "Info sup", EstBloque = true }); Assert.Equal(1, repo.UpdateCallCount); Assert.Equal("42", repo.LastUpdateId); Assert.Equal("C002", repo.LastUpdateCommand!.Code); Assert.Equal("Updated", repo.LastUpdateCommand!.Nom); Assert.Equal("ENT", repo.LastUpdateCommand!.TypeKey); Assert.Equal("ACT", repo.LastUpdateCommand!.StatutKey); Assert.Equal("SARL", repo.LastUpdateCommand!.FormeJuridique); Assert.Equal("FR123", repo.LastUpdateCommand!.CodeTva); Assert.Equal("411", repo.LastUpdateCommand!.CodeComptable); Assert.Equal("123456789", repo.LastUpdateCommand!.Siren); Assert.Equal("12345678900014", repo.LastUpdateCommand!.Siret); Assert.Equal("FR", repo.LastUpdateCommand!.Pays); Assert.Equal("a@b.c", repo.LastUpdateCommand!.Email); Assert.Equal("0102", repo.LastUpdateCommand!.Telephone); Assert.Equal("Info sup", repo.LastUpdateCommand!.InformationsComplementaires); Assert.True(repo.LastUpdateCommand!.EstBloque); } [Fact] public async Task UpdateAsync_NullsPreserved() { var repo = new FakeClientRepository { UpdateResult = new Client { Nom = "X" } }; var service = new ClientService(repo, IdentityResolver); await service.UpdateAsync("42", new UpdateClientRequest()); Assert.Null(repo.LastUpdateCommand!.TypeKey); Assert.Null(repo.LastUpdateCommand!.StatutKey); Assert.Null(repo.LastUpdateCommand!.FormeJuridique); Assert.Null(repo.LastUpdateCommand!.Email); Assert.Null(repo.LastUpdateCommand!.Telephone); } [Fact] public async Task UpdateAsync_RepoReturnsNull_ReturnsNull() { var repo = new FakeClientRepository { UpdateResult = null }; var service = new ClientService(repo, IdentityResolver); ClientDetailsDto? result = await service.UpdateAsync("42", new UpdateClientRequest()); Assert.Null(result); } [Fact] public async Task UpdateAsync_IdTransmittedExactly() { var repo = new FakeClientRepository { UpdateResult = new Client { Nom = "XY" } }; var service = new ClientService(repo, IdentityResolver); await service.UpdateAsync("my-guid-123", new UpdateClientRequest()); Assert.Equal("my-guid-123", repo.LastUpdateId); } [Fact] public async Task UpdateAsync_CancellationTokenTransmitted() { var repo = new FakeClientRepository { UpdateResult = new Client { Nom = "X" } }; var service = new ClientService(repo, IdentityResolver); using var cts = new CancellationTokenSource(); await service.UpdateAsync("42", new UpdateClientRequest(), cts.Token); Assert.Equal(cts.Token, repo.LastUpdateCancellationToken); } [Fact] public async Task UpdateAsync_ExceptionPropagated() { var repo = new FakeClientRepository { ThrowOnUpdate = new InvalidOperationException("Panne") }; var service = new ClientService(repo, IdentityResolver); var ex = await Assert.ThrowsAsync( () => service.UpdateAsync("42", new UpdateClientRequest())); Assert.Equal("Panne", ex.Message); } // ========================================================================= // ARCHIVE // ========================================================================= [Fact] public async Task ArchiveAsync_TruePropagated() { var repo = new FakeClientRepository { ArchiveResult = true }; var service = new ClientService(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 FakeClientRepository { ArchiveResult = false }; var service = new ClientService(repo, IdentityResolver); bool result = await service.ArchiveAsync("42"); Assert.False(result); } [Fact] public async Task ArchiveAsync_IdTransmittedExactly() { var repo = new FakeClientRepository { ArchiveResult = true }; var service = new ClientService(repo, IdentityResolver); await service.ArchiveAsync("my-guid-123"); Assert.Equal("my-guid-123", repo.LastArchiveId); } [Fact] public async Task ArchiveAsync_CancellationTokenTransmitted() { var repo = new FakeClientRepository { ArchiveResult = true }; var service = new ClientService(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 FakeClientRepository { ThrowOnArchive = new InvalidOperationException("Panne") }; var service = new ClientService(repo, IdentityResolver); var ex = await Assert.ThrowsAsync( () => service.ArchiveAsync("42")); Assert.Equal("Panne", ex.Message); } // ========================================================================= // ARCHITECTURE // ========================================================================= [Fact] public void Service_ImplementsIClientService() { Assert.True(typeof(IClientService).IsAssignableFrom(typeof(ClientService))); } [Fact] public void Service_DependsOnIClientRepository() { var type = typeof(ClientService); bool hasRepoParam = type.GetConstructors() .Any(c => c.GetParameters() .Any(p => p.ParameterType == typeof(IClientRepository))); Assert.True(hasRepoParam, "ClientService doit dépendre de IClientRepository."); } [Fact] public void Service_HasNoOmikaDataReferences() { var type = typeof(ClientService); 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(ClientService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Name.Contains("Batik", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Service_HasNoHfsqlReferences() { var type = typeof(ClientService); 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(ClientService)); Assert.DoesNotContain("SELECT ", source, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("FROM ", source, StringComparison.OrdinalIgnoreCase); } [Fact] public void Service_HasNoApplicationRuntimeContext() { var type = typeof(ClientService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Name.Contains("ApplicationRuntimeContext", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Service_HasNoDatabaseSchemaVersion() { var type = typeof(ClientService); var refs = GetReferencedTypes(type); Assert.DoesNotContain(refs, t => t.Name.Contains("DatabaseSchemaVersion", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Service_HasNoOdbcReferences() { var type = typeof(ClientService); 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 FakeClientRepository : IClientRepository { 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 ClientSearchCriteria? LastSearchCriteria { get; private set; } public string? LastDetailsId { get; private set; } public string? LastUpdateId { get; private set; } public string? LastArchiveId { get; private set; } public ClientUpdateCommand? LastUpdateCommand { get; private set; } public ClientCreateCommand? 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 Client? DetailsResult { get; set; } public Client? UpdateResult { get; set; } public Client? 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( ClientCreateCommand command, CancellationToken cancellationToken = default) { CreateCallCount++; LastCreateCommand = command; LastCreateCancellationToken = cancellationToken; if (ThrowOnCreate is not null) throw ThrowOnCreate; return Task.FromResult(CreateResult ?? new Client { Nom = command.Nom }); } public Task> SearchAsync( ClientSearchCriteria 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( ClientSearchCriteria 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, ClientUpdateCommand 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); } } 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; } }