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

1298 lines
46 KiB
C#

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;
/// <summary>
/// Tests unitaires de <see cref="InterventionService"/> avec un fake
/// <see cref="IInterventionRepository"/> en mémoire. Aucune base réelle.
/// </summary>
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<Intervention>([], 1, 20, 0, 0)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionSummaryDto> 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>([intervention], 1, 20, 1, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionSummaryDto> 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<Intervention>(interventions, 1, 20, 3, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionSummaryDto> 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<Intervention>([], 2, 10, 42, 5)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionSummaryDto> 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>([intervention], 1, 20, 1, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionSummaryDto> 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<Intervention>(
[new Intervention { Titre = "A" }, new Intervention { Titre = "B" }], 3, 15, 100, 7)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionSummaryDto> 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<Intervention>([], 1, 20, 0, 0)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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<Intervention>(interventions, 1, 20, 3, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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<Intervention>([], 2, 10, 42, 5)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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>([intervention], 1, 20, 1, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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>([intervention], 1, 20, 1, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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>([intervention], 1, 20, 1, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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>([intervention], 1, 20, 1, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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>([intervention], 1, 20, 1, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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<Intervention>(
[new Intervention { Titre = "A" }, new Intervention { Titre = "B" }], 1, 20, 2, 1)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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<Intervention>(
[new Intervention { Titre = "A" }, new Intervention { Titre = "B" }], 3, 15, 100, 7)
};
var service = new InterventionService(repo, IdentityResolver);
PagedResult<InterventionDetailsDto> 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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => 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<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 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<Intervention>? 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<Intervention> 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<PagedResult<Intervention>> SearchAsync(
InterventionSearchCriteria criteria,
CancellationToken cancellationToken = default)
{
SearchCallCount++;
LastSearchCriteria = criteria;
LastCancellationToken = cancellationToken;
if (ThrowOnSearch is not null)
throw ThrowOnSearch;
return Task.FromResult(SearchResult
?? new PagedResult<Intervention>([], 1, 20, 0, 0));
}
public Task<PagedResult<Intervention>> SearchDetailsAsync(
InterventionSearchCriteria criteria,
CancellationToken cancellationToken = default)
{
SearchCallCount++;
LastSearchCriteria = criteria;
LastCancellationToken = cancellationToken;
if (ThrowOnSearch is not null)
throw ThrowOnSearch;
return Task.FromResult(SearchResult
?? new PagedResult<Intervention>([], 1, 20, 0, 0));
}
public Task<Intervention?> GetDetailsAsync(
string id,
CancellationToken cancellationToken = default)
{
DetailsCallCount++;
LastDetailsId = id;
LastCancellationToken = cancellationToken;
if (ThrowOnDetails is not null)
throw ThrowOnDetails;
return Task.FromResult(DetailsResult);
}
public Task<Intervention?> 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<bool> ArchiveAsync(
string id,
CancellationToken cancellationToken = default)
{
ArchiveCallCount++;
LastArchiveId = id;
LastArchiveCancellationToken = cancellationToken;
if (ThrowOnArchive is not null)
throw ThrowOnArchive;
return Task.FromResult(ArchiveResult);
}
public Task<IReadOnlyList<NoteInterneDto>> GetNotesAsync(
long numericInterventionId,
CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<NoteInterneDto>>([]);
public Task<IReadOnlyList<NoteInterneDto>> GetNotesAdministrativesAsync(long numericInterventionId, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<NoteInterneDto>>([]);
public Task<IReadOnlyList<InterventionNoteBulkRow>> GetNotesForInterventionsAsync(IReadOnlyList<long> numericInterventionIds, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<InterventionNoteBulkRow>>([]);
public Task<IReadOnlyList<InterventionEmailBulkRow>> GetEmailsForInterventionsAsync(IReadOnlyList<long> numericInterventionIds, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<InterventionEmailBulkRow>>([]);
public Task<NoteInterneDto> AddNoteAsync(
long numericInterventionId,
string contenu,
long creationUser,
CancellationToken cancellationToken = default)
=> Task.FromResult(new NoteInterneDto());
public Task<IReadOnlyList<EmailInterventionDto>> GetEmailsAsync(
long numericInterventionId, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<EmailInterventionDto>>([]);
public Task<EmailInterventionDto> AddEmailAsync(
long numericInterventionId, string objet, string message, string? adrDestinataires, string? adrCC, string? adrCCI, IReadOnlyList<string> piecesJointes, bool brouillon, long creationUser, CancellationToken cancellationToken = default)
=> Task.FromResult(new EmailInterventionDto { PiecesJointes = piecesJointes });
public Task<long> 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;
}
}