321 lines
12 KiB
C#
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é
|
|
}
|
|
}
|