using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.EntityFrameworkCore; using SchotzIadKmTracker.Api.Models.DTOs; using SchotzIadKmTracker.Api.Services.Exceptions; using Xunit; namespace SchotzIadKmTracker.Tests.Auth; /// /// Tests unitaires / d'exemples du flux d'authentification (exemples ciblés /// complémentaires aux tests de propriétés) : login, non-divulgation, /// réinitialisation valide et cas d'erreur. /// public class AuthServiceTests { private const string Email = "utilisateur@example.com"; private const string MotDePasse = "MotDePasse123"; // Feature: authentification, Property 8 (exemple) : login OK puis rejet mauvais mdp / email inconnu. // Validates: Requirements 2.1, 2.2 [Fact] public async Task Login_avec_bons_identifiants_emet_un_jeton_validable() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); var resultat = await h.Service.LoginAsync(new LoginDto(Email, MotDePasse), CancellationToken.None); resultat.Token.Should().NotBeNullOrWhiteSpace(); JwtTestUtils.EstValide(resultat.Token, AuthTestHarness.SecretKey).Should().BeTrue(); resultat.Expiration.Should().BeAfter(DateTime.UtcNow); } [Fact] public async Task Login_avec_mauvais_mot_de_passe_est_rejete() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); Func acte = () => h.Service.LoginAsync(new LoginDto(Email, "MauvaisMotDePasse"), CancellationToken.None); await acte.Should().ThrowAsync(); } [Fact] public async Task Login_avec_email_inconnu_est_rejete() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); Func acte = () => h.Service.LoginAsync(new LoginDto("autre@example.com", MotDePasse), CancellationToken.None); await acte.Should().ThrowAsync(); } // Feature: authentification, Property 9 (exemple) : forgot-password réponse identique connu/inconnu. // Validates: Requirements 6.6 [Fact] public async Task ForgotPassword_email_inconnu_ne_cree_aucun_token_et_n_appelle_pas_email() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); await h.Service.ForgotPasswordAsync(new ForgotPasswordDto("inconnu@example.com"), CancellationToken.None); (await h.Context.ResetPasswordTokens.CountAsync()).Should().Be(0); h.NbAppelsEmail.Should().Be(0); } [Fact] public async Task ForgotPassword_email_connu_cree_un_token_et_appelle_email_une_fois() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); await h.Service.ForgotPasswordAsync(new ForgotPasswordDto(Email), CancellationToken.None); (await h.Context.ResetPasswordTokens.CountAsync()).Should().Be(1); h.NbAppelsEmail.Should().Be(1); h.DernierLienReset.Should().Contain("/reset-password?token="); } // Feature: authentification, Property 11 (exemple) : reset valide remplace le mot de passe. // Validates: Requirements 7.1 [Fact] public async Task ResetPassword_valide_permet_login_avec_nouveau_mdp() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); await h.Service.ForgotPasswordAsync(new ForgotPasswordDto(Email), CancellationToken.None); var tokenClair = AuthTestHarness.ExtraireToken(h.DernierLienReset!); const string nouveauMdp = "NouveauMdp456"; await h.Service.ResetPasswordAsync(new ResetPasswordDto(tokenClair, nouveauMdp), CancellationToken.None); var ok = await h.Service.LoginAsync(new LoginDto(Email, nouveauMdp), CancellationToken.None); ok.Token.Should().NotBeNullOrWhiteSpace(); Func ancien = () => h.Service.LoginAsync(new LoginDto(Email, MotDePasse), CancellationToken.None); await ancien.Should().ThrowAsync(); } // Feature: authentification, Property 12 (exemple) : usage unique du token de reset. // Validates: Requirements 7.2, 7.4 [Fact] public async Task ResetPassword_deuxieme_usage_du_meme_token_est_rejete() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); await h.Service.ForgotPasswordAsync(new ForgotPasswordDto(Email), CancellationToken.None); var tokenClair = AuthTestHarness.ExtraireToken(h.DernierLienReset!); await h.Service.ResetPasswordAsync(new ResetPasswordDto(tokenClair, "PremierMdp123"), CancellationToken.None); h.Context.ResetPasswordTokens.Single().UsedAt.Should().NotBeNull(); Func reuse = () => h.Service.ResetPasswordAsync(new ResetPasswordDto(tokenClair, "DeuxiemeMdp123"), CancellationToken.None); await reuse.Should().ThrowAsync(); } // Feature: authentification, Property 13 (exemples) : token inconnu / expiré rejetés, mdp inchangé. // Validates: Requirements 7.3, 7.4 [Fact] public async Task ResetPassword_token_inconnu_est_rejete() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); var hashInitial = h.Context.Comptes.Single().MotDePasseHash; Func acte = () => h.Service.ResetPasswordAsync(new ResetPasswordDto("inexistant", "PeuImporte123"), CancellationToken.None); await acte.Should().ThrowAsync(); h.Context.Comptes.Single().MotDePasseHash.Should().Be(hashInitial); } [Fact] public async Task ResetPassword_token_expire_est_rejete() { using var h = new AuthTestHarness(); await h.Service.RegisterAsync(new RegisterDto(Email, MotDePasse), CancellationToken.None); await h.Service.ForgotPasswordAsync(new ForgotPasswordDto(Email), CancellationToken.None); var hashInitial = h.Context.Comptes.Single().MotDePasseHash; var tokenClair = AuthTestHarness.ExtraireToken(h.DernierLienReset!); var token = h.Context.ResetPasswordTokens.Single(); token.Expiration = DateTime.UtcNow.AddMinutes(-1); await h.Context.SaveChangesAsync(); Func acte = () => h.Service.ResetPasswordAsync(new ResetPasswordDto(tokenClair, "PeuImporte123"), CancellationToken.None); await acte.Should().ThrowAsync(); h.Context.Entry(h.Context.Comptes.Single()).Reload(); h.Context.Comptes.Single().MotDePasseHash.Should().Be(hashInitial); } }