Skip to content

test: add component coverage gate and auth UI tests#2

Closed
mpaulosky wants to merge 1 commit into
mainfrom
squad/coverage-test-hardening-main
Closed

test: add component coverage gate and auth UI tests#2
mpaulosky wants to merge 1 commit into
mainfrom
squad/coverage-test-hardening-main

Conversation

@mpaulosky

Copy link
Copy Markdown
Owner

Summary

Closes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ Feature (non-breaking change that adds functionality)
  • ♻️ Refactor (no behavior change, code cleanup/restructure)
  • 🧪 Tests (new or updated tests only)
  • 📝 Docs (README, XML docs, comments)
  • ⚙️ Infra/CI (GitHub Actions, Aspire, NuGet, deployment)
  • 🔒 Security (auth, permissions, secrets, headers)
  • 💥 Breaking change (existing behavior changes)

Domain Affected

  • 🏗️ Architecture / domain logic / CQRS → Aragorn required
  • 🔧 Backend (handlers, repositories, API endpoints, MediatR) → Sam required
  • ⚛️ Frontend (Blazor components, Razor pages, CSS, JS) → Legolas required
  • 🧪 Unit / bUnit / integration tests → Gimli required
  • 🧪 E2E / Playwright / Aspire integration tests → Pippin required
  • ⚙️ CI/CD / GitHub Actions / NuGet / Aspire AppHost → Boromir required
  • 🔒 Auth0 / authorization / security-relevant changes → Gandalf required
  • 📝 Docs / README / XML docs → Frodo required

Self-Review Checklist

Code Quality

  • I ran dotnet build IssueTrackerApp.slnx --configuration Release — 0 errors, 0 warnings
  • I ran dotnet test IssueTrackerApp.slnx --configuration Release --no-build — all pass
  • No TODO/FIXME left unless tracked in a follow-up issue (link it)
  • No secrets, API keys, or credentials committed

Architecture

  • New handlers follow the Command/Query/Handler/Validator naming conventions
  • New handlers are sealed
  • Domain layer has no references to Web or Persistence.* projects
  • Result<T> / ResultErrorCode used for expected failures (no exception-driven control flow)
  • DTOs are records in Domain.DTOs; Models are in Domain.Models
  • No DTO types embedded in Model classes

Tests

  • New code has corresponding unit tests
  • Integration tests use domain-specific collections ([Collection("XxxIntegration")])
  • No test compares two IssueDto.Empty / CommentDto.Empty instances directly

Security (check if security-relevant)

  • New endpoints have appropriate RequireAuthorization / policy applied
  • No MarkupString used with user-supplied content
  • No user input reflected in MongoDB queries without sanitization

Merge Readiness

  • Branch is up to date with main (no merge conflicts)
  • CI checks are green (do not request review while checks are pending/failing)
  • PR description is complete — reviewers should not have to ask what this does

Screenshots / Evidence

Notes for Reviewers

@mpaulosky mpaulosky added the squad Squad triage inbox — Lead will assign to a member label Apr 18, 2026
Copilot AI review requested due to automatic review settings April 18, 2026 18:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a unit-test coverage gate and expands Blazor UI test coverage around authentication/authorization-driven UX, while also improving role-claim handling for Auth0 and adding a user profile page.

Changes:

  • Enable Coverlet collection with a line-coverage threshold in Unit.Tests.
  • Add RoleClaimsHelper + Auth0 token hook to normalize roles into ClaimTypes.Role, plus a new /profile page that displays identity/roles/claims.
  • Add bUnit smoke/component tests for NavMenu, profile, and key pages; update layout/nav and Manage Roles UI styling.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/Unit.Tests/Unit.Tests.csproj Enables coverage collection/threshold gating and adds bUnit dependencies.
tests/Unit.Tests/Testing/TestAuthorizationService.cs Adds a lightweight authorization service for component testing.
tests/Unit.Tests/Security/RoleClaimsHelperTests.cs Adds unit coverage for role claim parsing/aggregation behaviors.
tests/Unit.Tests/ResultTests.cs Adds baseline unit tests for Result/Result<T> behaviors.
tests/Unit.Tests/Features/UserManagement/ProfileTests.cs Adds bUnit tests for the new Profile UI (happy path + fallbacks).
tests/Unit.Tests/Components/RazorSmokeTests.cs Adds broad “renders / basic interactions” coverage for key Razor components/pages.
tests/Unit.Tests/Components/Layout/NavMenuTests.cs Adds bUnit tests for auth-dependent links and theme JS interop behavior.
src/Web/Security/RoleClaimsHelper.cs Introduces helper utilities to expand/normalize role claims from Auth0.
src/Web/Properties/AssemblyInfo.cs Grants internals access to the unit test assembly.
src/Web/Program.cs Hooks OIDC token validation to add normalized role claims using RoleClaimsHelper.
src/Web/Features/UserManagement/Profile.razor Adds a new authorized Profile page showing identity details, roles, and claims.
src/Web/Features/UserManagement/ManageRoles.razor Updates Manage Roles UI styling/structure and error display.
src/Web/Components/Pages/Home.razor Updates the home page greeting/copy.
src/Web/Components/Layout/NavMenu.razor Reworks navigation UI; adds profile link + theme controls + nav state handling.
src/Web/Components/Layout/MainLayout.razor Updates overall page layout structure and footer content.


public static IReadOnlyList<string> GetRoles(ClaimsPrincipal user, IEnumerable<string>? roleClaimTypes = null)
{
var types = (roleClaimTypes ?? DefaultRoleClaimTypes.Append(ClaimTypes.Role))

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When roleClaimTypes is provided, GetRoles no longer includes ClaimTypes.Role in the allowed claim types. That makes the parameter behave like a full replacement rather than “additional claim types”, and can cause standard role claims to be ignored. Consider always appending ClaimTypes.Role regardless of whether a custom list is supplied.

Suggested change
var types = (roleClaimTypes ?? DefaultRoleClaimTypes.Append(ClaimTypes.Role))
var types = (roleClaimTypes ?? DefaultRoleClaimTypes)
.Append(ClaimTypes.Role)

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +22
<input type="checkbox" id="menu-toggle" class="peer sr-only" aria-hidden="true"/>
<label for="menu-toggle"
class="md:hidden cursor-pointer p-2 rounded hover:bg-primary-500 dark:hover:bg-primary-600 text-white"
aria-label="Toggle navigation">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mobile nav toggle uses a visually-hidden checkbox with aria-hidden="true" and a <label> as the interactive control. Because the checkbox is hidden from assistive technology and the label isn’t keyboard-focusable by default, this is not accessible for keyboard/screen-reader users. Consider using a real <button> with aria-expanded/aria-controls (or make the control focusable and expose state).

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +155
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
try
{
await InvokeAsync(SyncThemeFromJs);

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnLocationChanged is an async void handler that swallows all exceptions. This can hide real failures (and async void exceptions can crash the renderer in some cases). Consider making the handler synchronous and starting a fire-and-forget task with explicit exception handling/logging, or at least capture/log exceptions in the catch block.

Copilot uses AI. Check for mistakes.
<button type="button" class="btn-close" @onclick="() => _error = null"></button>
<div class="rounded-lg border border-red-300 bg-red-50 text-red-700 px-4 py-3 text-sm mb-4 flex items-center justify-between" role="alert">
<span>@_error</span>
<button type="button" class="ml-4 cursor-pointer text-red-700 hover:text-red-900" @onclick="() => _error = null">&times;</button>

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dismiss button for the error alert only renders “×” and has no accessible name. Add an aria-label (e.g., “Close error”) so screen readers can identify the control.

Suggested change
<button type="button" class="ml-4 cursor-pointer text-red-700 hover:text-red-900" @onclick="() => _error = null">&times;</button>
<button type="button" class="ml-4 cursor-pointer text-red-700 hover:text-red-900" aria-label="Close error" @onclick="() => _error = null">&times;</button>

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("MyBlog.Unit.Tests")]

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InternalsVisibleTo includes MyBlog.Unit.Tests, but the unit test assembly name appears to be Unit.Tests (default from the project file name). The extra attribute likely won’t match any assembly and unnecessarily widens friend-assembly access. Consider keeping only the correct assembly name (or explicitly set <AssemblyName> in the test csproj and align this list).

Suggested change
[assembly: InternalsVisibleTo("MyBlog.Unit.Tests")]

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +52
catch (JsonException)
{

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExpandRoleValues falls through to the comma-splitting logic when JSON parsing fails. For a malformed JSON array value that still contains commas, this will produce broken role strings like ["Admin" rather than failing safely. Consider returning an empty list (or a single trimmed value) when JsonDocument.Parse throws, and avoid swallowing the exception silently.

Suggested change
catch (JsonException)
{
catch (JsonException exception)
{
System.Diagnostics.Trace.TraceWarning(
"Failed to parse role claim value as JSON array: {0}",
exception.Message);
return [];

Copilot uses AI. Check for mistakes.

@mpaulosky mpaulosky left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧙 Gandalf's Security Review — Gate Assessment

Verdict: ❌ REQUEST CHANGES — 2 blocking issues before merge


🔴 [HIGH] Open Redirect — src/Web/Program.cs:111

returnUrl from the query string is passed directly to WithRedirectUri(returnUrl ?? "/") with no local-path validation. An attacker can craft /Account/Login?returnUrl=https://evil.com and after Auth0 authentication the user lands on a phishing site.

Fix required:

var safeReturn = Uri.IsWellFormedUriString(returnUrl, UriKind.Relative) ? returnUrl : "/";
WithRedirectUri(safeReturn)

�� [MEDIUM] Null dereference breaks all logins — src/Web/Program.cs:49

existingOnTokenValidated is captured before PostConfigure runs; Auth0 SDK may leave it null. await existingOnTokenValidated(context) throws NullReferenceException silently, breaking login for all users.

Fix required:

if (existingOnTokenValidated is not null)
    await existingOnTokenValidated(context);

ℹ️ [INFO] Full JWT claim exposure on /profile

All claims (sub, auth_time, nonce) rendered to authenticated users. No immediate risk but consider filtering to display-safe claims only.


✅ Clean Areas

  • RoleClaimsHelper — correct deduplication, no privilege escalation path
  • Route authorization — all endpoints correctly gated
  • TestAuthorizationService — properly internal sealed, scoped to test assembly
  • Middleware pipeline order — correct
  • No secrets in source

The security fundamentals are solid — role normalization, route guards, and auth middleware are all correct. Fix the open redirect and null guard, then this gets an ✅.

— Gandalf, Squad Reviewer

@mpaulosky mpaulosky left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚒️ Gimli's Test Review

Verdict: REQUEST CHANGES — squad rules violations


🔴 Missing File Headers (Rule #6)

ALL 6 new test files lack the required block-format copyright header present in all existing test files.

Affected:

  • tests/Unit.Tests/Components/Layout/NavMenuTests.cs
  • tests/Unit.Tests/Components/RazorSmokeTests.cs
  • tests/Unit.Tests/Features/UserManagement/ProfileTests.cs
  • tests/Unit.Tests/ResultTests.cs
  • tests/Unit.Tests/Security/RoleClaimsHelperTests.cs
  • tests/Unit.Tests/Testing/TestAuthorizationService.cs

🔴 Missing AAA Comments (Rule #3)

No test methods use // Arrange, // Act, // Assert markers as required by charter and demonstrated in existing tests/Unit.Tests/Handlers/*.cs files.


✅ Passes

  • FluentAssertions .Should() used throughout ✅
  • NSubstitute for mocking ✅
  • File-scoped namespaces ✅
  • Correct namespace pattern MyBlog.Unit.Tests.{Folder}
  • bUnit configured properly with BunitContext
  • 89% coverage threshold configured ✅
  • No Dto.Empty comparison anti-patterns ✅

Coverage quality is excellent — NavMenu auth states, Profile claims, RoleClaimsHelper theory tests, smoke tests. Just needs the two formatting rules applied.

— Gimli, Squad Tester

@mpaulosky mpaulosky left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧙 Gandalf's Gate Review — PR #2: test: add component coverage gate and auth UI tests

"You shall not pass — until these are fixed."

Branch: squad/coverage-test-hardeningmain
Verdict: ⚠️ REQUEST CHANGES

This PR ships genuinely valuable work — the bUnit smoke suite, the Auth0 role-normalization pipeline, and the Coverlet gate are all the right investments. But two P1 defects (one crash, one security) must land before this touches main. I also have two accessibility P2s and one additional security observation.


❌ P1 — MUST FIX (Blocking)

1. Program.cs — Null-reference crash in the Auth pipeline

var existingOnTokenValidated = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
    await existingOnTokenValidated(context);   // ← NullReferenceException

OpenIdConnectEvents.OnTokenValidated is null by default in a fresh OIDC registration. The Auth0 SDK may or may not pre-populate it before PostConfigure runs — if it does not, every token validation will throw NullReferenceException, silently aborting the login flow. The fix is a one-liner null guard:

if (existingOnTokenValidated is not null)
    await existingOnTokenValidated(context);

This is a silent authentication failure that will not appear in tests (token validation is not exercised by the bUnit suite) but will manifest in every production login attempt on a fresh deployment.


2. RoleClaimsHelper.cs:52 — Malformed JSON produces broken role strings (Copilot comment #6 — confirmed P1)

Copilot flagged this correctly. The fallthrough path is:

Input: '["Admin","Auth   ← malformed/truncated JSON
  → JsonException caught, silently swallowed
  → falls through to comma-split
  → produces: [ "[\"Admin\"", "\"Auth" ]

A claim value beginning with [ that is not valid JSON will produce role names containing JSON syntax characters (brackets, quotes). These garbage strings will never match any [Authorize(Roles=...)] attribute, so they won't grant unauthorized access. But they will silently discard legitimate roles for a user whose token is slightly malformed (e.g., an older Auth0 action producing non-standard JSON), causing denied-access false negatives.

The fix should log the JsonException (even at Debug level) and/or explicitly return [] after a parse failure rather than falling through to comma splitting:

catch (JsonException ex)
{
    logger.LogDebug(ex, "Failed to parse role claim as JSON array: {Value}", trimmed);
    return [];   // do not fall through — malformed JSON is not a CSV
}

⚠️ P2 — Should Fix (Non-blocking but expected before merge)

3. NavMenu.razor:19 — Mobile hamburger inaccessible to AT (Copilot comment #2 — confirmed P2)

<input type=checkbox id="menu-toggle" class="peer sr-only" aria-hidden="true"/>
<label for="menu-toggle" aria-label="Toggle navigation"></label>

The <label> has aria-label — good. But aria-hidden="true" on the checkbox removes its checked/unchecked state from the accessibility tree. Screen reader users will activate the label (announcing "Toggle navigation"), but the AT will report no state change because the associated control is hidden from it. The mobile menu will silently open/close with no feedback. Remove aria-hidden from the checkbox input — the sr-only class already positions it off-screen visually.


4. ManageRoles.razor:23 — Dismiss button has no accessible name (Copilot comment #4 — confirmed P2)

<button type="button"  @onclick="() => _error = null">&times;</button>

&times; (×) is a Unicode symbol. Screen readers may announce it as "times" or "multiplication sign". Add aria-label="Dismiss error" to give the control a meaningful name.


5. Profile.razor — Full claims table exposes OIDC internals (new finding)

The page renders _user.Claims.OrderBy(…).ToList() with no filtering. This includes internal OIDC claims: nonce, c_hash, at_hash, aud, iss, iat, exp, and any Auth0 internal metadata claims. While Blazor HTML-encodes output (no XSS risk), this is an information-disclosure concern:

  • Exposes audience identifiers and issuer URLs that aid OIDC enumeration.
  • Exposes the raw sub identifier used as the Auth0 user ID (already shown separately, but doubly visible).
  • If Auth0 ever includes a short-lived opaque token claim, it appears here verbatim.

Recommendation: filter to an allowlist of user-facing claim types, or add a note in the UI (and PR description) marking this page as a developer/debug feature restricted in production.


6. TestAuthorizationService — Policy-based overload always grants authenticated users (new finding)

public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName)
    => Task.FromResult(user.Identity?.IsAuthenticated == true
        ? AuthorizationResult.Success()
        : AuthorizationResult.Failed());

This means every bUnit test that hits a [Authorize(Policy="SomePolicy")] page will pass authorization regardless of what the policy actually requires. If a future policy (e.g., RequireEmailVerified) is added, the tests will not catch a missing guard. Consider routing policyName == Auth0Constants.AuthenticationScheme explicitly, or at minimum document the deliberate simplification.


🟡 P3 — Nice to Have

7. RoleClaimsHelper.cs:83GetRoles parameter fully replaces defaults (Copilot comment #1 — P3)

var types = (roleClaimTypes ?? DefaultRoleClaimTypes.Append(ClaimTypes.Role))

When roleClaimTypes is passed, DefaultRoleClaimTypes are excluded. The Profile page calls GetRoles(_user) with no argument, so it works correctly today. But AddRoleClaims takes explicit types from startup config — the two code paths use different type sets, which means the claims added at login may not perfectly match what the Profile page displays. This is a latent inconsistency, not a current bug. Document the intended semantics.

8. NavMenu.razor:161async void on event handler (Copilot comment #3 — P3)

The empty catch {} makes this functionally safe, but async void is still an anti-pattern. If a future contributor removes the try/catch, it becomes a process-crashing unhandled exception. Prefer a Task-returning inner method and fire-and-forget pattern:

private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    => InvokeAsync(() => SyncThemeFromJs()).ContinueWith(_ => { });

9. AssemblyInfo.cs — Dual InternalsVisibleTo entries (Copilot comment #5 — P3, acceptable)

Both "MyBlog.Unit.Tests" and "Unit.Tests" are present. The csproj sets <RootNamespace>MyBlog.Unit.Tests</RootNamespace> but no <AssemblyName> override, so the actual assembly name defaults to Unit.Tests (the project filename). The first entry is redundant but harmless. Remove InternalsVisibleTo("MyBlog.Unit.Tests") to avoid confusion, but this is not blocking.


📋 Merge Checklist

# Issue File Severity Status
1 Null-ref crash on OnTokenValidated Program.cs P1 �� Must fix
2 Malformed JSON fallthrough → broken role strings RoleClaimsHelper.cs:52 P1 🔴 Must fix
3 Mobile hamburger AT inaccessible (aria-hidden on checkbox) NavMenu.razor:19 P2 🟡 Should fix
4 Dismiss button no accessible name ManageRoles.razor:23 P2 🟡 Should fix
5 Profile page exposes all OIDC internal claims Profile.razor P2 🟡 Should fix
6 Test auth service policy overload masks regressions TestAuthorizationService.cs P2 🟡 Should fix
7 GetRoles vs AddRoleClaims type-set inconsistency RoleClaimsHelper.cs:83 P3 🔵 Nice to have
8 async void on nav event handler NavMenu.razor:161 P3 🔵 Nice to have
9 Redundant InternalsVisibleTo entry AssemblyInfo.cs:3 P3 🔵 Nice to have
10 Branch conflicts with main Infra 🔴 Must resolve

The auth pipeline crash (#1) and the JSON fallthrough (#2) are gates. Fix those two, address the P2 accessibility issues, and this PR earns the ring.

— Gandalf, Squad Reviewer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

squad Squad triage inbox — Lead will assign to a member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants