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:
@@ -18,7 +18,7 @@ namespace ZB.MOM.WW.ScadaBridge.Security.Tests;
|
||||
|
||||
#region WP-6: LdapAuthService Tests (re-aimed at the shared ZB.MOM.WW.Auth.Ldap service)
|
||||
|
||||
// Task 1.2 cutover: ScadaBridge's bespoke LdapAuthService was replaced by the shared
|
||||
// Cutover: ScadaBridge's bespoke LdapAuthService was replaced by the shared
|
||||
// ZB.MOM.WW.Auth.Ldap.LdapAuthService (ScadaBridge was the donor for its hardened
|
||||
// bind-then-search / RFC-4514+4515 escaping / fail-closed / service-account-bind-
|
||||
// distinction / per-op-timeout semantics). The deep hygiene parity now lives in the
|
||||
@@ -40,6 +40,8 @@ public class LdapAuthServiceTests
|
||||
ServiceAccountDn = "cn=admin,dc=example,dc=com",
|
||||
};
|
||||
|
||||
/// <summary>Verifies the case: authenticate Async, disabled, fails With Disabled, no Network.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_Disabled_FailsWithDisabled_NoNetwork()
|
||||
{
|
||||
@@ -52,6 +54,9 @@ public class LdapAuthServiceTests
|
||||
Assert.Equal(LdapAuthFailure.Disabled, result.Failure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: authenticate Async, empty Username, fails Bad Credentials, no Network.</summary>
|
||||
/// <param name="username">The username test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
@@ -68,10 +73,12 @@ public class LdapAuthServiceTests
|
||||
Assert.Equal(LdapAuthFailure.BadCredentials, result.Failure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: authenticate Async, connection Failure, fails Closed, never Throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_ConnectionFailure_FailsClosed_NeverThrows()
|
||||
{
|
||||
// Security-025: point at a guaranteed-unroutable loopback address (127.0.0.1 on a
|
||||
// Point at a guaranteed-unroutable loopback address (127.0.0.1 on a
|
||||
// closed high port) rather than DNS-resolving "nonexistent.invalid". The connect is
|
||||
// refused deterministically and immediately, with no external DNS dependency and no
|
||||
// multi-second timeout dead-time, so this stays a fast, network-sandbox-safe unit
|
||||
@@ -114,6 +121,7 @@ public class JwtTokenServiceTests
|
||||
NullLogger<JwtTokenService>.Instance);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: generate Token, contains Correct Claims.</summary>
|
||||
[Fact]
|
||||
public void GenerateToken_ContainsCorrectClaims()
|
||||
{
|
||||
@@ -140,6 +148,7 @@ public class JwtTokenServiceTests
|
||||
Assert.NotNull(principal.FindFirst(JwtTokenService.LastActivityClaimType));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: generate Token, null Site Ids, no Site Id Claims.</summary>
|
||||
[Fact]
|
||||
public void GenerateToken_NullSiteIds_NoSiteIdClaims()
|
||||
{
|
||||
@@ -151,6 +160,7 @@ public class JwtTokenServiceTests
|
||||
Assert.Empty(principal!.FindAll(JwtTokenService.SiteIdClaimType));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: validate Token, invalid Token, returns Null.</summary>
|
||||
[Fact]
|
||||
public void ValidateToken_InvalidToken_ReturnsNull()
|
||||
{
|
||||
@@ -159,6 +169,7 @@ public class JwtTokenServiceTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: validate Token, wrong Key, returns Null.</summary>
|
||||
[Fact]
|
||||
public void ValidateToken_WrongKey_ReturnsNull()
|
||||
{
|
||||
@@ -176,6 +187,7 @@ public class JwtTokenServiceTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: validate Token, uses Hmac Sha256.</summary>
|
||||
[Fact]
|
||||
public void ValidateToken_UsesHmacSha256()
|
||||
{
|
||||
@@ -189,6 +201,7 @@ public class JwtTokenServiceTests
|
||||
Assert.Contains("HS256", headerJson);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: should Refresh, token Near Expiry, returns True.</summary>
|
||||
[Fact]
|
||||
public void ShouldRefresh_TokenNearExpiry_ReturnsTrue()
|
||||
{
|
||||
@@ -202,6 +215,7 @@ public class JwtTokenServiceTests
|
||||
Assert.True(service.ShouldRefresh(principal!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: should Refresh, token Far From Expiry, returns False.</summary>
|
||||
[Fact]
|
||||
public void ShouldRefresh_TokenFarFromExpiry_ReturnsFalse()
|
||||
{
|
||||
@@ -212,6 +226,7 @@ public class JwtTokenServiceTests
|
||||
Assert.False(service.ShouldRefresh(principal!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: is Idle Timed Out, recent Activity, returns False.</summary>
|
||||
[Fact]
|
||||
public void IsIdleTimedOut_RecentActivity_ReturnsFalse()
|
||||
{
|
||||
@@ -222,6 +237,7 @@ public class JwtTokenServiceTests
|
||||
Assert.False(service.IsIdleTimedOut(principal!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: is Idle Timed Out, no Last Activity Claim, returns True.</summary>
|
||||
[Fact]
|
||||
public void IsIdleTimedOut_NoLastActivityClaim_ReturnsTrue()
|
||||
{
|
||||
@@ -234,6 +250,7 @@ public class JwtTokenServiceTests
|
||||
Assert.True(service.IsIdleTimedOut(principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: refresh Token, returns New Token With Updated Claims.</summary>
|
||||
[Fact]
|
||||
public void RefreshToken_ReturnsNewTokenWithUpdatedClaims()
|
||||
{
|
||||
@@ -249,6 +266,7 @@ public class JwtTokenServiceTests
|
||||
Assert.Contains("Designer", roles);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: refresh Token, missing Claims, returns Null.</summary>
|
||||
[Fact]
|
||||
public void RefreshToken_MissingClaims_ReturnsNull()
|
||||
{
|
||||
@@ -270,6 +288,7 @@ public class RoleMapperTests : IDisposable
|
||||
private readonly SecurityRepository _securityRepo;
|
||||
private readonly RoleMapper _roleMapper;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RoleMapperTests"/> class.</summary>
|
||||
public RoleMapperTests()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
|
||||
@@ -283,12 +302,15 @@ public class RoleMapperTests : IDisposable
|
||||
_roleMapper = new RoleMapper(_securityRepo);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: map Groups To Roles, multi Role Extraction.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_MultiRoleExtraction()
|
||||
{
|
||||
@@ -304,6 +326,8 @@ public class RoleMapperTests : IDisposable
|
||||
Assert.DoesNotContain(Roles.Deployer, result.Roles);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: map Groups To Roles, site Scoped Deployment.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_SiteScopedDeployment()
|
||||
{
|
||||
@@ -327,10 +351,12 @@ public class RoleMapperTests : IDisposable
|
||||
Assert.Contains(site2.Id.ToString(), result.PermittedSiteIds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: map Groups To Roles, user In Both System Wide And Scoped Deployment Group, is System Wide.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_UserInBothSystemWideAndScopedDeploymentGroup_IsSystemWide()
|
||||
{
|
||||
// Security-016: a user in BOTH an unscoped Deployer mapping
|
||||
// A user in BOTH an unscoped Deployer mapping
|
||||
// (SCADA-Deploy-All, Id=3) AND a scoped Deployer mapping
|
||||
// (SCADA-Deploy-SiteA, Id=4) used to be silently narrowed to the site-A
|
||||
// grant. The union semantics now preserve the broader grant: the
|
||||
@@ -350,6 +376,8 @@ public class RoleMapperTests : IDisposable
|
||||
Assert.Empty(result.PermittedSiteIds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: map Groups To Roles, system Wide Deployment, no Scope Rules.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_SystemWideDeployment_NoScopeRules()
|
||||
{
|
||||
@@ -363,6 +391,8 @@ public class RoleMapperTests : IDisposable
|
||||
Assert.Empty(result.PermittedSiteIds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: map Groups To Roles, unrecognized Groups, ignored.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_UnrecognizedGroups_Ignored()
|
||||
{
|
||||
@@ -373,6 +403,8 @@ public class RoleMapperTests : IDisposable
|
||||
Assert.False(result.IsSystemWideDeployment);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: map Groups To Roles, no Matching Groups, no Roles.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_NoMatchingGroups_NoRoles()
|
||||
{
|
||||
@@ -381,10 +413,12 @@ public class RoleMapperTests : IDisposable
|
||||
Assert.Empty(result.Roles);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: map Groups To Roles, case Insensitive Group Match.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_CaseInsensitiveGroupMatch()
|
||||
{
|
||||
// "SCADA-Admins" is seeded (role canonicalized to Administrator, Task 1.7)
|
||||
// "SCADA-Admins" is seeded (role canonicalized to Administrator)
|
||||
var result = await _roleMapper.MapGroupsToRolesAsync(new[] { "scada-admins" });
|
||||
|
||||
Assert.Contains(Roles.Administrator, result.Roles);
|
||||
@@ -396,12 +430,12 @@ public class RoleMapperTests : IDisposable
|
||||
#region Code Review Regression Tests
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for Security-001 (StartTLS dead code), Security-002 (cookie not
|
||||
/// marked Secure), and Security-003 (JWT signing key length never validated).
|
||||
/// Regression tests for StartTLS dead code, cookie not
|
||||
/// marked Secure), and JWT signing key length never validated.
|
||||
/// </summary>
|
||||
public class SecurityReviewRegressionTests
|
||||
{
|
||||
// --- Security-003: JWT signing key length validation ---
|
||||
// --- JWT signing key length validation ---
|
||||
|
||||
private static SecurityOptions JwtOptions(string signingKey) => new()
|
||||
{
|
||||
@@ -411,6 +445,7 @@ public class SecurityReviewRegressionTests
|
||||
JwtRefreshThresholdMinutes = 5
|
||||
};
|
||||
|
||||
/// <summary>Verifies the case: jwt Token Service, empty Signing Key, throws At Construction.</summary>
|
||||
[Fact]
|
||||
public void JwtTokenService_EmptySigningKey_ThrowsAtConstruction()
|
||||
{
|
||||
@@ -419,6 +454,7 @@ public class SecurityReviewRegressionTests
|
||||
Assert.Contains("JwtSigningKey", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: jwt Token Service, short Signing Key, throws At Construction.</summary>
|
||||
[Fact]
|
||||
public void JwtTokenService_ShortSigningKey_ThrowsAtConstruction()
|
||||
{
|
||||
@@ -429,6 +465,7 @@ public class SecurityReviewRegressionTests
|
||||
Assert.Contains("32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: jwt Token Service, adequate Signing Key, constructs Successfully.</summary>
|
||||
[Fact]
|
||||
public void JwtTokenService_AdequateSigningKey_ConstructsSuccessfully()
|
||||
{
|
||||
@@ -438,8 +475,9 @@ public class SecurityReviewRegressionTests
|
||||
Assert.False(string.IsNullOrEmpty(token));
|
||||
}
|
||||
|
||||
// --- Security-002: authentication cookie must be marked Secure ---
|
||||
// --- authentication cookie must be marked Secure ---
|
||||
|
||||
/// <summary>Verifies the case: add Security, auth Cookie, is Marked Secure Always.</summary>
|
||||
[Fact]
|
||||
public void AddSecurity_AuthCookie_IsMarkedSecureAlways()
|
||||
{
|
||||
@@ -462,11 +500,12 @@ public class SecurityReviewRegressionTests
|
||||
Assert.True(cookieOptions.Cookie.HttpOnly);
|
||||
}
|
||||
|
||||
// --- CentralUI-005: cookie auth must use a sliding session window ---
|
||||
// --- cookie auth must use a sliding session window ---
|
||||
// Documented policy (CLAUDE.md Security & Auth): sliding refresh with a
|
||||
// 30-minute idle timeout. The cookie middleware must enable SlidingExpiration
|
||||
// so an active session is renewed on activity and an idle session expires.
|
||||
|
||||
/// <summary>Verifies the case: add Security, auth Cookie, uses Sliding Expiration.</summary>
|
||||
[Fact]
|
||||
public void AddSecurity_AuthCookie_UsesSlidingExpiration()
|
||||
{
|
||||
@@ -483,6 +522,7 @@ public class SecurityReviewRegressionTests
|
||||
Assert.True(cookieOptions.SlidingExpiration);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: add Security, auth Cookie, expire Time Span Matches Idle Timeout.</summary>
|
||||
[Fact]
|
||||
public void AddSecurity_AuthCookie_ExpireTimeSpanMatchesIdleTimeout()
|
||||
{
|
||||
@@ -501,6 +541,7 @@ public class SecurityReviewRegressionTests
|
||||
Assert.Equal(TimeSpan.FromMinutes(30), cookieOptions.ExpireTimeSpan);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: add Security, auth Cookie, expire Time Span Is Configurable.</summary>
|
||||
[Fact]
|
||||
public void AddSecurity_AuthCookie_ExpireTimeSpanIsConfigurable()
|
||||
{
|
||||
@@ -523,6 +564,7 @@ public class SecurityReviewRegressionTests
|
||||
// (browser cookies are scoped by host+path, NOT port) must be able to use
|
||||
// distinct cookie names so signing into one does not clobber the other's session. ---
|
||||
|
||||
/// <summary>Verifies the case: add Security, auth Cookie, defaults To Canonical Name.</summary>
|
||||
[Fact]
|
||||
public void AddSecurity_AuthCookie_DefaultsToCanonicalName()
|
||||
{
|
||||
@@ -542,6 +584,7 @@ public class SecurityReviewRegressionTests
|
||||
Assert.Equal("ZB.MOM.WW.ScadaBridge.Auth", cookieOptions.Cookie.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: add Security, auth Cookie, cookie Name Is Configurable.</summary>
|
||||
[Fact]
|
||||
public void AddSecurity_AuthCookie_CookieNameIsConfigurable()
|
||||
{
|
||||
@@ -565,6 +608,8 @@ public class SecurityReviewRegressionTests
|
||||
Assert.True(cookieOptions.SlidingExpiration);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: add Security, auth Cookie, blank Cookie Name, falls Back To Default.</summary>
|
||||
/// <param name="blank">The blank test input.</param>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
@@ -585,8 +630,9 @@ public class SecurityReviewRegressionTests
|
||||
Assert.Equal(SecurityOptions.DefaultCookieName, cookieOptions.Cookie.Name);
|
||||
}
|
||||
|
||||
// --- Security-001: transport security (now owned by the shared LdapOptions) ---
|
||||
// --- transport security (now owned by the shared LdapOptions) ---
|
||||
|
||||
/// <summary>Verifies the case: ldap Options, transport, defaults To Ldaps.</summary>
|
||||
[Fact]
|
||||
public void LdapOptions_Transport_DefaultsToLdaps()
|
||||
{
|
||||
@@ -595,6 +641,8 @@ public class SecurityReviewRegressionTests
|
||||
Assert.Equal(LdapTransport.Ldaps, options.Transport);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: authenticate Async, start Tls Transport, not Blocked By Insecure Guard.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_StartTlsTransport_NotBlockedByInsecureGuard()
|
||||
{
|
||||
@@ -620,6 +668,7 @@ public class SecurityReviewRegressionTests
|
||||
Assert.Equal(LdapAuthFailure.ServiceAccountBindFailed, result.Failure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: ldap Options Validator, no Tls Transport, rejected Without Allow Insecure.</summary>
|
||||
[Fact]
|
||||
public void LdapOptionsValidator_NoTlsTransport_RejectedWithoutAllowInsecure()
|
||||
{
|
||||
@@ -649,12 +698,12 @@ public class SecurityReviewRegressionTests
|
||||
#region Code Review Regression Tests — Security-004/005/006/007
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for Security-006 (JWT issuer/audience checks disabled) and
|
||||
/// Security-007 (idle-timeout claim reset on every token refresh).
|
||||
/// Regression tests for disabled JWT issuer/audience checks and
|
||||
/// idle-timeout claim reset on every token refresh.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Task 1.2 cutover: the former Security-004 (uid/cn attribute parity) and
|
||||
/// Security-005 (no-service-account fallback-DN injection) cases tested ScadaBridge's
|
||||
/// Cutover: the former uid/cn attribute parity and
|
||||
/// no-service-account fallback-DN injection cases tested ScadaBridge's
|
||||
/// bespoke <c>LdapAuthService.BuildFallbackUserDn</c> / <c>EscapeLdapDn</c> static
|
||||
/// helpers. The shared <c>ZB.MOM.WW.Auth.Ldap</c> service is search-then-bind only (no
|
||||
/// no-service-account fallback DN) and its RFC-4514/4515 escaping parity is covered by
|
||||
@@ -674,8 +723,9 @@ public class SecurityReviewRegressionTests2
|
||||
private static JwtTokenService CreateJwtService(SecurityOptions? options = null) =>
|
||||
new(Options.Create(options ?? JwtOptions()), NullLogger<JwtTokenService>.Instance);
|
||||
|
||||
// --- Security-006: JWT issuer/audience must be bound and validated ---
|
||||
// --- JWT issuer/audience must be bound and validated ---
|
||||
|
||||
/// <summary>Verifies the case: generate Token, sets Issuer And Audience.</summary>
|
||||
[Fact]
|
||||
public void GenerateToken_SetsIssuerAndAudience()
|
||||
{
|
||||
@@ -687,6 +737,7 @@ public class SecurityReviewRegressionTests2
|
||||
Assert.Contains(JwtTokenService.TokenAudience, jwt.Audiences);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: validate Token, rejects Token With Wrong Issuer.</summary>
|
||||
[Fact]
|
||||
public void ValidateToken_RejectsTokenWithWrongIssuer()
|
||||
{
|
||||
@@ -708,6 +759,7 @@ public class SecurityReviewRegressionTests2
|
||||
Assert.Null(service.ValidateToken(foreignToken));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: validate Token, accepts Own Issuer And Audience.</summary>
|
||||
[Fact]
|
||||
public void ValidateToken_AcceptsOwnIssuerAndAudience()
|
||||
{
|
||||
@@ -716,8 +768,9 @@ public class SecurityReviewRegressionTests2
|
||||
Assert.NotNull(service.ValidateToken(token));
|
||||
}
|
||||
|
||||
// --- Security-007: refresh must preserve the original LastActivity timestamp ---
|
||||
// --- refresh must preserve the original LastActivity timestamp ---
|
||||
|
||||
/// <summary>Verifies the case: refresh Token, preserves Original Last Activity Claim.</summary>
|
||||
[Fact]
|
||||
public void RefreshToken_PreservesOriginalLastActivityClaim()
|
||||
{
|
||||
@@ -746,6 +799,7 @@ public class SecurityReviewRegressionTests2
|
||||
$"LastActivity was reset on refresh: expected ~{staleActivity:o}, got {carried:o}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: refresh Token, does Not Reset Idle Timeout When User Is Actually Idle.</summary>
|
||||
[Fact]
|
||||
public void RefreshToken_DoesNotResetIdleTimeoutWhenUserIsActuallyIdle()
|
||||
{
|
||||
@@ -771,6 +825,7 @@ public class SecurityReviewRegressionTests2
|
||||
"Refresh wrongly reset the idle clock to ~now");
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: record Activity, updates Last Activity To Now.</summary>
|
||||
[Fact]
|
||||
public void RecordActivity_UpdatesLastActivityToNow()
|
||||
{
|
||||
@@ -801,31 +856,34 @@ public class SecurityReviewRegressionTests2
|
||||
#region Code Review Regression Tests — Security-009/011
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for Security-009 (no LDAP connection timeout — a hung server can
|
||||
/// Regression tests for the missing LDAP connection timeout (a hung server can
|
||||
/// pin a thread-pool thread indefinitely because <c>ct</c> only guards work-item
|
||||
/// scheduling) and the remaining Security-011 coverage gaps.
|
||||
/// scheduling) and the remaining coverage gaps.
|
||||
/// </summary>
|
||||
public class SecurityReviewRegressionTests3
|
||||
{
|
||||
// --- Security-009: LDAP connection timeout must be configurable and bounded ---
|
||||
// --- LDAP connection timeout must be configurable and bounded ---
|
||||
|
||||
/// <summary>Verifies the case: ldap Options, connection Timeout, has Sane Default.</summary>
|
||||
[Fact]
|
||||
public void LdapOptions_ConnectionTimeout_HasSaneDefault()
|
||||
{
|
||||
var options = new LdapOptions();
|
||||
// A positive, finite default so a hung LDAP server cannot pin a thread forever
|
||||
// (the donor's Security-009 safeguard, preserved on the shared LdapOptions).
|
||||
// (the donor's connection-timeout safeguard, preserved on the shared LdapOptions).
|
||||
Assert.True(options.ConnectionTimeoutMs > 0);
|
||||
Assert.True(options.ConnectionTimeoutMs <= 60_000,
|
||||
"Default LDAP connection timeout should be bounded (<= 60s).");
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: authenticate Async, unreachable Host, fails Within Configured Timeout.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_UnreachableHost_FailsWithinConfiguredTimeout()
|
||||
{
|
||||
// A routable-but-non-responsive address would otherwise hang for the OS default
|
||||
// (often minutes). With ConnectionTimeoutMs applied to the LdapConnection the call
|
||||
// must give up promptly. 198.51.100.0/24 (TEST-NET-2, RFC 5737) is reserved and not
|
||||
// must give up promptly. 198.51.100.0/24 (the second documentation test network, RFC 5737) is reserved and not
|
||||
// routed, so the connect attempt stalls until the timeout fires.
|
||||
var options = new LdapOptions
|
||||
{
|
||||
@@ -851,7 +909,7 @@ public class SecurityReviewRegressionTests3
|
||||
$"Authentication did not honour the LDAP connection timeout: took {sw.Elapsed}.");
|
||||
}
|
||||
|
||||
// Note: the former Security-011 no-service-account fallback-DN / DN-escape static-helper
|
||||
// Note: the former no-service-account fallback-DN / DN-escape static-helper
|
||||
// cases (BuildFallbackUserDn / EscapeLdapDn) were removed: the shared service is
|
||||
// search-then-bind only (no fallback DN), and its escaping parity is covered by the
|
||||
// library's own LdapEscapingTests.
|
||||
@@ -862,12 +920,12 @@ public class SecurityReviewRegressionTests3
|
||||
#region Code Review Regression Tests — Security-012/013/014/015
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for Security-014 (<c>RefreshToken</c> re-issues a token without
|
||||
/// Regression tests for <c>RefreshToken</c> re-issuing a token without
|
||||
/// checking the idle timeout, so an idle-expired session can be renewed indefinitely).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Task 1.2 cutover: the former Security-013 (<c>ExtractFirstRdnValue</c> escaped-comma
|
||||
/// parsing) and Security-015 (username trim before use) cases tested ScadaBridge's
|
||||
/// Cutover: the former <c>ExtractFirstRdnValue</c> escaped-comma
|
||||
/// parsing) and username trim before use cases tested ScadaBridge's
|
||||
/// bespoke <c>LdapAuthService.ExtractFirstRdnValue</c> / <c>NormalizeUsername</c> /
|
||||
/// <c>BuildFallbackUserDn</c> statics. Those behaviours are now owned by the shared
|
||||
/// library and covered by its own tests — <c>LdapEscapingTests.FirstRdnValue_IsEscapeAware</c>
|
||||
@@ -887,8 +945,9 @@ public class SecurityReviewRegressionTests4
|
||||
private static JwtTokenService CreateJwtService(SecurityOptions? options = null) =>
|
||||
new(Options.Create(options ?? JwtOptions()), NullLogger<JwtTokenService>.Instance);
|
||||
|
||||
// --- Security-014: RefreshToken must reject an idle-expired principal ---
|
||||
// --- RefreshToken must reject an idle-expired principal ---
|
||||
|
||||
/// <summary>Verifies the case: refresh Token, idle Expired Principal, returns Null.</summary>
|
||||
[Fact]
|
||||
public void RefreshToken_IdleExpiredPrincipal_ReturnsNull()
|
||||
{
|
||||
@@ -909,6 +968,7 @@ public class SecurityReviewRegressionTests4
|
||||
Assert.Null(refreshed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: refresh Token, active Session Within Idle Window, still Refreshes.</summary>
|
||||
[Fact]
|
||||
public void RefreshToken_ActiveSessionWithinIdleWindow_StillRefreshes()
|
||||
{
|
||||
@@ -928,6 +988,7 @@ public class SecurityReviewRegressionTests4
|
||||
Assert.NotNull(refreshed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: refresh Token, missing Last Activity Claim, returns Null.</summary>
|
||||
[Fact]
|
||||
public void RefreshToken_MissingLastActivityClaim_ReturnsNull()
|
||||
{
|
||||
@@ -951,12 +1012,12 @@ public class SecurityReviewRegressionTests4
|
||||
#region Code Review Regression Tests — Security-012 (partial LDAP outage)
|
||||
|
||||
/// <summary>
|
||||
/// Security-012 (fail-closed group lookup) + the ScadaBridge-side
|
||||
/// Fail-closed group lookup + the ScadaBridge-side
|
||||
/// <see cref="LdapAuthFailureMessages"/> failure-code→message adapter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Task 1.2 cutover: the donor's fail-closed group-lookup rule (a post-bind group
|
||||
/// Cutover: the donor's fail-closed group-lookup rule (a post-bind group
|
||||
/// search failure FAILS the login rather than admitting a roleless session) is now
|
||||
/// owned by the shared <c>ZB.MOM.WW.Auth.Ldap</c> service and asserted by its own
|
||||
/// <c>LdapAuthServiceFailureTests.GroupLookupFailed_WhenUserHasNoGroups</c> /
|
||||
@@ -981,6 +1042,7 @@ public class SecurityReviewRegressionTests4
|
||||
/// </remarks>
|
||||
public class Security012GroupLookupFailureTests
|
||||
{
|
||||
/// <summary>Verifies the case: ldap Auth Result, fail, group Lookup Failed, is Failed Login With No Groups.</summary>
|
||||
[Fact]
|
||||
public void LdapAuthResult_Fail_GroupLookupFailed_IsFailedLoginWithNoGroups()
|
||||
{
|
||||
@@ -993,6 +1055,7 @@ public class Security012GroupLookupFailureTests
|
||||
Assert.Empty(result.Groups);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: ldap Auth Result, success, carries Groups.</summary>
|
||||
[Fact]
|
||||
public void LdapAuthResult_Success_CarriesGroups()
|
||||
{
|
||||
@@ -1003,6 +1066,7 @@ public class Security012GroupLookupFailureTests
|
||||
Assert.Null(result.Failure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: empty Groups, is Treated As Group Lookup Failed, library Is Stricter.</summary>
|
||||
[Fact]
|
||||
public void EmptyGroups_IsTreatedAsGroupLookupFailed_LibraryIsStricter()
|
||||
{
|
||||
@@ -1013,12 +1077,15 @@ public class Security012GroupLookupFailureTests
|
||||
var failClosed = LdapAuthResult.Fail(LdapAuthFailure.GroupLookupFailed);
|
||||
Assert.False(failClosed.Succeeded);
|
||||
|
||||
// And the donor's "Security-012 directory-unavailable" message is preserved for it.
|
||||
// And the donor's "directory-unavailable" message is preserved for it.
|
||||
Assert.Equal(
|
||||
LdapAuthFailureMessages.DirectoryUnavailable,
|
||||
LdapAuthFailureMessages.ToMessage(LdapAuthFailure.GroupLookupFailed));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: ldap Auth Failure Messages, maps Each Failure, to Donor Equivalent Message.</summary>
|
||||
/// <param name="failure">The failure test input.</param>
|
||||
/// <param name="expected">The expected test input.</param>
|
||||
[Theory]
|
||||
[InlineData(LdapAuthFailure.BadCredentials, LdapAuthFailureMessages.InvalidCredentials)]
|
||||
[InlineData(LdapAuthFailure.UserNotFound, LdapAuthFailureMessages.InvalidCredentials)]
|
||||
@@ -1029,12 +1096,13 @@ public class Security012GroupLookupFailureTests
|
||||
public void LdapAuthFailureMessages_MapsEachFailure_ToDonorEquivalentMessage(
|
||||
LdapAuthFailure failure, string expected)
|
||||
{
|
||||
// Security-019 framing preserved: user-credential failures are enumeration-safe
|
||||
// Framing preserved: user-credential failures are enumeration-safe
|
||||
// ("Invalid username or password."), system-side faults point the OPERATOR at the
|
||||
// cause ("...misconfigured...") rather than blaming user input.
|
||||
Assert.Equal(expected, LdapAuthFailureMessages.ToMessage(failure));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: ldap Auth Failure Messages, null Failure, falls Back To Generic.</summary>
|
||||
[Fact]
|
||||
public void LdapAuthFailureMessages_NullFailure_FallsBackToGeneric()
|
||||
{
|
||||
@@ -1048,6 +1116,8 @@ public class Security012GroupLookupFailureTests
|
||||
|
||||
public class AuthorizationPolicyTests
|
||||
{
|
||||
/// <summary>Verifies the case: admin Policy, administrator Role, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AdminPolicy_AdministratorRole_Succeeds()
|
||||
{
|
||||
@@ -1056,6 +1126,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: admin Policy, designer Role, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AdminPolicy_DesignerRole_Fails()
|
||||
{
|
||||
@@ -1064,6 +1136,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: design Policy, designer Role, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DesignPolicy_DesignerRole_Succeeds()
|
||||
{
|
||||
@@ -1072,6 +1146,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: deployment Policy, deployer Role, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeploymentPolicy_DeployerRole_Succeeds()
|
||||
{
|
||||
@@ -1080,6 +1156,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: no Roles, denied All.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NoRoles_DeniedAll()
|
||||
{
|
||||
@@ -1092,8 +1170,8 @@ public class AuthorizationPolicyTests
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Audit Log #23 — OperationalAudit + AuditExport policies (M7-T15),
|
||||
// post Task 1.7 canonicalization + SoD collapse. Default mapping
|
||||
// OperationalAudit + AuditExport policies,
|
||||
// post-canonicalization + SoD collapse. Default mapping
|
||||
// (see AuthorizationPolicies XML doc):
|
||||
// Administrator → OperationalAudit + AuditExport
|
||||
// Viewer → OperationalAudit only (former AuditReadOnly home)
|
||||
@@ -1104,6 +1182,9 @@ public class AuthorizationPolicyTests
|
||||
// AuditReadOnly → collapsed into Viewer
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies the case: operational Audit Policy, granted Roles, succeed.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Administrator")]
|
||||
[InlineData("Viewer")]
|
||||
@@ -1113,6 +1194,9 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: operational Audit Policy, ungranted Roles, fail.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Designer")]
|
||||
[InlineData("Deployer")]
|
||||
@@ -1122,6 +1206,9 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: audit Export Policy, granted Roles, succeed.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Administrator")]
|
||||
public async Task AuditExportPolicy_GrantedRoles_Succeed(string role)
|
||||
@@ -1130,6 +1217,9 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: audit Export Policy, ungranted Roles, fail.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Viewer")]
|
||||
[InlineData("Designer")]
|
||||
@@ -1140,6 +1230,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: viewer, reads Audit, but Cannot Export, preserved Half So D.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Viewer_ReadsAudit_ButCannotExport_PreservedHalfSoD()
|
||||
{
|
||||
@@ -1151,6 +1243,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: former Audit User, now Administrator, gains Export And Full Admin, documented Escalation.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FormerAuditUser_NowAdministrator_GainsExportAndFullAdmin_DocumentedEscalation()
|
||||
{
|
||||
@@ -1164,7 +1258,7 @@ public class AuthorizationPolicyTests
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Security-024 — M7 two-person Secured Write separation-of-duties (SoD)
|
||||
// Two-person Secured Write separation-of-duties (SoD)
|
||||
// policies. The whole safety argument of Secured Writes is that the role
|
||||
// that INITIATES (Operator → RequireOperator) is distinct from the role
|
||||
// that APPROVES (Verifier → RequireVerifier). These functional
|
||||
@@ -1174,6 +1268,8 @@ public class AuthorizationPolicyTests
|
||||
// SoD (the prior coverage only asserted the constant string values).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies the case: operator Policy, operator Role, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OperatorPolicy_OperatorRole_Succeeds()
|
||||
{
|
||||
@@ -1181,6 +1277,9 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.RequireOperator, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: operator Policy, non Operator Roles, fail.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Verifier")]
|
||||
[InlineData("Administrator")]
|
||||
@@ -1193,6 +1292,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireOperator, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: operator Policy, no Roles, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OperatorPolicy_NoRoles_Fails()
|
||||
{
|
||||
@@ -1200,6 +1301,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireOperator, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: verifier Policy, verifier Role, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task VerifierPolicy_VerifierRole_Succeeds()
|
||||
{
|
||||
@@ -1207,6 +1310,9 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.RequireVerifier, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: verifier Policy, non Verifier Roles, fail.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Operator")]
|
||||
[InlineData("Administrator")]
|
||||
@@ -1219,6 +1325,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireVerifier, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: verifier Policy, no Roles, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task VerifierPolicy_NoRoles_Fails()
|
||||
{
|
||||
@@ -1226,6 +1334,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireVerifier, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: secured Write So D, operator Principal Cannot Satisfy Verifier, and Vice Versa.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SecuredWriteSoD_OperatorPrincipalCannotSatisfyVerifier_AndViceVersa()
|
||||
{
|
||||
@@ -1242,6 +1352,9 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireOperator, verifierOnly));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: require Secured Write Access, secured Write Roles, satisfy.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Operator")]
|
||||
[InlineData("Verifier")]
|
||||
@@ -1254,6 +1367,9 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.RequireSecuredWriteAccess, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: require Secured Write Access, other Roles, denied.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Viewer")]
|
||||
[InlineData("Designer")]
|
||||
@@ -1272,6 +1388,9 @@ public class AuthorizationPolicyTests
|
||||
// by the ManagementActor role matrix; Designer gets neither.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies the case: require Flow Debugger, replay Capable Roles, satisfy.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Deployer")]
|
||||
[InlineData("Administrator")]
|
||||
@@ -1282,6 +1401,9 @@ public class AuthorizationPolicyTests
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.RequireFlowDebugger, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: require Flow Debugger, other Roles, denied.</summary>
|
||||
/// <param name="role">The role test input.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Designer")]
|
||||
[InlineData("Operator")]
|
||||
@@ -1292,6 +1414,8 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireFlowDebugger, principal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: require Flow Debugger, does Not Imply Deployment.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequireFlowDebugger_DoesNotImplyDeployment()
|
||||
{
|
||||
@@ -1333,12 +1457,12 @@ public class AuthorizationPolicyTests
|
||||
#region Code Review Regression Tests — Security-020 (LDAP fail-fast validation)
|
||||
|
||||
/// <summary>
|
||||
/// Security-020: a missing/empty required LDAP field must fail fast at startup with a
|
||||
/// A missing/empty required LDAP field must fail fast at startup with a
|
||||
/// clear, field-naming message, so a typo'd appsettings section fails at boot instead
|
||||
/// of surfacing minutes/hours later as a generic LDAP error on the first real login.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Task 1.2/1.4 cutover: the donor's <c>SecurityOptionsValidator</c> (which checked
|
||||
/// Cutover: the donor's <c>SecurityOptionsValidator</c> (which checked
|
||||
/// <c>Security:LdapServer</c> + <c>Security:LdapSearchBase</c> non-empty) was replaced by
|
||||
/// the shared <c>ZB.MOM.WW.Auth.Ldap.LdapOptionsValidator</c>, registered with
|
||||
/// <c>ValidateOnStart()</c> by <c>AddZbLdapAuth</c> inside <c>AddSecurity</c>. The shared
|
||||
@@ -1357,6 +1481,7 @@ public class SecurityOptionsValidatorTests
|
||||
Transport = LdapTransport.Ldaps,
|
||||
};
|
||||
|
||||
/// <summary>Verifies the case: validate, all Required Fields Set, succeeds.</summary>
|
||||
[Fact]
|
||||
public void Validate_AllRequiredFieldsSet_Succeeds()
|
||||
{
|
||||
@@ -1364,6 +1489,8 @@ public class SecurityOptionsValidatorTests
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: validate, empty Or Whitespace Server, fails.</summary>
|
||||
/// <param name="server">The server test input.</param>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
@@ -1377,6 +1504,8 @@ public class SecurityOptionsValidatorTests
|
||||
Assert.Contains(nameof(LdapOptions.Server), result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: validate, empty Or Whitespace Search Base, fails.</summary>
|
||||
/// <param name="searchBase">The searchBase test input.</param>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
@@ -1390,6 +1519,7 @@ public class SecurityOptionsValidatorTests
|
||||
Assert.Contains(nameof(LdapOptions.SearchBase), result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: validate, empty Service Account Dn, fails.</summary>
|
||||
[Fact]
|
||||
public void Validate_EmptyServiceAccountDn_Fails()
|
||||
{
|
||||
@@ -1439,7 +1569,7 @@ public class SecurityOptionsValidatorTests
|
||||
#region Task 1.5: Canonical claim vocabulary (ZbClaimTypes) — role/scope migration
|
||||
|
||||
/// <summary>
|
||||
/// Task 1.5 (full canonical adoption): the Security module's claim-type constants now
|
||||
/// Full canonical adoption: the Security module's claim-type constants now
|
||||
/// alias the shared <see cref="ZbClaimTypes"/> vocabulary. These tests pin the contract
|
||||
/// that the MINTED claim type is exactly the type the policies + token validation CONSUME,
|
||||
/// across both the JWT bearer and the cookie principal, so a future drift fails loudly.
|
||||
@@ -1457,6 +1587,7 @@ public class CanonicalClaimVocabularyTests
|
||||
private static JwtTokenService CreateService() =>
|
||||
new(Options.Create(CreateOptions()), NullLogger<JwtTokenService>.Instance);
|
||||
|
||||
/// <summary>Verifies the case: claim Type Constants, are Canonical Zb Claim Types.</summary>
|
||||
[Fact]
|
||||
public void ClaimTypeConstants_AreCanonicalZbClaimTypes()
|
||||
{
|
||||
@@ -1477,6 +1608,7 @@ public class CanonicalClaimVocabularyTests
|
||||
Assert.Equal("LastActivity", JwtTokenService.LastActivityClaimType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: minted Jwt, round Trips, canonical Claim Types Verbatim.</summary>
|
||||
[Fact]
|
||||
public void MintedJwt_RoundTrips_CanonicalClaimTypesVerbatim()
|
||||
{
|
||||
@@ -1497,11 +1629,13 @@ public class CanonicalClaimVocabularyTests
|
||||
Assert.Equal("7", principal.FindFirst(ZbClaimTypes.ScopeId)?.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: minted Jwt, role Claim, satisfies Operational Audit Policy.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MintedJwt_RoleClaim_SatisfiesOperationalAuditPolicy()
|
||||
{
|
||||
// The load-bearing round-trip: a JWT minted with RoleClaimType=Administrator
|
||||
// (post Task 1.7, the home of the former full-audit Audit role) must satisfy
|
||||
// (the home of the former full-audit Audit role) must satisfy
|
||||
// a RequireClaim(RoleClaimType, OperationalAuditRoles) policy after validation.
|
||||
var service = CreateService();
|
||||
var token = service.GenerateToken("Jane Roe", "janer", new[] { Roles.Administrator }, null);
|
||||
@@ -1512,7 +1646,7 @@ public class CanonicalClaimVocabularyTests
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, principal!));
|
||||
// Administrator grants AuditExport too (it absorbed the former Audit role):
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal!));
|
||||
// Viewer (post Task 1.7 home of the former AuditReadOnly role) is read-only —
|
||||
// Viewer (home of the former AuditReadOnly role) is read-only —
|
||||
// separate assertion that the read-not-export half-SoD survives the type
|
||||
// migration AND the role collapse.
|
||||
var roPrincipal = service.ValidateToken(
|
||||
@@ -1521,6 +1655,8 @@ public class CanonicalClaimVocabularyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.AuditExport, roPrincipal!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: cookie Principal, built Like Login, authorizes And Exposes Canonical Types.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CookiePrincipal_BuiltLikeLogin_AuthorizesAndExposesCanonicalTypes()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user