using System.Net.Http.Json;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Omika.Core.Abstractions;
using Omika.Core.Compatibility;
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Documents;
using Omika.Core.Contracts.Interventions;
using Omika.Core.Contracts.Mobile.Interventions;
using Omika.Core.Contracts.Planning;
using Omika.Core.Interfaces;
using Omika.Core.Interfaces.Mobile;
using Omika.Core.Runtime;
using Omika.Data.Configuration;
using Omika.Data.Connection;
using Omika.Data.HfSql.Metadata;
using Omika.Data.Runtime;
using Omika.Core.Domain;
namespace Omika.Tests.Api;
///
/// Tests HTTP des endpoints Intervention. Utilise
/// avec un fake (pas de HFSQL)
/// et un fake pour contrôler les réponses.
///
/// Les tests modifient le contexte statique ,
/// donc ils sont sérialisés via la collection "ApplicationRuntimeContext".
///
[Collection("ApplicationRuntimeContext")]
public class InterventionEndpointsTests : IDisposable
{
private readonly FakeInterventionService _fakeService = new();
private readonly FakeMobileInterventionSearchService _fakeMobileSearch = new();
private readonly WebApplicationFactory _factory;
public InterventionEndpointsTests()
{
ApplicationRuntimeContext.Reset();
_factory = new WebApplicationFactory()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary
{
["Hfsql:Server"] = "test-server",
["Hfsql:DatabaseName"] = "test-db"
})
.Build();
services.AddSingleton(config);
services.RemoveAll();
services.AddScoped();
services.RemoveAll();
services.AddScoped();
services.RemoveAll();
services.AddScoped(_ => _fakeService);
services.RemoveAll();
services.AddScoped(_ => _fakeMobileSearch);
});
});
}
public void Dispose()
{
_factory.Dispose();
ApplicationRuntimeContext.Reset();
}
// =========================================================================
// GET /interventions/{id}
// =========================================================================
[Fact]
public async Task GetInterventionById_NumericId_TransmittedAsString()
{
var client = _factory.CreateClient();
await client.GetAsync("/interventions/12345");
Assert.Equal("12345", _fakeService.LastDetailsId);
}
[Fact]
public async Task GetInterventionById_GuidId_TransmittedAsString()
{
var client = _factory.CreateClient();
var guid = Guid.NewGuid().ToString("D");
await client.GetAsync($"/interventions/{guid}");
Assert.Equal(guid, _fakeService.LastDetailsId);
}
[Fact]
public async Task GetInterventionById_Returns200WithDto()
{
_fakeService.DetailsResult = new InterventionDetailsDto
{
Id = "test-id",
Reference = "INT-001",
Titre = "Réparation"
};
var client = _factory.CreateClient();
var response = await client.GetAsync("/interventions/42");
var body = await response.Content.ReadFromJsonAsync();
Assert.Equal(200, (int)response.StatusCode);
Assert.NotNull(body);
Assert.Equal("test-id", body.Id);
Assert.Equal("INT-001", body.Reference);
Assert.Equal("Réparation", body.Titre);
}
[Fact]
public async Task GetInterventionById_NullResult_Returns404()
{
_fakeService.DetailsResult = null;
var client = _factory.CreateClient();
var response = await client.GetAsync("/interventions/999");
Assert.Equal(404, (int)response.StatusCode);
}
[Fact]
public async Task GetInterventionById_ServiceException_NotTransformedTo404()
{
_fakeService.ThrowOnDetails = new InvalidOperationException("Infrastructure error");
var client = _factory.CreateClient();
var response = await client.GetAsync("/interventions/42");
// L'exception n'est pas catchée dans le endpoint → 500.
Assert.Equal(500, (int)response.StatusCode);
}
// =========================================================================
// HEALTH
// =========================================================================
[Fact]
public async Task GetHealth_StillResponds()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(200, (int)response.StatusCode);
}
// =========================================================================
// MOBILE SEARCH / BY-IDS — route enregistrée sous /api/interventions
// =========================================================================
[Fact]
public async Task MobileSearch_RouteRegistered_Returns401WithoutToken_Not404()
{
var client = _factory.CreateClient();
// Sans token, l'endpoint authentifié renvoie 401. S'il renvoyait 404,
// c'est que la route /api/interventions/search n'est pas enregistrée.
var response = await client.PostAsJsonAsync(
"/api/interventions/search", new MobileInterventionSearchRequest());
Assert.Equal(401, (int)response.StatusCode);
Assert.Equal(0, _fakeMobileSearch.SearchCallCount);
}
[Fact]
public async Task MobileByIds_RouteRegistered_Returns401WithoutToken_Not404()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync(
"/api/interventions/by-ids", new MobileInterventionsByIdsRequest { Ids = [] });
Assert.Equal(401, (int)response.StatusCode);
Assert.Equal(0, _fakeMobileSearch.GetByIdsCallCount);
}
// =========================================================================
// ARCHITECTURE
// =========================================================================
[Fact]
public void Endpoints_DependsOnlyOnIInterventionService()
{
var type = typeof(Omika.Api.Endpoints.InterventionEndpoints);
var methods = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Static);
foreach (var method in methods)
{
var parameters = method.GetParameters();
Assert.DoesNotContain(parameters, p =>
p.ParameterType.Name.Contains("Repository", StringComparison.OrdinalIgnoreCase)
&& p.ParameterType.Name != "IInterventionService");
}
}
[Fact]
public void Endpoints_HasNoOmikaDataReferences()
{
var type = typeof(Omika.Api.Endpoints.InterventionEndpoints);
var refs = GetReferencedNamespaces(type);
Assert.DoesNotContain(refs, ns => ns.StartsWith("Omika.Data"));
}
[Fact]
public void Endpoints_HasNoApplicationRuntimeContext()
{
var type = typeof(Omika.Api.Endpoints.InterventionEndpoints);
var refs = GetReferencedNamespaces(type);
Assert.DoesNotContain(refs, ns =>
ns.Contains("ApplicationRuntimeContext", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Endpoints_HasNoHfsqlRowReferences()
{
var type = typeof(Omika.Api.Endpoints.InterventionEndpoints);
var refs = GetReferencedNamespaces(type);
Assert.DoesNotContain(refs, ns =>
ns.Contains("HfSql", StringComparison.OrdinalIgnoreCase));
}
// =========================================================================
// HELPERS
// =========================================================================
private static IEnumerable GetReferencedNamespaces(Type type)
{
var ns = new HashSet();
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
{
if (method.DeclaringType?.Namespace is not null)
ns.Add(method.DeclaringType.Namespace);
if (method.ReturnType.Namespace is not null)
ns.Add(method.ReturnType.Namespace);
foreach (var p in method.GetParameters())
{
if (p.ParameterType.Namespace is not null)
ns.Add(p.ParameterType.Namespace);
}
}
return ns;
}
// =========================================================================
// FAKES
// =========================================================================
private sealed class FakeRuntimeInitializer : IDatabaseRuntimeInitializer
{
public Task InitializeAsync(CancellationToken cancellationToken = default)
{
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
return Task.FromResult(Result.Success());
}
}
private sealed class FakeInterventionService : IInterventionService
{
public int SearchCallCount { get; private set; }
public int DetailsCallCount { get; private set; }
public int CreateCallCount { get; private set; }
public int UpdateCallCount { get; private set; }
public int ArchiveCallCount { get; private set; }
public SearchInterventionsRequest? LastRequest { get; private set; }
public string? LastDetailsId { get; private set; }
public CreateInterventionRequest? LastCreateRequest { get; private set; }
public string? LastUpdateId { get; private set; }
public UpdateInterventionRequest? LastUpdateRequest { get; private set; }
public string? LastArchiveId { get; private set; }
public PagedResult? SearchResult { get; set; }
public InterventionDetailsDto? DetailsResult { get; set; }
public InterventionDetailsDto? CreateResult { get; set; }
public InterventionDetailsDto? UpdateResult { get; set; }
public bool ArchiveResult { get; set; }
public Exception? ThrowOnSearch { get; set; }
public Exception? ThrowOnDetails { get; set; }
public Exception? ThrowOnCreate { get; set; }
public Exception? ThrowOnUpdate { get; set; }
public Exception? ThrowOnArchive { get; set; }
public Task> SearchAsync(
SearchInterventionsRequest request, CancellationToken cancellationToken = default)
{
SearchCallCount++;
LastRequest = request;
if (ThrowOnSearch is not null) throw ThrowOnSearch;
return Task.FromResult(SearchResult
?? new PagedResult([], 1, 20, 0, 0));
}
public Task> SearchDetailsAsync(
SearchInterventionsRequest request, CancellationToken cancellationToken = default)
{
SearchCallCount++;
LastRequest = request;
if (ThrowOnSearch is not null) throw ThrowOnSearch;
return Task.FromResult(new PagedResult(
DetailsResult is null ? [] : [DetailsResult], 1, 20, DetailsResult is null ? 0 : 1, 1));
}
public Task GetByIdAsync(
string id, CancellationToken cancellationToken = default)
{
DetailsCallCount++;
LastDetailsId = id;
if (ThrowOnDetails is not null) throw ThrowOnDetails;
return Task.FromResult(DetailsResult);
}
public Task CreateAsync(
CreateInterventionRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
{
CreateCallCount++;
LastCreateRequest = request;
if (ThrowOnCreate is not null) throw ThrowOnCreate;
return Task.FromResult(CreateResult
?? new InterventionDetailsDto { Id = "new" });
}
public Task UpdateAsync(
string id, UpdateInterventionRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null)
{
UpdateCallCount++;
LastUpdateId = id;
LastUpdateRequest = request;
if (ThrowOnUpdate is not null) throw ThrowOnUpdate;
return Task.FromResult(UpdateResult);
}
public Task ArchiveAsync(string id, CancellationToken cancellationToken = default)
{
ArchiveCallCount++;
LastArchiveId = id;
if (ThrowOnArchive is not null) throw ThrowOnArchive;
return Task.FromResult(ArchiveResult);
}
public Task> GetNotesAsync(
string interventionId, CancellationToken cancellationToken = default)
=> Task.FromResult>([]);
public Task> GetNotesAdministrativesAsync(string interventionId, CancellationToken cancellationToken = default)
=> Task.FromResult>([]);
public Task AddNoteAsync(
string interventionId, string contenu, AuditActor? auteur, CancellationToken cancellationToken = default)
=> Task.FromResult(new NoteInterneDto());
public Task> GetEmailsAsync(
string interventionId, CancellationToken cancellationToken = default)
=> Task.FromResult>([]);
public Task AddEmailAsync(
string interventionId, CreateEmailInterventionRequest request, AuditActor? auteur, CancellationToken cancellationToken = default)
=> Task.FromResult(new EmailInterventionDto());
}
private sealed class FakeMobileInterventionSearchService : IMobileInterventionSearchService
{
public int SearchCallCount { get; private set; }
public int GetByIdsCallCount { get; private set; }
public Task> SearchAsync(MobileInterventionSearchRequest request, MobileTokenIdentity? identity, IReadOnlyDictionary permissions, CancellationToken cancellationToken = default)
=> Task.FromResult>([]);
public Task> GetByIdsAsync(
IReadOnlyList ids, CancellationToken cancellationToken = default)
{
GetByIdsCallCount++;
return Task.FromResult>([]);
}
public Task> GetRendezVousAsync(
string interventionId, CancellationToken cancellationToken = default)
=> Task.FromResult>([]);
}
private sealed class FakeHfsqlConnection : IHfsqlConnection, IDisposable
{
public HfsqlOptions Options => new();
public HfsqlConnectionState State => HfsqlConnectionState.Closed;
public void Dispose() { }
public Task OpenAsync(CancellationToken ct = default) => Task.CompletedTask;
public Task CloseAsync() => Task.CompletedTask;
public Task