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 ExecuteScalarAsync(string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) => Task.FromResult(null); public Task ExecuteNonQueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) => Task.FromResult(0); public Task>> QueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) => Task.FromResult>>([]); public Task> GetColumnsAsync(string tableName, CancellationToken ct = default) => Task.FromResult>([]); public ValueTask DisposeAsync() => ValueTask.CompletedTask; } }