1034 lines
35 KiB
C#
1034 lines
35 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Tests unitaires de <see cref="ClientService"/> avec un fake
|
|
/// <see cref="IClientRepository"/> en mémoire. Aucune base réelle.
|
|
/// </summary>
|
|
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<Client>([], 1, 20, 0, 0)
|
|
};
|
|
var service = new ClientService(repo, IdentityResolver);
|
|
|
|
PagedResult<ClientSummaryDto> 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>([client], 1, 20, 1, 1)
|
|
};
|
|
var service = new ClientService(repo, IdentityResolver);
|
|
|
|
PagedResult<ClientSummaryDto> 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<Client>(clients, 1, 20, 3, 1)
|
|
};
|
|
var service = new ClientService(repo, IdentityResolver);
|
|
|
|
PagedResult<ClientSummaryDto> 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<Client>([], 2, 10, 42, 5)
|
|
};
|
|
var service = new ClientService(repo, IdentityResolver);
|
|
|
|
PagedResult<ClientSummaryDto> 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>([client], 1, 20, 1, 1)
|
|
};
|
|
var service = new ClientService(repo, IdentityResolver);
|
|
|
|
PagedResult<ClientSummaryDto> 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<Client>(
|
|
[new Client { Nom = "A" }, new Client { Nom = "B" }], 3, 15, 100, 7)
|
|
};
|
|
var service = new ClientService(repo, IdentityResolver);
|
|
|
|
PagedResult<ClientSummaryDto> 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<InvalidOperationException>(
|
|
() => 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<InvalidOperationException>(
|
|
() => 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<InvalidOperationException>(
|
|
() => 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<InvalidOperationException>(
|
|
() => 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<Type> 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<Type>();
|
|
|
|
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<Client>? 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<Client> 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<PagedResult<Client>> SearchAsync(
|
|
|
|
ClientSearchCriteria criteria,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
SearchCallCount++;
|
|
LastSearchCriteria = criteria;
|
|
LastCancellationToken = cancellationToken;
|
|
|
|
if (ThrowOnSearch is not null)
|
|
throw ThrowOnSearch;
|
|
|
|
return Task.FromResult(SearchResult
|
|
?? new PagedResult<Client>([], 1, 20, 0, 0));
|
|
}
|
|
|
|
public Task<PagedResult<Client>> SearchDetailsAsync(
|
|
ClientSearchCriteria criteria,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
SearchCallCount++;
|
|
LastSearchCriteria = criteria;
|
|
LastCancellationToken = cancellationToken;
|
|
|
|
if (ThrowOnSearch is not null)
|
|
throw ThrowOnSearch;
|
|
|
|
return Task.FromResult(SearchResult
|
|
?? new PagedResult<Client>([], 1, 20, 0, 0));
|
|
}
|
|
|
|
public Task<Client?> GetDetailsAsync(
|
|
|
|
string id,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
DetailsCallCount++;
|
|
LastDetailsId = id;
|
|
LastCancellationToken = cancellationToken;
|
|
|
|
if (ThrowOnDetails is not null)
|
|
throw ThrowOnDetails;
|
|
|
|
return Task.FromResult(DetailsResult);
|
|
}
|
|
|
|
public Task<Client?> 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<bool> 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;
|
|
}
|
|
}
|