From f1fb886f719c9519eca8a9ba48a2c57d793a3baf Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 14:10:30 +0000 Subject: [PATCH] Member dashboard hides the bootstrap-promotion receipt; page-model test guards the reason projection on its own Co-Authored-By: Claude Sonnet 5.5 --- .../Shared/DashboardSnapshotService.cs | 2 +- .../SuspensionReasonExposureTests.cs | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Bifrost.Web/Features/Shared/DashboardSnapshotService.cs b/src/Bifrost.Web/Features/Shared/DashboardSnapshotService.cs index cd0d8f2..0837f7e 100644 --- a/src/Bifrost.Web/Features/Shared/DashboardSnapshotService.cs +++ b/src/Bifrost.Web/Features/Shared/DashboardSnapshotService.cs @@ -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( diff --git a/tests/Bifrost.Web.Tests/SuspensionReasonExposureTests.cs b/tests/Bifrost.Web.Tests/SuspensionReasonExposureTests.cs index f4f5d30..0921036 100644 --- a/tests/Bifrost.Web.Tests/SuspensionReasonExposureTests.cs +++ b/tests/Bifrost.Web.Tests/SuspensionReasonExposureTests.cs @@ -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 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(); + var viewer = await db.UserAccounts.Include(x => x.Membership!).ThenInclude(x => x.RoleAssignments).SingleAsync(x => x.BifrostIdentity == "suspendedtwo"); + + async Task> 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(), sp.GetRequiredService(), 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(); + 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() {