using System.Net;
using System.Net.Http.Json;
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.Constants;
using Omika.Core.Contracts.Mobile.Bootstrap;
using Omika.Core.Contracts.Mobile.License;
using Omika.Core.Contracts.Security;
using Omika.Core.Interfaces.Mobile;
using Omika.Core.Runtime;
using Omika.Data.Configuration;
using Omika.Data.Connection;
using Omika.Data.Runtime;
namespace Omika.Tests.Api;
///
/// Tests d'intégration de la résolution réelle des permissions :
/// token → MobileAuthMiddleware → MobileTokenIdentity → IMobilePermissionService
/// → IMobileIdentityProvider → IMobileIdentityRepository → A.LIC.MOB.
///
/// Utilise le vrai et le vrai
/// , avec un faux
/// repository d'identité. Vérifie qu'un grand identifiant 64 bits traverse la
/// chaîne sans troncature et que A.LIC.MOB est résolu dynamiquement.
///
[Collection("ApplicationRuntimeContext")]
public class MobilePermissionResolutionIntegrationTests : IDisposable
{
private const string SigningKey = "test-signing-key-for-integration-tests-only";
private const string CodeClient = "BATIK.01";
private const string CollaborateurId = "7";
private const string BigUserId = "9000000000"; // > Int32.MaxValue, aucun risque de troncature
private readonly FakeMobileIdentityRepository _identityRepository = new();
private readonly FakeLicenseService _licenseService = new();
private readonly WebApplicationFactory _factory;
public MobilePermissionResolutionIntegrationTests()
{
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",
["MobileAuth:SigningKey"] = SigningKey
})
.Build();
services.AddSingleton(config);
services.RemoveAll();
services.AddScoped();
services.RemoveAll();
services.AddScoped();
// Réel IMobilePermissionService + IMobileIdentityProvider,
// avec un faux repository d'identité (résolution contrôlée).
services.RemoveAll();
services.AddScoped(_ => _identityRepository);
services.RemoveAll();
services.AddScoped(_ => _licenseService);
});
});
}
public void Dispose()
{
_factory.Dispose();
ApplicationRuntimeContext.Reset();
}
[Fact]
public async Task PermissionAccordee_EndpointLicenceNonRefuse_UserIdGrandIntact()
{
_identityRepository.GrantLicenceMobile = true;
_licenseService.GetResult = new CollaborateurLicenceDto { Numero = "LIC-001", CodeClient = CodeClient, Active = true };
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateToken());
var response = await client.GetAsync($"/collaborateurs/{CollaborateurId}/licence-mobile");
// 200 (licence trouvée) ou 404 (licence absente), mais jamais 401 pour manque de droit.
Assert.NotEqual(401, (int)response.StatusCode);
// Le UserId du token (grand identifiant 64 bits) est transmis intact à la résolution.
Assert.Equal(BigUserId, _identityRepository.LastUserId);
}
[Fact]
public async Task PermissionRefusee_EndpointLicence_Retourne401()
{
_identityRepository.GrantLicenceMobile = false;
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateToken());
var response = await client.GetAsync($"/collaborateurs/{CollaborateurId}/licence-mobile");
Assert.Equal(401, (int)response.StatusCode);
}
[Fact]
public async Task PermissionAbsente_EndpointLicence_Retourne401()
{
// Aucune permission A.LIC.MOB dans le catalogue retourné.
_identityRepository.GrantLicenceMobile = null;
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateToken());
var response = await client.GetAsync($"/collaborateurs/{CollaborateurId}/licence-mobile");
Assert.Equal(401, (int)response.StatusCode);
}
private string CreateToken()
{
using var scope = _factory.Services.CreateScope();
var tokenService = scope.ServiceProvider.GetRequiredService();
return tokenService.GenerateToken(new MobileTokenIdentity
{
CodeClient = CodeClient,
DeviceId = "backoffice-windev",
UserId = BigUserId,
CollaborateurId = CollaborateurId,
NumeroLicence = string.Empty
});
}
// ===== Fakes =====
private sealed class FakeMobileIdentityRepository : IMobileIdentityRepository
{
/// true = accordée, false = refusée, null = absente.
public bool? GrantLicenceMobile { get; set; }
public string? LastUserId { get; private set; }
public Task GetProfileAsync(string userId, CancellationToken cancellationToken = default)
{
LastUserId = userId;
return Task.FromResult(new MobileProfileDto { Code = "ROLE", Nom = "ROLE" });
}
public Task> GetPermissionsAsync(string userId, CancellationToken cancellationToken = default)
{
LastUserId = userId;
var permissions = MobilePermissionKeys.Empty();
if (GrantLicenceMobile is { } granted)
{
permissions[MobilePermissionKeys.LicenceMobile] = granted;
}
return Task.FromResult>(permissions);
}
public Task GetLoginByUserIdAsync(string userId, CancellationToken cancellationToken = default)
{
LastUserId = userId;
return Task.FromResult("TEST_USER");
}
public Task GetUtilisateurPkIdByCollaborateurAsync(long collaborateurPkId, CancellationToken cancellationToken = default)
=> Task.FromResult(null);
public Task GetLoginByCollaborateurAsync(long collaborateurPkId, CancellationToken cancellationToken = default)
=> Task.FromResult(null);
}
private sealed class FakeLicenseService : ILicenseService
{
public CollaborateurLicenceDto? GetResult { get; set; }
public Task ActivateAsync(LicenseActivateRequest request, CancellationToken cancellationToken = default)
=> Task.FromResult(new LicenseResponse());
public Task ValidateAsync(LicenseValidateRequest request, CancellationToken cancellationToken = default)
=> Task.FromResult(new LicenseResponse());
public Task GetMobileLicenceForCollaborateurAsync(string collaborateurId, string codeClient, CancellationToken cancellationToken = default)
=> Task.FromResult(GetResult);
public Task AssignMobileLicenceAsync(string collaborateurId, string codeClient, CancellationToken cancellationToken = default)
=> Task.FromResult(null);
public Task ReleaseMobileLicenceAsync(string collaborateurId, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
}
private sealed class FakeRuntimeInitializer : IDatabaseRuntimeInitializer
{
public Task InitializeAsync(CancellationToken cancellationToken = default)
{
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
return Task.FromResult(Result.Success());
}
}
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