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.Contracts.Mobile.Bootstrap; 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 HTTP des endpoints de bootstrap mobile. /// Vérifie l'authentification (sans token, token invalide) et le contrat retourné. /// [Collection("ApplicationRuntimeContext")] public class MobileBootstrapEndpointsTests : IDisposable { private const string SigningKey = "test-signing-key-for-integration-tests-only"; private readonly FakeBootstrapService _fakeService = new(); private readonly WebApplicationFactory _factory; public MobileBootstrapEndpointsTests() { 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(_ => _fakeService); }); }); } public void Dispose() { _factory.Dispose(); ApplicationRuntimeContext.Reset(); } [Fact] public async Task Bootstrap_WithoutToken_Returns401() { var client = _factory.CreateClient(); var response = await client.GetAsync("/api/mobile/bootstrap"); Assert.Equal(401, (int)response.StatusCode); } [Fact] public async Task Bootstrap_InvalidToken_Returns401() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "invalid.token.value"); var response = await client.GetAsync("/api/mobile/bootstrap"); Assert.Equal(401, (int)response.StatusCode); } [Fact] public async Task Bootstrap_DeviceMismatch_Returns403_Not500() { _fakeService.ThrowDeviceMismatch = true; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.GetAsync("/api/mobile/bootstrap"); Assert.Equal(403, (int)response.StatusCode); } [Fact] public async Task Bootstrap_ValidToken_Returns200WithContract() { _fakeService.Result = new MobileBootstrapResponse { Utilisateur = new CurrentUserDto { Id = "user-1", Login = "user-1" }, Profil = new MobileProfileDto { Code = "BATIK", Nom = "BATIK" } }; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.GetAsync("/api/mobile/bootstrap"); Assert.Equal(200, (int)response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal("user-1", body.Utilisateur.Id); Assert.Equal("BATIK", body.Profil.Code); } 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 = "7", NumeroLicence = "1ab053eb5e274e1f92455bbb7de9f2d3" }); } // ===== Fakes ===== private sealed class FakeBootstrapService : IMobileBootstrapService { public MobileBootstrapResponse Result { get; set; } = new(); public bool ThrowDeviceMismatch { get; set; } public Task GetBootstrapAsync( MobileTokenIdentity identity, CancellationToken cancellationToken = default) { if (ThrowDeviceMismatch) { throw new UnauthorizedAccessException( "Le DeviceId authentifié ne correspond pas à la licence attribuée."); } return Task.FromResult(Result); } } 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; } }