using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using SchotzIadKmTracker.Api.Configuration; using SchotzIadKmTracker.Api.Data; using SchotzIadKmTracker.Api.Models.Entities; using SchotzIadKmTracker.Api.Services; namespace SchotzIadKmTracker.Tests.Auth; /// /// Banc d'essai pour tester en isolation. /// /// Utilise un adossé à une base SQLite EN MÉMOIRE /// ("Data Source=:memory:") dont la connexion est maintenue ouverte pour la /// durée du harnais (la base disparaît à la fermeture de la connexion). /// On emploie de vrais et /// (JWT signé HS256 avec une clé >= 32 octets), et on mocke /// (Moq) afin de capturer le lien de réinitialisation transmis. /// /// Chaque instance crée une base fraîche : l'isolation entre itérations de /// propriété est obtenue en instanciant un nouveau harnais par cas. /// public sealed class AuthTestHarness : IDisposable { // Clé secrète >= 32 octets (exigée par HMAC-SHA256). public const string SecretKey = "cle-secrete-de-test-suffisamment-longue-1234567890"; private readonly SqliteConnection _connection; public AppDbContext Context { get; } public AuthService Service { get; } public Mock EmailMock { get; } public JwtService Jwt { get; } public PasswordHasher Hasher { get; } public JwtOptions JwtOptions { get; } public AuthOptions AuthOptions { get; } /// Dernier lien de réinitialisation capturé via le mock IEmailService. public string? DernierLienReset { get; private set; } /// Nombre d'appels à EnvoyerLienResetAsync capturés. public int NbAppelsEmail { get; private set; } public AuthTestHarness(int jwtExpirationMinutes = 720, int resetTokenLifetimeMinutes = 60) { _connection = new SqliteConnection("Data Source=:memory:"); _connection.Open(); // maintenue ouverte : garde la base en vie var options = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; Context = new AppDbContext(options); Context.Database.EnsureCreated(); Hasher = new PasswordHasher(); JwtOptions = new JwtOptions { SecretKey = SecretKey, Issuer = "SchotzIadKmTracker", Audience = "SchotzIadKmTracker", ExpirationMinutes = jwtExpirationMinutes, }; AuthOptions = new AuthOptions { ResetTokenLifetimeMinutes = resetTokenLifetimeMinutes, FrontendBaseUrl = "http://localhost:8080", }; Jwt = new JwtService(Options.Create(JwtOptions)); EmailMock = new Mock(); EmailMock .Setup(e => e.EnvoyerLienResetAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((_, lien, _) => { DernierLienReset = lien; NbAppelsEmail++; }) .Returns(Task.CompletedTask); Service = new AuthService( Context, Hasher, Jwt, EmailMock.Object, Options.Create(AuthOptions), Options.Create(JwtOptions), NullLogger.Instance); } /// Extrait la valeur du paramètre "token" de la query string d'un lien de reset. public static string ExtraireToken(string lienReset) { var uri = new Uri(lienReset); var query = uri.Query.TrimStart('?'); foreach (var paire in query.Split('&', StringSplitOptions.RemoveEmptyEntries)) { var kv = paire.Split('=', 2); if (kv.Length == 2 && kv[0] == "token") { return Uri.UnescapeDataString(kv[1]); } } throw new InvalidOperationException($"Aucun paramètre 'token' dans le lien : {lienReset}"); } public void Dispose() { Context.Dispose(); _connection.Dispose(); } }