docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)

Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.Security.Tests;
/// <summary>
/// Tests for the cookie <c>OnValidatePrincipal</c> core (<see cref="CookieSessionValidator"/>)
/// and the shared <see cref="SessionClaimBuilder"/>. These pin the always-achievable M2.19
/// and the shared <see cref="SessionClaimBuilder"/>. These pin the always-achievable
/// layers: idle-timeout enforcement, mid-session role-mapping refresh from STORED groups
/// WITHOUT any LDAP call, the fail-soft refresh policy (errors keep the session), and claim
/// parity between the login path and the refresh path.
@@ -29,8 +29,13 @@ public class CookieSessionValidatorTests
private sealed class TestTimeProvider : TimeProvider
{
private DateTimeOffset _now;
/// <summary>Creates a clock fixed at the given start time.</summary>
/// <param name="start">The initial time reported by the clock.</param>
public TestTimeProvider(DateTimeOffset start) => _now = start;
/// <inheritdoc />
public override DateTimeOffset GetUtcNow() => _now;
/// <summary>Advances the clock by the given amount.</summary>
/// <param name="by">The amount of time to advance.</param>
public void Advance(TimeSpan by) => _now = _now.Add(by);
}
@@ -42,15 +47,27 @@ public class CookieSessionValidatorTests
{
private RoleMappingResult _result;
private readonly Exception? _throw;
/// <summary>The number of times <c>MapAsync</c> has been invoked.</summary>
public int CallCount { get; private set; }
/// <summary>The groups passed to the most recent <c>MapAsync</c> call.</summary>
public IReadOnlyList<string>? LastGroups { get; private set; }
/// <summary>Creates a mapper that returns the given result.</summary>
/// <param name="result">The mapping result to return.</param>
public FakeGroupRoleMapper(RoleMappingResult result) => _result = result;
/// <summary>Creates a mapper that throws the given exception.</summary>
/// <param name="toThrow">The exception to throw from <c>MapAsync</c>.</param>
public FakeGroupRoleMapper(Exception toThrow) => (_result, _throw) =
(new RoleMappingResult([], [], false), toThrow);
/// <summary>Replaces the result returned by subsequent mapping calls.</summary>
/// <param name="result">The new mapping result.</param>
public void SetResult(RoleMappingResult result) => _result = result;
/// <summary>Records the call and returns the canned mapping, or throws the canned exception.</summary>
/// <param name="groups">The directory groups being mapped.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>A task yielding the canned role mapping.</returns>
public Task<GroupRoleMapping<string>> MapAsync(IReadOnlyList<string> groups, CancellationToken ct)
{
CallCount++;
@@ -83,6 +100,8 @@ public class CookieSessionValidatorTests
// --- Idle timeout ---
/// <summary>Verifies that validation rejects a session that is past the idle timeout.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_PastIdleTimeout_Rejects()
{
@@ -100,6 +119,8 @@ public class CookieSessionValidatorTests
Assert.Equal(0, mapper.CallCount);
}
/// <summary>Verifies that validation keeps a session that is within the idle window.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_WithinIdleWindow_KeptAndNotRejected()
{
@@ -116,6 +137,8 @@ public class CookieSessionValidatorTests
Assert.Equal(SessionValidationAction.Keep, result.Action);
}
/// <summary>Verifies that a missing last-activity claim is treated as idle timed out.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_MissingLastActivityClaim_TreatedAsIdleTimedOut()
{
@@ -137,6 +160,8 @@ public class CookieSessionValidatorTests
Assert.Equal(SessionValidationAction.Reject, result.Action);
}
/// <summary>Verifies that an unauthenticated principal is kept unchanged.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_UnauthenticatedPrincipal_Kept()
{
@@ -151,6 +176,8 @@ public class CookieSessionValidatorTests
// --- Role-mapping refresh WITHOUT LDAP ---
/// <summary>Verifies that after the role refresh threshold roles are remapped from the stored groups without an LDAP call.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_AfterRoleRefreshThreshold_RemapsFromStoredGroups_NoLdap()
{
@@ -188,6 +215,8 @@ public class CookieSessionValidatorTests
Assert.Equal(clock.GetUtcNow(), refreshedAt);
}
/// <summary>Verifies that a user whose group was revoked loses the roles on refresh.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_RevokedGroupUser_LosesRolesOnRefresh()
{
@@ -208,6 +237,8 @@ public class CookieSessionValidatorTests
Assert.Empty(result.Principal.FindAll(JwtTokenService.SiteIdClaimType));
}
/// <summary>Verifies that validation does not refresh roles before the refresh threshold.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_BeforeRoleRefreshThreshold_DoesNotRefresh()
{
@@ -229,6 +260,8 @@ public class CookieSessionValidatorTests
// --- Failure policy: a refresh exception keeps the existing principal ---
/// <summary>Verifies that a throwing refresh keeps the existing principal without signing out or throwing.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_RefreshThrows_KeepsExistingPrincipal_NoSignOut_NoThrow()
{
@@ -250,6 +283,7 @@ public class CookieSessionValidatorTests
// --- IsIdleTimedOut / IsRoleRefreshDue boundaries ---
/// <summary>Verifies that a session exactly at the idle boundary is not timed out.</summary>
[Fact]
public void IsIdleTimedOut_AtBoundary_NotTimedOut()
{
@@ -262,6 +296,7 @@ public class CookieSessionValidatorTests
Assert.True(sut.IsIdleTimedOut(principal, Start.AddMinutes(30).AddSeconds(1)));
}
/// <summary>Verifies that a role refresh exactly at the boundary is not yet due.</summary>
[Fact]
public void IsRoleRefreshDue_AtBoundary_NotDue()
{
@@ -275,6 +310,8 @@ public class CookieSessionValidatorTests
// --- Missing/unparsable zb:lastrolerefresh anchor treated as refresh-due ---
/// <summary>Verifies that a missing last-role-refresh claim is treated as due, refreshes the roles and restamps the claim.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ValidateAsync_MissingLastRoleRefreshClaim_TreatedAsRefreshDue_RefreshesAndRestamps()
{
@@ -319,6 +356,7 @@ public class CookieSessionValidatorTests
Assert.True(DateTimeOffset.TryParse(refreshClaim!.Value, out _));
}
/// <summary>Verifies that a missing refresh claim makes the role refresh due.</summary>
[Fact]
public void IsRoleRefreshDue_MissingClaim_ReturnsDue()
{
@@ -332,6 +370,7 @@ public class CookieSessionValidatorTests
Assert.True(sut.IsRoleRefreshDue(principal, Start));
}
/// <summary>Verifies that an unparsable refresh claim makes the role refresh due.</summary>
[Fact]
public void IsRoleRefreshDue_UnparsableClaim_ReturnsDue()
{
@@ -351,7 +390,7 @@ public class CookieSessionValidatorTests
/// <summary>
/// Claim-parity tests: the shared <see cref="SessionClaimBuilder"/> must produce EXACTLY
/// the claim shape the login endpoint historically minted (plus the M2.19 group +
/// the claim shape the login endpoint historically minted (plus the group +
/// refresh-anchor additions), and the refresh path — which also goes through the builder —
/// must therefore be identical. This is the load-bearing anti-drift guarantee.
/// </summary>
@@ -359,6 +398,7 @@ public class SessionClaimBuilderParityTests
{
private static readonly DateTimeOffset At = new(2026, 6, 15, 9, 0, 0, TimeSpan.Zero);
/// <summary>Verifies that the builder produces the canonical identity, role and scope claims like the login path.</summary>
[Fact]
public void Build_ProducesCanonicalIdentityRoleAndScopeClaims_LikeLogin()
{
@@ -382,6 +422,7 @@ public class SessionClaimBuilderParityTests
Assert.Equal(new[] { "3", "4" }.OrderBy(s => s), sites.OrderBy(s => s));
}
/// <summary>Verifies that a system-wide mapping omits the site id claims.</summary>
[Fact]
public void Build_SystemWideMapping_OmitsSiteIdClaims()
{
@@ -394,6 +435,7 @@ public class SessionClaimBuilderParityTests
Assert.Contains(Roles.Deployer, principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value));
}
/// <summary>Verifies that the builder stores the groups and the refresh anchor claims.</summary>
[Fact]
public void Build_StoresGroupsAndRefreshAnchor_M219Additions()
{
@@ -401,14 +443,15 @@ public class SessionClaimBuilderParityTests
var principal = SessionClaimBuilder.Build("janer", "Jane Roe", ["G1", "G2"], mapping, At);
// M2.19: one zb:group claim per LDAP group, round-trippable via ReadGroups.
// One zb:group claim per LDAP group, round-trippable via ReadGroups.
Assert.Equal(new[] { "G1", "G2" }, SessionClaimBuilder.ReadGroups(principal));
// M2.19: refresh anchor AND idle anchor seeded from the same instant.
// Refresh anchor AND idle anchor seeded from the same instant.
Assert.Equal(At.ToString("o"), principal.FindFirst(JwtTokenService.LastRoleRefreshClaimType)?.Value);
Assert.Equal(At.ToString("o"), principal.FindFirst(JwtTokenService.LastActivityClaimType)?.Value);
}
/// <summary>Verifies that the login and refresh paths produce an identical claim shape.</summary>
[Fact]
public void Build_LoginThenRefresh_ProduceIdenticalClaimShape()
{
@@ -458,20 +501,46 @@ public class OnValidatePrincipalAdapterTests
// the Reject path actually invokes SignOutAsync.
private sealed class StubAuthenticationService : IAuthenticationService
{
/// <summary>The number of times <c>SignOutAsync</c> has been called.</summary>
public int SignOutCallCount { get; private set; }
/// <summary>Returns a no-result authentication outcome.</summary>
/// <param name="context">The HTTP context (ignored).</param>
/// <param name="scheme">The authentication scheme (ignored).</param>
/// <returns>A task yielding a no-result outcome.</returns>
public Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string? scheme) =>
Task.FromResult(AuthenticateResult.NoResult());
/// <summary>Does nothing.</summary>
/// <param name="context">The HTTP context (ignored).</param>
/// <param name="scheme">The authentication scheme (ignored).</param>
/// <param name="properties">The authentication properties (ignored).</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) =>
Task.CompletedTask;
/// <summary>Does nothing.</summary>
/// <param name="context">The HTTP context (ignored).</param>
/// <param name="scheme">The authentication scheme (ignored).</param>
/// <param name="properties">The authentication properties (ignored).</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task ForbidAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) =>
Task.CompletedTask;
/// <summary>Does nothing.</summary>
/// <param name="context">The HTTP context (ignored).</param>
/// <param name="scheme">The authentication scheme (ignored).</param>
/// <param name="principal">The principal (ignored).</param>
/// <param name="properties">The authentication properties (ignored).</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task SignInAsync(HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties) =>
Task.CompletedTask;
/// <summary>Counts the sign-out call.</summary>
/// <param name="context">The HTTP context (ignored).</param>
/// <param name="scheme">The authentication scheme (ignored).</param>
/// <param name="properties">The authentication properties (ignored).</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task SignOutAsync(HttpContext context, string? scheme, AuthenticationProperties? properties)
{
SignOutCallCount++;
@@ -515,6 +584,8 @@ public class OnValidatePrincipalAdapterTests
new RoleMappingResult([Roles.Administrator], [], true),
AdapterStart);
/// <summary>Verifies that applying a reject result rejects the principal and signs out.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ApplyResult_Reject_CallsRejectPrincipalAndSignsOut()
{
@@ -533,6 +604,8 @@ public class OnValidatePrincipalAdapterTests
Assert.False(ctx.ShouldRenew);
}
/// <summary>Verifies that applying a replace result replaces the principal and requests a renewal.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ApplyResult_Replace_ReplacesAndSetsRenew()
{
@@ -553,6 +626,8 @@ public class OnValidatePrincipalAdapterTests
Assert.Equal(0, authService.SignOutCallCount);
}
/// <summary>Verifies that applying a keep result leaves the context unchanged.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ApplyResult_Keep_LeavesContextUnchanged()
{
@@ -587,6 +662,7 @@ public class SecurityOptionsValidatorConfigGuardTests
RoleRefreshThresholdMinutes = 15,
};
/// <summary>Verifies that the default options pass validation.</summary>
[Fact]
public void Validate_DefaultOptions_Succeeds()
{
@@ -594,6 +670,7 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.True(result.Succeeded);
}
/// <summary>Verifies that validation fails when the refresh interval equals the idle timeout.</summary>
[Fact]
public void Validate_RefreshEqualsIdle_Fails()
{
@@ -609,6 +686,7 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.Contains(nameof(SecurityOptions.IdleTimeoutMinutes), result.FailureMessage);
}
/// <summary>Verifies that validation fails when the refresh interval is greater than the idle timeout.</summary>
[Fact]
public void Validate_RefreshGreaterThanIdle_Fails()
{
@@ -623,6 +701,7 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.Contains(nameof(SecurityOptions.RoleRefreshThresholdMinutes), result.FailureMessage);
}
/// <summary>Verifies that validation succeeds when the refresh interval is one less than the idle timeout.</summary>
[Fact]
public void Validate_RefreshOneLessThanIdle_Succeeds()
{
@@ -636,6 +715,7 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.True(result.Succeeded);
}
/// <summary>Verifies that AddSecurity registers the security options validator with validation on start.</summary>
[Fact]
public void AddSecurity_RegistersSecurityOptionsValidator_WithValidateOnStart()
{
@@ -652,6 +732,7 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.Contains(validators, v => v is SecurityOptionsValidator);
}
/// <summary>Verifies that the registered validator detects a misconfiguration when called directly.</summary>
[Fact]
public void AddSecurity_RegisteredValidator_DetectsMisconfiguration_WhenCalledDirectly()
{
@@ -682,8 +763,9 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.Contains(nameof(SecurityOptions.RoleRefreshThresholdMinutes), result.FailureMessage);
}
// ── arch-review 2026-08-19 P6-9: PerIpMaxFailures must exceed the per-pair ceiling ──
// ── PerIpMaxFailures must exceed the per-pair ceiling ──
/// <summary>Verifies that the default per-IP maximum failures setting passes validation.</summary>
[Fact]
public void Validate_DefaultPerIpMaxFailures_Succeeds()
{
@@ -691,6 +773,7 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.True(result.Succeeded);
}
/// <summary>Verifies that validation fails when the per-IP maximum failures equals the per-pair ceiling.</summary>
[Fact]
public void Validate_PerIpMaxFailuresEqualsPerPairCeiling_Fails()
{
@@ -704,6 +787,7 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.Contains(nameof(SecurityOptions.MaxLoginFailuresPerWindow), result.FailureMessage);
}
/// <summary>Verifies that validation fails when the per-IP maximum failures is below the per-pair ceiling.</summary>
[Fact]
public void Validate_PerIpMaxFailuresBelowPerPairCeiling_Fails()
{
@@ -716,6 +800,7 @@ public class SecurityOptionsValidatorConfigGuardTests
Assert.Contains(nameof(SecurityOptions.PerIpMaxFailures), result.FailureMessage);
}
/// <summary>Verifies that validation succeeds when the per-IP maximum failures is one above the per-pair ceiling.</summary>
[Fact]
public void Validate_PerIpMaxFailuresOneAbovePerPairCeiling_Succeeds()
{