Files
Omika/tests/Omika.Diagnostics.Tests/DiagnosticUserPageModelTests.cs
T
2026-10-23 18:46:50 +02:00

321 lines
12 KiB
C#

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;
/// <summary>
/// T23 — PageModels /Users (création, doublon, désactivation, rétrogradation, reset,
/// dernier Admin, auto-*) et /Account (changement de son propre mot de passe).
/// </summary>
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>(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é
}
}