Member dashboard hides the bootstrap-promotion receipt; page-model test guards the reason projection on its own

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Heimdall 2026-11-11 14:10:30 +00:00
commit f1fb886f71
2 changed files with 51 additions and 1 deletions

View file

@ -92,7 +92,7 @@ public sealed class DashboardSnapshotService(BifrostDbContext dbContext, TimePro
var recentActivity = await dbContext.AuditEvents
.AsNoTracking()
.Include(x => x.ActorUserAccount)
.Where(x => !x.Action.StartsWith("membership.suspended") && !x.Action.StartsWith("membership.unsuspended")) // a reason is for admins, not the member dashboard
.Where(x => !x.Action.StartsWith("membership.suspended") && !x.Action.StartsWith("membership.unsuspended") && x.Action != "membership.bootstrap-promoted") // suspension reasons and the bootstrap-admin promotion are for admins, not the member dashboard
.OrderByDescending(x => x.OccurredAtUtc)
.Take(8)
.Select(x => new DashboardActivity(

View file

@ -5,6 +5,8 @@ using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using Bifrost.Web.Data;
using Bifrost.Web.Domain;
using Bifrost.Web.Features.Membership;
using Bifrost.Web.Features.Shared;
using Bifrost.Web.Tests.Support;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
@ -38,6 +40,54 @@ public sealed class SuspensionReasonExposureTests
return await response.Content.ReadAsStringAsync();
}
private sealed class FixedActor(CurrentBifrostActor actor) : ICurrentBifrostActorAccessor
{
public Task<CurrentBifrostActor> GetAsync(CancellationToken cancellationToken) => Task.FromResult(actor);
}
// Guards the projection on its own: the view's admin gate cannot be the only thing keeping the reason off a non-admin's page model.
[Fact]
public async Task The_members_page_model_carries_no_reason_for_a_non_admin_viewer_and_does_for_an_admin()
{
using var factory = new TestWebApplicationFactory();
await SeedAsync(factory.Services, "suspendedtwo", MembershipStatus.Suspended, Canary);
using var scope = factory.Services.CreateScope();
var sp = scope.ServiceProvider;
var db = sp.GetRequiredService<BifrostDbContext>();
var viewer = await db.UserAccounts.Include(x => x.Membership!).ThenInclude(x => x.RoleAssignments).SingleAsync(x => x.BifrostIdentity == "suspendedtwo");
async Task<IReadOnlyList<Bifrost.Web.Pages.App.Members.IndexModel.MemberDirectoryItem>> UsersSeenBy(bool admin)
{
viewer.Membership!.IsPlatformAdmin = admin;
var model = new Bifrost.Web.Pages.App.Members.IndexModel(db, new FixedActor(new CurrentBifrostActor(true, viewer)),
sp.GetRequiredService<AuditTrailService>(), sp.GetRequiredService<MembershipSuspensionService>(), TimeProvider.System);
await model.OnGetAsync(CancellationToken.None);
return model.Users;
}
Assert.All(await UsersSeenBy(admin: false), u => Assert.DoesNotContain(Canary, u.SuspensionReason));
Assert.Contains(await UsersSeenBy(admin: true), u => u.SuspensionReason == Canary);
}
[Fact]
public async Task The_member_dashboard_hides_the_bootstrap_promotion_receipt_but_shows_ordinary_activity()
{
using var factory = new TestWebApplicationFactory();
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
db.AuditEvents.Add(new AuditEvent { Action = "membership.bootstrap-promoted", Detail = "PROMO-CANARY-41", EntityType = "Membership", EntityId = "x", OccurredAtUtc = DateTimeOffset.UtcNow });
db.AuditEvents.Add(new AuditEvent { Action = "membership.approved", Detail = "ORDINARY-CANARY-41", EntityType = "Membership", EntityId = "x", OccurredAtUtc = DateTimeOffset.UtcNow.AddMinutes(-1) });
await db.SaveChangesAsync();
}
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, BaseAddress = new Uri("https://localhost") });
var html = await client.GetStringAsync("/App");
Assert.Contains("ORDINARY-CANARY-41", html);
Assert.DoesNotContain("PROMO-CANARY-41", html);
}
[Fact]
public async Task Member_facing_pages_never_carry_the_reason_and_the_admin_members_page_does()
{