Members page: approve/role refusals (suspended, no membership, own admin role) show a fixed notice; role toggle on a suspended account is refused like approve
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d91d4a3da6
commit
5a3d36ed1b
2 changed files with 97 additions and 6 deletions
|
|
@ -22,7 +22,7 @@ public sealed class IndexModel(
|
|||
[BindProperty]
|
||||
public UpdateProfileInput Profile { get; set; } = new();
|
||||
|
||||
/// <summary>Why the last suspend or unsuspend did nothing; one of <see cref="RefusalNotices"/>, carried across the redirect.</summary>
|
||||
/// <summary>Why the last membership action did nothing; one of <see cref="RefusalNotices"/>, carried across the redirect.</summary>
|
||||
[TempData]
|
||||
public string? Notice { get; set; }
|
||||
|
||||
|
|
@ -34,6 +34,11 @@ public sealed class IndexModel(
|
|||
[SuspensionOutcome.NotApplicable] = "Nothing changed: the account is already in that state."
|
||||
};
|
||||
|
||||
internal const string ApproveSuspendedNotice = "Nothing changed: a suspended account can only be lifted with Unsuspend.";
|
||||
internal const string NoMembershipNotice = "Nothing changed: that account has no membership yet.";
|
||||
internal const string OwnAdminRoleNotice = "Nothing changed: you cannot remove or change your own platform admin role.";
|
||||
internal const string RoleOnSuspendedNotice = "Nothing changed: a suspended account's roles cannot be changed until it is unsuspended.";
|
||||
|
||||
public CurrentBifrostActor Actor { get; private set; } = CurrentBifrostActor.Anonymous;
|
||||
|
||||
public IReadOnlyList<MemberDirectoryItem> Users { get; private set; } = [];
|
||||
|
|
@ -118,7 +123,8 @@ public sealed class IndexModel(
|
|||
|
||||
if (user.Membership!.Status == MembershipStatus.Suspended)
|
||||
{
|
||||
return RedirectToPage(); // only Unsuspend lifts a suspension
|
||||
Notice = ApproveSuspendedNotice; // only Unsuspend lifts a suspension
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
user.Membership.Status = MembershipStatus.Active;
|
||||
|
|
@ -179,6 +185,19 @@ public sealed class IndexModel(
|
|||
|
||||
if (user.Membership is null)
|
||||
{
|
||||
Notice = NoMembershipNotice;
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
if (user.Id == Actor.UserAccount.Id && role == MemberRole.PlatformAdmin)
|
||||
{
|
||||
Notice = OwnAdminRoleNotice;
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
if (user.Membership.Status == MembershipStatus.Suspended)
|
||||
{
|
||||
Notice = RoleOnSuspendedNotice;
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +212,7 @@ public sealed class IndexModel(
|
|||
Notes = "Assigned from the member admin page."
|
||||
});
|
||||
}
|
||||
else if (!(user.Id == Actor.UserAccount.Id && role == MemberRole.PlatformAdmin))
|
||||
else
|
||||
{
|
||||
// Out of the collection too, so the flags below derive from the roles as they will stand after the save.
|
||||
user.Membership.RoleAssignments.Remove(existingAssignment);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public sealed class MembersPageNoticeTests(TestWebApplicationFactory factory) :
|
|||
HandleCookies = true
|
||||
});
|
||||
|
||||
private async Task<Guid> SeedMemberAsync(MembershipStatus status)
|
||||
private async Task<Guid> SeedMemberAsync(MembershipStatus status, bool withMembership = true)
|
||||
{
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
|
||||
|
|
@ -27,7 +27,7 @@ public sealed class MembersPageNoticeTests(TestWebApplicationFactory factory) :
|
|||
var account = new UserAccount
|
||||
{
|
||||
DisplayName = id, BifrostIdentity = id, NormalizedBifrostIdentity = id,
|
||||
Membership = new Bifrost.Web.Domain.Membership { Status = status, ApprovedAtUtc = DateTimeOffset.UtcNow }
|
||||
Membership = withMembership ? new Bifrost.Web.Domain.Membership { Status = status, ApprovedAtUtc = DateTimeOffset.UtcNow } : null
|
||||
};
|
||||
db.UserAccounts.Add(account);
|
||||
await db.SaveChangesAsync();
|
||||
|
|
@ -47,7 +47,7 @@ public sealed class MembersPageNoticeTests(TestWebApplicationFactory factory) :
|
|||
return Regex.Match(html, "name=\"__RequestVerificationToken\"[^>]*value=\"([^\"]+)\"").Groups[1].Value;
|
||||
}
|
||||
|
||||
private static async Task<string> PostThenFollowAsync(HttpClient client, string handler, Guid target, string reason)
|
||||
private static async Task<string> PostThenFollowAsync(HttpClient client, string handler, Guid target, string reason, string? role = null)
|
||||
{
|
||||
var token = await TokenAsync(client);
|
||||
Assert.NotEmpty(token);
|
||||
|
|
@ -55,6 +55,7 @@ public sealed class MembersPageNoticeTests(TestWebApplicationFactory factory) :
|
|||
{
|
||||
["userAccountId"] = target.ToString(),
|
||||
["reason"] = reason,
|
||||
["role"] = role ?? "Producer",
|
||||
["__RequestVerificationToken"] = token
|
||||
}));
|
||||
Assert.Equal(HttpStatusCode.Redirect, post.StatusCode);
|
||||
|
|
@ -127,4 +128,75 @@ public sealed class MembersPageNoticeTests(TestWebApplicationFactory factory) :
|
|||
|
||||
Assert.DoesNotContain("role=\"alert\"", html);
|
||||
}
|
||||
|
||||
private async Task<(MembershipStatus Status, int Roles)> StateAsync(Guid user)
|
||||
{
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var m = await scope.ServiceProvider.GetRequiredService<BifrostDbContext>().Memberships.AsNoTracking().Include(x => x.RoleAssignments).SingleAsync(x => x.UserAccountId == user);
|
||||
return (m.Status, m.RoleAssignments.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approving_a_suspended_account_says_only_unsuspend_lifts_it_and_leaves_it_suspended()
|
||||
{
|
||||
using var client = Client();
|
||||
_ = await SignedInAdminIdAsync(client);
|
||||
var member = await SeedMemberAsync(MembershipStatus.Suspended);
|
||||
|
||||
var html = await PostThenFollowAsync(client, "Approve", member, "");
|
||||
|
||||
Assert.Contains("only be lifted with Unsuspend", Alert.Match(html).Groups[1].Value);
|
||||
Assert.Equal(MembershipStatus.Suspended, (await StateAsync(member)).Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Toggling_a_role_on_a_suspended_account_is_refused_with_a_notice_and_changes_nothing()
|
||||
{
|
||||
using var client = Client();
|
||||
_ = await SignedInAdminIdAsync(client);
|
||||
var member = await SeedMemberAsync(MembershipStatus.Suspended);
|
||||
|
||||
var html = await PostThenFollowAsync(client, "ToggleRole", member, "", role: "Producer");
|
||||
|
||||
Assert.Contains("roles cannot be changed until it is unsuspended", Alert.Match(html).Groups[1].Value);
|
||||
Assert.Equal(0, (await StateAsync(member)).Roles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Toggling_a_role_on_an_account_without_a_membership_says_so()
|
||||
{
|
||||
using var client = Client();
|
||||
_ = await SignedInAdminIdAsync(client);
|
||||
var member = await SeedMemberAsync(MembershipStatus.Active, withMembership: false);
|
||||
|
||||
var html = await PostThenFollowAsync(client, "ToggleRole", member, "", role: "Producer");
|
||||
|
||||
Assert.Contains("has no membership yet", Alert.Match(html).Groups[1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_admin_toggling_their_own_platform_admin_role_is_refused_and_keeps_the_role()
|
||||
{
|
||||
using var client = Client();
|
||||
var admin = await SignedInAdminIdAsync(client);
|
||||
var before = (await StateAsync(admin)).Roles;
|
||||
|
||||
var html = await PostThenFollowAsync(client, "ToggleRole", admin, "", role: "PlatformAdmin");
|
||||
|
||||
Assert.Contains("cannot remove or change your own platform admin role", Alert.Match(html).Groups[1].Value);
|
||||
Assert.Equal(before, (await StateAsync(admin)).Roles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_role_toggle_on_an_active_account_still_works_without_a_refusal()
|
||||
{
|
||||
using var client = Client();
|
||||
_ = await SignedInAdminIdAsync(client);
|
||||
var member = await SeedMemberAsync(MembershipStatus.Active);
|
||||
|
||||
var html = await PostThenFollowAsync(client, "ToggleRole", member, "", role: "Producer");
|
||||
|
||||
Assert.DoesNotContain("role=\"alert\"", html);
|
||||
Assert.True((await StateAsync(member)).Roles >= 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue