using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.RazorPages; using Omika.Diagnostics.Security; using Omika.Diagnostics.Tests.Fakes; using Omika.Diagnostics.Users; using Omika.Diagnostics.Web.Pages; namespace Omika.Diagnostics.Tests; /// /// T23 — PageModels /Users (création, doublon, désactivation, rétrogradation, reset, /// dernier Admin, auto-*) et /Account (changement de son propre mot de passe). /// public class DiagnosticUserPageModelTests { private const string Password = "correct-password-123"; private const string NewPassword = "nouveau-password-456"; private static DiagnosticUserRecord User(long id, string username, string role, bool enabled = true) => new() { Id = id, Username = username, PasswordHash = DiagnosticPasswordHasher.Hash(Password), Role = role, Enabled = enabled, CreatedAtUtc = DateTimeOffset.UtcNow, UpdatedAtUtc = DateTimeOffset.UtcNow, PasswordChangedAtUtc = DateTimeOffset.UtcNow }; private static UsersModel NewUsersModel(FakeDiagnosticUserRepository repo, string actor) => WithActor(new UsersModel(repo), actor); private static T WithActor(T model, string actor) where T : PageModel { var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, actor) }, "test"); var principal = new ClaimsPrincipal(identity); model.PageContext = new PageContext { HttpContext = new DefaultHttpContext { User = principal } }; return model; } // -------------------------------------------------- création ---- [Fact] public async Task Create_StoresHash_NotPlaintext() { var repo = new FakeDiagnosticUserRepository(); var model = NewUsersModel(repo, "admin1"); model.NewUsername = "jean.dupont"; model.NewPassword = Password; model.NewPasswordConfirmation = Password; model.NewRole = DiagnosticRoles.Support; await model.OnPostCreateAsync(); DiagnosticUserRecord? created = await repo.GetByUsernameAsync("jean.dupont"); Assert.NotNull(created); Assert.NotEqual(Password, created!.PasswordHash); Assert.DoesNotContain(Password, created.PasswordHash, StringComparison.Ordinal); Assert.True(DiagnosticPasswordHasher.Verify(Password, created.PasswordHash)); Assert.Equal(DiagnosticRoles.Support, created.Role); } [Fact] public async Task Create_DuplicateCaseInsensitive_Refused() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "Jean.Dupont", DiagnosticRoles.Support)); var model = NewUsersModel(repo, "admin1"); model.NewUsername = "jean.dupont"; model.NewPassword = Password; model.NewPasswordConfirmation = Password; model.NewRole = DiagnosticRoles.Support; await model.OnPostCreateAsync(); Assert.Equal("Ce nom d'utilisateur existe déjà.", model.ErrorMessage); Assert.Single(await repo.ListAsync()); } [Theory] [InlineData("mauvais username")] [InlineData("")] [InlineData("accentué")] public async Task Create_InvalidUsername_Refused(string username) { var repo = new FakeDiagnosticUserRepository(); var model = NewUsersModel(repo, "admin1"); model.NewUsername = username; model.NewPassword = Password; model.NewPasswordConfirmation = Password; model.NewRole = DiagnosticRoles.Support; await model.OnPostCreateAsync(); Assert.Contains("Nom d'utilisateur invalide", model.ErrorMessage); Assert.Empty(await repo.ListAsync()); } [Fact] public async Task Create_ShortPassword_Refused() { var repo = new FakeDiagnosticUserRepository(); var model = NewUsersModel(repo, "admin1"); model.NewUsername = "jean"; model.NewPassword = "court"; model.NewPasswordConfirmation = "court"; model.NewRole = DiagnosticRoles.Support; await model.OnPostCreateAsync(); Assert.Contains("au moins 12 caractères", model.ErrorMessage); Assert.Empty(await repo.ListAsync()); } [Fact] public async Task Create_PasswordMismatch_Refused() { var repo = new FakeDiagnosticUserRepository(); var model = NewUsersModel(repo, "admin1"); model.NewUsername = "jean"; model.NewPassword = Password; model.NewPasswordConfirmation = "different-password"; model.NewRole = DiagnosticRoles.Support; await model.OnPostCreateAsync(); Assert.Contains("confirmation", model.ErrorMessage); Assert.Empty(await repo.ListAsync()); } [Fact] public async Task Create_InvalidRole_Refused() { var repo = new FakeDiagnosticUserRepository(); var model = NewUsersModel(repo, "admin1"); model.NewUsername = "jean"; model.NewPassword = Password; model.NewPasswordConfirmation = Password; model.NewRole = "Owner"; await model.OnPostCreateAsync(); Assert.Contains("Rôle invalide", model.ErrorMessage); Assert.Empty(await repo.ListAsync()); } // -------------------------------------------------- désactivation ---- [Fact] public async Task Disable_LastActiveAdmin_Refused() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-db", DiagnosticRoles.Admin, enabled: true)); var model = NewUsersModel(repo, "bootstrap-admin"); // acteur opérateur (hors DB) await model.OnPostToggleAsync(1, enabled: false); Assert.Equal(DiagnosticUserGuard.LastAdminMessage, model.ErrorMessage); Assert.True((await repo.GetByUsernameAsync("admin-db"))!.Enabled); } [Fact] public async Task Disable_NonLastAdmin_Succeeds() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-a", DiagnosticRoles.Admin), User(2, "admin-b", DiagnosticRoles.Admin)); var model = NewUsersModel(repo, "admin-a"); await model.OnPostToggleAsync(2, enabled: false); Assert.Null(model.ErrorMessage); Assert.False((await repo.GetByUsernameAsync("admin-b"))!.Enabled); } [Fact] public async Task Disable_Self_Refused() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-a", DiagnosticRoles.Admin), User(2, "admin-b", DiagnosticRoles.Admin)); var model = NewUsersModel(repo, "admin-a"); await model.OnPostToggleAsync(1, enabled: false); Assert.Equal(DiagnosticUserGuard.SelfDisableMessage, model.ErrorMessage); Assert.True((await repo.GetByUsernameAsync("admin-a"))!.Enabled); } [Fact] public async Task Enable_DisabledUser_Succeeds() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "support-a", DiagnosticRoles.Support, enabled: false)); var model = NewUsersModel(repo, "admin-a"); await model.OnPostToggleAsync(1, enabled: true); Assert.Null(model.ErrorMessage); Assert.True((await repo.GetByUsernameAsync("support-a"))!.Enabled); } // -------------------------------------------------- changement de rôle ---- [Fact] public async Task Demote_LastActiveAdmin_Refused() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-db", DiagnosticRoles.Admin, enabled: true)); var model = NewUsersModel(repo, "bootstrap-admin"); await model.OnPostSetRoleAsync(1, DiagnosticRoles.Support); Assert.Equal(DiagnosticUserGuard.LastAdminMessage, model.ErrorMessage); Assert.Equal(DiagnosticRoles.Admin, (await repo.GetByUsernameAsync("admin-db"))!.Role); } [Fact] public async Task Demote_Self_Refused() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-a", DiagnosticRoles.Admin), User(2, "admin-b", DiagnosticRoles.Admin)); var model = NewUsersModel(repo, "admin-a"); await model.OnPostSetRoleAsync(1, DiagnosticRoles.Support); Assert.Equal(DiagnosticUserGuard.SelfDemoteMessage, model.ErrorMessage); Assert.Equal(DiagnosticRoles.Admin, (await repo.GetByUsernameAsync("admin-a"))!.Role); } [Fact] public async Task Promote_SupportToAdmin_Succeeds() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-a", DiagnosticRoles.Admin), User(2, "support-a", DiagnosticRoles.Support)); var model = NewUsersModel(repo, "admin-a"); await model.OnPostSetRoleAsync(2, DiagnosticRoles.Admin); Assert.Null(model.ErrorMessage); Assert.Equal(DiagnosticRoles.Admin, (await repo.GetByUsernameAsync("support-a"))!.Role); } [Fact] public async Task SetRole_InvalidRole_Refused() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-a", DiagnosticRoles.Admin), User(2, "support-a", DiagnosticRoles.Support)); var model = NewUsersModel(repo, "admin-a"); await model.OnPostSetRoleAsync(2, "Owner"); Assert.Contains("Rôle invalide", model.ErrorMessage); Assert.Equal(DiagnosticRoles.Support, (await repo.GetByUsernameAsync("support-a"))!.Role); } // -------------------------------------------------- reset mot de passe ---- [Fact] public async Task ResetPassword_OldRejected_NewAccepted() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "support-a", DiagnosticRoles.Support)); var model = NewUsersModel(repo, "admin-a"); model.ResetPassword = NewPassword; model.ResetPasswordConfirmation = NewPassword; await model.OnPostResetPasswordAsync(1); DiagnosticUserRecord? user = await repo.GetByUsernameAsync("support-a"); Assert.False(DiagnosticPasswordHasher.Verify(Password, user!.PasswordHash)); // ancien refusé Assert.True(DiagnosticPasswordHasher.Verify(NewPassword, user.PasswordHash)); // nouveau accepté } // -------------------------------------------------- Mon compte ---- [Fact] public async Task Account_ChangeOwnPassword_CorrectCurrent_Succeeds() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-a", DiagnosticRoles.Admin)); var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "admin-a"), new Claim(Omika.Diagnostics.Web.Authorization.DiagnosticClaimTypes.UserSource, "db") }, "test"); var model = new AccountModel(repo); model.CurrentPassword = Password; model.NewPassword = NewPassword; model.NewPasswordConfirmation = NewPassword; model.PageContext = new PageContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } }; await model.OnPostAsync(); DiagnosticUserRecord? user = await repo.GetByUsernameAsync("admin-a"); Assert.Equal("Mot de passe modifié.", model.SuccessMessage); Assert.True(DiagnosticPasswordHasher.Verify(NewPassword, user!.PasswordHash)); } [Fact] public async Task Account_WrongCurrentPassword_HashUnchanged() { var repo = new FakeDiagnosticUserRepository(); repo.Seed(User(1, "admin-a", DiagnosticRoles.Admin)); var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "admin-a"), new Claim(Omika.Diagnostics.Web.Authorization.DiagnosticClaimTypes.UserSource, "db") }, "test"); var model = new AccountModel(repo); model.CurrentPassword = "mauvais-mot-de-passe"; model.NewPassword = NewPassword; model.NewPasswordConfirmation = NewPassword; model.PageContext = new PageContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } }; await model.OnPostAsync(); DiagnosticUserRecord? user = await repo.GetByUsernameAsync("admin-a"); Assert.Equal("Mot de passe actuel incorrect.", model.ErrorMessage); Assert.True(DiagnosticPasswordHasher.Verify(Password, user!.PasswordHash)); // hash inchangé } }