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:
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user