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);
}
}