using System.Linq; using FluentAssertions; using FluentValidation.Results; using FsCheck; using FsCheck.Xunit; using SchotzIadKmTracker.Api.Models.DTOs; using SchotzIadKmTracker.Api.Validators; using Xunit; namespace SchotzIadKmTracker.Tests.Auth; /// /// Tests de la validation FluentValidation des DTOs d'authentification. /// La longueur du mot de passe et la validité syntaxique de l'email sont /// contrôlées au niveau du validateur (en amont d'AuthService). /// public class AuthValidatorTests { private static readonly RegisterDtoValidator RegisterValidator = new(); private static readonly LoginDtoValidator LoginValidator = new(); private static readonly ResetPasswordDtoValidator ResetValidator = new(); private const string EmailValide = "utilisateur@example.com"; // Feature: authentification, Property 7: Un mot de passe trop court est refusé à l'inscription // Toute chaîne de longueur 0..7 => invalide ; >= 8 => valide (avec email valide). // Validates: Requirements 1.3 [Property(MaxTest = 100)] public Property P7_RegisterValidator_rejette_mdp_court_accepte_mdp_long() { // Génère des mots de passe de longueur 0..20 à partir de caractères imprimables. var carac = Gen.Elements("abcdefghijklmnopqrstuvwxyz0123456789ABCDEF!@#".ToCharArray()); var mdpArb = Arb.From( from n in Gen.Choose(0, 20) from cs in Gen.ArrayOf(n, carac) select new string(cs)); return Prop.ForAll(mdpArb, mdp => { var resultat = RegisterValidator.Validate(new RegisterDto(EmailValide, mdp)); if (mdp.Length < 8) { resultat.IsValid.Should().BeFalse($"un mot de passe de longueur {mdp.Length} (< 8) doit être rejeté"); } else { resultat.IsValid.Should().BeTrue($"un mot de passe de longueur {mdp.Length} (>= 8) doit être accepté"); } }); } // Feature: authentification, Property 14: Un nouveau mot de passe trop court est refusé à la réinitialisation // ResetPasswordDtoValidator : NouveauMotDePasse < 8 => invalide ; >= 8 (token non vide) => valide. // Validates: Requirements 7.5 [Property(MaxTest = 100)] public Property P14_ResetValidator_rejette_nouveau_mdp_court() { var carac = Gen.Elements("abcdefghijklmnopqrstuvwxyz0123456789ABCDEF!@#".ToCharArray()); var mdpArb = Arb.From( from n in Gen.Choose(0, 20) from cs in Gen.ArrayOf(n, carac) select new string(cs)); return Prop.ForAll(mdpArb, mdp => { var resultat = ResetValidator.Validate(new ResetPasswordDto("un-jeton-non-vide", mdp)); if (mdp.Length < 8) { resultat.IsValid.Should().BeFalse($"un nouveau mot de passe de longueur {mdp.Length} (< 8) doit être rejeté"); } else { resultat.IsValid.Should().BeTrue($"un nouveau mot de passe de longueur {mdp.Length} (>= 8) doit être accepté"); } }); } // Feature: authentification, Property 7 (exemples) : bornes de longueur du mot de passe. // Validates: Requirements 1.3 [Theory] [InlineData("", false)] [InlineData("1234567", false)] // 7 caractères [InlineData("12345678", true)] // 8 caractères [InlineData("123456789", true)] // 9 caractères public void RegisterValidator_longueur_mdp_bornes(string mdp, bool valideAttendu) { var resultat = RegisterValidator.Validate(new RegisterDto(EmailValide, mdp)); resultat.IsValid.Should().Be(valideAttendu); } // Feature: authentification, tests d'exemples (6.13) : emails invalides rejetés. // Validates: Requirements 1.4 [Theory] [InlineData("a@")] [InlineData("abc")] [InlineData("@x")] [InlineData("")] public void RegisterValidator_rejette_les_emails_invalides(string email) { var resultat = RegisterValidator.Validate(new RegisterDto(email, "MotDePasse123")); resultat.IsValid.Should().BeFalse(); } [Theory] [InlineData("a@")] [InlineData("abc")] [InlineData("@x")] [InlineData("")] public void LoginValidator_rejette_les_emails_invalides(string email) { var resultat = LoginValidator.Validate(new LoginDto(email, "MotDePasse123")); resultat.IsValid.Should().BeFalse(); } [Fact] public void RegisterValidator_accepte_un_email_valide() { var resultat = RegisterValidator.Validate(new RegisterDto(EmailValide, "MotDePasse123")); resultat.IsValid.Should().BeTrue(); } // Feature: authentification, tests d'exemples (6.13) : aucun DTO d'auth n'expose de champ de hash. // LoginResultDto ne comporte que Token + Expiration (Requirement 8.2). // Validates: Requirements 8.2 [Fact] public void LoginResultDto_n_expose_que_token_et_expiration() { var proprietes = typeof(LoginResultDto).GetProperties().Select(p => p.Name).ToArray(); proprietes.Should().BeEquivalentTo(new[] { "Token", "Expiration" }); proprietes.Should().NotContain(n => n.ToLowerInvariant().Contains("hash")); proprietes.Should().NotContain(n => n.ToLowerInvariant().Contains("motdepasse")); } [Fact] public void Aucun_dto_d_auth_ne_comporte_de_champ_hash_ou_mot_de_passe_hash() { var types = new[] { typeof(RegisterDto), typeof(LoginDto), typeof(LoginResultDto), typeof(ForgotPasswordDto), typeof(ResetPasswordDto), typeof(CompteExisteDto) }; foreach (var type in types) { var noms = type.GetProperties().Select(p => p.Name.ToLowerInvariant()); noms.Should().NotContain(n => n.Contains("hash"), $"{type.Name} ne doit exposer aucun champ de hash"); } } }