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.Api.Services; using Omika.Core.Abstractions; using Omika.Core.Compatibility; using Omika.Core.Constants; using Omika.Core.Contracts.Mobile.License; 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 HTTP des endpoints administratifs de licence mobile /// (GET/POST /collaborateurs/{id}/licence-mobile[/assign|/release]). /// Vérifie l'authentification (Bearer) et l'autorisation (droit A.LIC.MOB), /// ainsi que le contrat de réponse pour l'attribution, la consultation et la /// libération. /// [Collection("ApplicationRuntimeContext")] public class CollaborateurLicenceEndpointsTests : IDisposable { private const string SigningKey = "test-signing-key-for-integration-tests-only"; private const string CollaborateurId = "7"; private readonly FakeLicenseService _licenseService = new(); private readonly FakePermissionService _permissionService = new(); private readonly WebApplicationFactory _factory; public CollaborateurLicenceEndpointsTests() { 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(); services.RemoveAll(); services.AddScoped(_ => _licenseService); services.RemoveAll(); services.AddScoped(_ => _permissionService); }); }); } public void Dispose() { _factory.Dispose(); ApplicationRuntimeContext.Reset(); } // ===== Consultation (GET) ===== [Fact] public async Task Get_SansToken_Retourne401() { var client = _factory.CreateClient(); var response = await client.GetAsync($"/collaborateurs/{CollaborateurId}/licence-mobile"); Assert.Equal(401, (int)response.StatusCode); } [Fact] public async Task Get_DroitLicenceMobileAbsent_Retourne401() { _permissionService.Permissions[MobilePermissionKeys.LicenceMobile] = false; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.GetAsync($"/collaborateurs/{CollaborateurId}/licence-mobile"); Assert.Equal(401, (int)response.StatusCode); } [Fact] public async Task Get_DroitLicenceMobilePresent_Retourne200AvecDto() { _permissionService.Permissions[MobilePermissionKeys.LicenceMobile] = true; _licenseService.GetResult = new CollaborateurLicenceDto { Numero = "LIC-001", CodeClient = "BATIK.01", Active = true, QrCode = "omika-enroll-v1:cGF5bG9hZA.sig" }; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.GetAsync($"/collaborateurs/{CollaborateurId}/licence-mobile"); Assert.Equal(200, (int)response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal("LIC-001", body.Numero); Assert.Equal("BATIK.01", _licenseService.LastCodeClient); } // ===== Attribution (POST assign) ===== [Fact] public async Task Assign_DroitLicenceMobileAbsent_Retourne401() { _permissionService.Permissions[MobilePermissionKeys.LicenceMobile] = false; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.PostAsync( $"/collaborateurs/{CollaborateurId}/licence-mobile/assign", content: null); Assert.Equal(401, (int)response.StatusCode); } [Fact] public async Task Assign_DroitLicenceMobilePresent_Retourne200AvecDto() { _permissionService.Permissions[MobilePermissionKeys.LicenceMobile] = true; _licenseService.AssignResult = new CollaborateurLicenceDto { Numero = "LIC-002", CodeClient = "BATIK.01", Active = true, QrCode = "omika-enroll-v1:cGF5bG9hZA.sig" }; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.PostAsync( $"/collaborateurs/{CollaborateurId}/licence-mobile/assign", content: null); Assert.Equal(200, (int)response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal("LIC-002", body.Numero); Assert.Equal("BATIK.01", _licenseService.LastCodeClient); } // ===== Libération (POST release) ===== [Fact] public async Task Release_DroitLicenceMobilePresent_Retourne204() { _permissionService.Permissions[MobilePermissionKeys.LicenceMobile] = true; _licenseService.ReleaseResult = true; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.PostAsync( $"/collaborateurs/{CollaborateurId}/licence-mobile/release", content: null); Assert.Equal(204, (int)response.StatusCode); } private string CreateValidToken() { using var scope = _factory.Services.CreateScope(); var tokenService = scope.ServiceProvider.GetRequiredService(); return tokenService.GenerateToken(new MobileTokenIdentity { CodeClient = "BATIK.01", DeviceId = "device-abc-123", UserId = "user-1", CollaborateurId = CollaborateurId, NumeroLicence = "1ab053eb5e274e1f92455bbb7de9f2d3" }); } // ===== Fakes ===== private sealed class FakeLicenseService : ILicenseService { public CollaborateurLicenceDto? GetResult { get; set; } public CollaborateurLicenceDto? AssignResult { get; set; } public bool ReleaseResult { get; set; } = true; public string? LastCodeClient { get; private 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) { LastCodeClient = codeClient; return Task.FromResult(GetResult); } public Task AssignMobileLicenceAsync( string collaborateurId, string codeClient, CancellationToken cancellationToken = default) { LastCodeClient = codeClient; return Task.FromResult(AssignResult); } public Task ReleaseMobileLicenceAsync( string collaborateurId, CancellationToken cancellationToken = default) => Task.FromResult(ReleaseResult); } private sealed class FakePermissionService : IMobilePermissionService { public Dictionary Permissions { get; } = new(); public MobileTokenIdentity? Identity => null; public Task> GetPermissionsAsync(CancellationToken cancellationToken = default) => Task.FromResult>(Permissions); public Task HasPermissionAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Permissions.TryGetValue(key, out bool allowed) && allowed); } 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; } }