docs(src): add missing XML docs and strip tracking-ID comments
Sweep of 203 source files resolving CommentChecker findings: add <summary>/<param>/<returns>/<inheritdoc> where missing, and remove resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN, Task N) from code comments. Comment/doc-only — no logic changes. Server+Tests build clean under TreatWarningsAsErrors.
This commit is contained in:
+15
-2
@@ -29,6 +29,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
private readonly List<TempDatabaseDirectory> _tempDirectories = [];
|
||||
|
||||
/// <summary>Verifies that unauthorized users cannot create API keys.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateAsync_UnauthorizedUser_DoesNotCreate()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that authorized users create a verifiable, constrained key and audit it.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateAsync_AuthorizedUser_CreatesVerifiableKeyAndAudits()
|
||||
{
|
||||
@@ -79,6 +81,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a key whose id already exists is rejected.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateAsync_DuplicateKeyId_ReportsConflict()
|
||||
{
|
||||
@@ -96,6 +99,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that authorized users can revoke keys with audit trail.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RevokeAsync_AuthorizedUser_RevokesAndAudits()
|
||||
{
|
||||
@@ -121,6 +125,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that authorized users can rotate a key's secret with audit trail.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RotateAsync_AuthorizedUser_RotatesAndAudits()
|
||||
{
|
||||
@@ -153,6 +158,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that authorized users can delete revoked keys with audit trail.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteAsync_AuthorizedUser_DeletesRevokedKeyAndAudits()
|
||||
{
|
||||
@@ -181,6 +187,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
/// <c>dashboard-delete-key</c> audit entry with <c>Details = "not-found-or-active"</c> is still
|
||||
/// written — audit completeness for refused deletes.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ActiveKey_ReportsFriendlyErrorAndAudits()
|
||||
{
|
||||
@@ -206,6 +213,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
|
||||
/// <summary>A blank key id fails validation before any store or audit call runs.</summary>
|
||||
/// <param name="blankKeyId">A blank or whitespace key identifier.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
@@ -225,10 +233,11 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-004 regression: the dashboard create path must reject a request carrying a
|
||||
/// The dashboard create path must reject a request carrying a
|
||||
/// non-canonical scope string rather than persisting a key whose scope the authorization
|
||||
/// resolver never matches.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateAsync_UnknownScope_DoesNotCreate()
|
||||
{
|
||||
@@ -255,6 +264,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
/// Phase 3 canonical audit shape: the dashboard-create-key canonical AuditEvent records
|
||||
/// the operator username as Actor and the managed keyId as Target.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateAsync_AuthorizedUser_CanonicalAuditEventHasOperatorAsActorAndKeyIdAsTarget()
|
||||
{
|
||||
@@ -386,7 +396,10 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
|
||||
/// <summary>Gets the recorded canonical audit events.</summary>
|
||||
public List<ZB.MOM.WW.Audit.AuditEvent> Events { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Records the audit event in memory instead of writing it to a real sink.</summary>
|
||||
/// <param name="auditEvent">The audit event to record.</param>
|
||||
/// <param name="cancellationToken">A token to observe for cancellation requests.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task WriteAsync(ZB.MOM.WW.Audit.AuditEvent auditEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Events.Add(auditEvent);
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
public sealed class DashboardAuthenticatorTests
|
||||
{
|
||||
/// <summary>A blank username is rejected without touching the LDAP provider.</summary>
|
||||
/// <param name="username">The blank/whitespace/null username under test.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -40,6 +42,7 @@ public sealed class DashboardAuthenticatorTests
|
||||
}
|
||||
|
||||
/// <summary>A blank password is rejected without touching the LDAP provider.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_BlankPassword_FailsWithoutCallingLdap()
|
||||
{
|
||||
@@ -60,6 +63,8 @@ public sealed class DashboardAuthenticatorTests
|
||||
/// A failed LDAP outcome (any failure bucket, including <see cref="LdapAuthFailure.Disabled"/>)
|
||||
/// maps to the generic dashboard failure without leaking the raw password.
|
||||
/// </summary>
|
||||
/// <param name="failure">The LDAP failure bucket under test.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(LdapAuthFailure.Disabled)]
|
||||
[InlineData(LdapAuthFailure.BadCredentials)]
|
||||
@@ -87,6 +92,7 @@ public sealed class DashboardAuthenticatorTests
|
||||
/// name (ClaimTypes.Name), and the username (ClaimTypes.NameIdentifier), under the
|
||||
/// dashboard authentication scheme.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_Success_BuildsPrincipalWithExpectedClaims()
|
||||
{
|
||||
@@ -127,11 +133,12 @@ public sealed class DashboardAuthenticatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task 1.5: the principal emits canonical ZbClaimTypes.Username ("zb:username") with
|
||||
/// The principal emits canonical ZbClaimTypes.Username ("zb:username") with
|
||||
/// the login username, ZbClaimTypes.Role (= ClaimTypes.Role) for each resolved role, and
|
||||
/// ZbClaimTypes.DisplayName ("zb:displayname") with the display name — while keeping
|
||||
/// ClaimTypes.NameIdentifier, ClaimTypes.Name, and mxgateway:ldap_group claims intact.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_Success_EmitsCanonicalZbClaims()
|
||||
{
|
||||
@@ -183,6 +190,7 @@ public sealed class DashboardAuthenticatorTests
|
||||
/// When the user authenticates but none of their groups map to a dashboard role,
|
||||
/// the login is denied (the long-standing "no roles matched → denied" rule).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_NoRolesMatched_DeniesLogin()
|
||||
{
|
||||
@@ -216,6 +224,7 @@ public sealed class DashboardAuthenticatorTests
|
||||
/// for the realistic (already-short) group shape.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_GroupAsDistinguishedNameFromService_ResolvesRoleAndSurfacesServiceValue()
|
||||
{
|
||||
@@ -243,6 +252,7 @@ public sealed class DashboardAuthenticatorTests
|
||||
}
|
||||
|
||||
/// <summary>The (already-trimmed) username from the LDAP result flows onto the principal.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_UsesUsernameFromLdapResult()
|
||||
{
|
||||
@@ -296,10 +306,17 @@ public sealed class DashboardAuthenticatorTests
|
||||
/// </summary>
|
||||
private sealed class FakeLdapAuthService(LdapAuthResult result) : ILdapAuthService
|
||||
{
|
||||
/// <summary>Gets a value indicating whether <see cref="AuthenticateAsync"/> was invoked.</summary>
|
||||
public bool WasCalled { get; private set; }
|
||||
|
||||
/// <summary>Gets the username passed to the most recent <see cref="AuthenticateAsync"/> call.</summary>
|
||||
public string? LastUsername { get; private set; }
|
||||
|
||||
/// <summary>Records the call and returns the configured fixed result.</summary>
|
||||
/// <param name="username">The username to authenticate.</param>
|
||||
/// <param name="password">The password to authenticate.</param>
|
||||
/// <param name="ct">Token to observe for cancellation.</param>
|
||||
/// <returns>The fixed <see cref="LdapAuthResult"/> configured for this fake.</returns>
|
||||
public Task<LdapAuthResult> AuthenticateAsync(string username, string password, CancellationToken ct)
|
||||
{
|
||||
WasCalled = true;
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
public sealed class DashboardAuthorizationHandlerTests
|
||||
{
|
||||
/// <summary>Verifies that unauthenticated remote requests fail authorization.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleAsync_UnauthenticatedRemoteRequest_DoesNotSucceed()
|
||||
{
|
||||
@@ -24,6 +25,7 @@ public sealed class DashboardAuthorizationHandlerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that anonymous localhost access succeeds when allowed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleAsync_AnonymousLocalhostAllowed_Succeeds()
|
||||
{
|
||||
@@ -40,6 +42,7 @@ public sealed class DashboardAuthorizationHandlerTests
|
||||
/// Verifies that the anonymous-localhost bypass is denied when <c>AllowAnonymousLocalhost</c>
|
||||
/// is off, even on a loopback connection — the misconfiguration must not expose the dashboard.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleAsync_AnonymousLocalhostDisallowed_DoesNotSucceed()
|
||||
{
|
||||
@@ -56,6 +59,7 @@ public sealed class DashboardAuthorizationHandlerTests
|
||||
/// Verifies that the anonymous-localhost bypass stays scoped to loopback: an anonymous
|
||||
/// request from a non-loopback address is denied even when <c>AllowAnonymousLocalhost</c> is on.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleAsync_AnonymousLocalhostAllowedFromRemoteAddress_DoesNotSucceed()
|
||||
{
|
||||
@@ -69,6 +73,7 @@ public sealed class DashboardAuthorizationHandlerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an authenticated user without any dashboard role fails the viewer requirement.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleAsync_AuthenticatedWithoutDashboardRole_DoesNotSucceed()
|
||||
{
|
||||
@@ -82,6 +87,7 @@ public sealed class DashboardAuthorizationHandlerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Viewer satisfies the viewer-or-admin requirement.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleAsync_ViewerRole_SatisfiesViewerPolicy()
|
||||
{
|
||||
@@ -95,6 +101,7 @@ public sealed class DashboardAuthorizationHandlerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an Admin satisfies the admin-only requirement.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleAsync_AdminRole_SatisfiesAdminPolicy()
|
||||
{
|
||||
@@ -108,6 +115,7 @@ public sealed class DashboardAuthorizationHandlerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Viewer does NOT satisfy the admin-only requirement.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task HandleAsync_ViewerRole_DoesNotSatisfyAdminPolicy()
|
||||
{
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
public sealed class DashboardAutoLoginAuthenticationHandlerTests
|
||||
{
|
||||
/// <summary>The default auto-login user mints an authenticated principal with both dashboard roles.</summary>
|
||||
[Fact]
|
||||
public void CreatePrincipal_MintsAuthenticatedMultiRoleUser()
|
||||
{
|
||||
@@ -16,6 +17,8 @@ public sealed class DashboardAutoLoginAuthenticationHandlerTests
|
||||
Assert.True(principal.IsInRole(DashboardRoles.Viewer));
|
||||
}
|
||||
|
||||
/// <summary>A null, empty, or whitespace-only user falls back to the configured default user.</summary>
|
||||
/// <param name="user">The blank/whitespace/null user under test.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -27,6 +30,7 @@ public sealed class DashboardAutoLoginAuthenticationHandlerTests
|
||||
Assert.Equal(DashboardAutoLoginAuthenticationHandler.DefaultUser, principal.Identity!.Name);
|
||||
}
|
||||
|
||||
/// <summary>Leading/trailing whitespace around the user name is trimmed before minting the principal.</summary>
|
||||
[Fact]
|
||||
public void CreatePrincipal_TrimsUser()
|
||||
{
|
||||
@@ -35,6 +39,7 @@ public sealed class DashboardAutoLoginAuthenticationHandlerTests
|
||||
Assert.Equal("multi-role", principal.Identity!.Name);
|
||||
}
|
||||
|
||||
/// <summary>A custom user name preserves its identity name and still receives both dashboard roles.</summary>
|
||||
[Fact]
|
||||
public void CreatePrincipal_CustomUser_PreservesNameAndRoles()
|
||||
{
|
||||
|
||||
+4
-6
@@ -171,13 +171,11 @@ public sealed class DashboardBrowseAndAlarmModelTests
|
||||
Assert.True(model.IsDegraded);
|
||||
Assert.Contains("bg-warning", model.BadgeCssClass, StringComparison.Ordinal);
|
||||
Assert.Equal("x", model.Reason);
|
||||
// Tests-033: pin the amber label text, not just the CSS class — a label swap
|
||||
// would otherwise pass this test.
|
||||
Assert.Equal(DashboardAlarmProviderStatus.DegradedLabel, model.Label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests-033: an explicitly-degraded status whose mode is still Alarmmgr (the
|
||||
/// An explicitly-degraded status whose mode is still Alarmmgr (the
|
||||
/// <c>Degraded || Mode==Subtag</c> guard's second, independent branch) must still
|
||||
/// map to the degraded amber badge.
|
||||
/// </summary>
|
||||
@@ -199,7 +197,7 @@ public sealed class DashboardBrowseAndAlarmModelTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests-033: the <c>SinceUtc</c> field must carry the protobuf <c>Since</c>
|
||||
/// The <c>SinceUtc</c> field must carry the protobuf <c>Since</c>
|
||||
/// timestamp converted to a <see cref="DateTimeOffset" />.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -220,7 +218,7 @@ public sealed class DashboardBrowseAndAlarmModelTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests-033: <see cref="DashboardAlarmProviderStatus.FromFeed" /> — the entry the
|
||||
/// <see cref="DashboardAlarmProviderStatus.FromFeed" /> — the entry the
|
||||
/// dashboard SignalR snapshot path actually calls — projects a provider-status
|
||||
/// feed message into the badge model.
|
||||
/// </summary>
|
||||
@@ -246,7 +244,7 @@ public sealed class DashboardBrowseAndAlarmModelTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests-033: <see cref="DashboardAlarmProviderStatus.FromFeed" /> throws
|
||||
/// <see cref="DashboardAlarmProviderStatus.FromFeed" /> throws
|
||||
/// <see cref="ArgumentException" /> when the feed message does not carry a
|
||||
/// provider-status payload.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
public sealed class DashboardCookieOptionsTests
|
||||
{
|
||||
/// <summary>Verifies that the application configures secure dashboard authentication cookies.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Build_ConfiguresSecureDashboardCookie()
|
||||
{
|
||||
@@ -36,6 +37,7 @@ public sealed class DashboardCookieOptionsTests
|
||||
/// relaxes the cookie to <see cref="CookieSecurePolicy.SameAsRequest"/> so
|
||||
/// the dashboard can be reached over plain HTTP in dev.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Build_WithRequireHttpsCookieFalse_UsesSameAsRequest()
|
||||
{
|
||||
@@ -55,6 +57,7 @@ public sealed class DashboardCookieOptionsTests
|
||||
/// cookie name, so a gateway instance sharing a hostname with another can be given a
|
||||
/// distinct name (browser cookies are scoped by host+path, not port).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Build_WithCookieNameOverride_UsesConfiguredName()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
public sealed class DashboardDisableLoginTests
|
||||
{
|
||||
/// <summary>Verifies that when dashboard login is not disabled, the dashboard authentication scheme resolves to the cookie authentication handler.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisableLoginOff_CookieSchemeUsesCookieHandler()
|
||||
{
|
||||
@@ -25,6 +27,8 @@ public sealed class DashboardDisableLoginTests
|
||||
Assert.Equal(typeof(CookieAuthenticationHandler), scheme!.HandlerType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that when <c>MxGateway:Dashboard:DisableLogin</c> is enabled, the dashboard authentication scheme resolves to the auto-login handler instead of the cookie handler.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisableLoginOn_CookieSchemeUsesAutoLoginHandler()
|
||||
{
|
||||
@@ -40,6 +44,8 @@ public sealed class DashboardDisableLoginTests
|
||||
Assert.Equal(typeof(DashboardAutoLoginAuthenticationHandler), scheme!.HandlerType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the auto-login principal created when login is disabled satisfies the Admin, Viewer, and hub-clients authorization policies.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DisableLoginOn_AutoLoginPrincipalSatisfiesAdminAndViewerPolicies()
|
||||
{
|
||||
|
||||
+2
@@ -130,6 +130,8 @@ public sealed class DashboardGalaxySummaryProjectorTests
|
||||
/// Verifies that the cache status enum is faithfully mapped to the dashboard
|
||||
/// status enum across every defined value.
|
||||
/// </summary>
|
||||
/// <param name="cacheStatus">The Galaxy cache status to project.</param>
|
||||
/// <param name="expected">The expected dashboard status for the given cache status.</param>
|
||||
[Theory]
|
||||
[InlineData(GalaxyCacheStatus.Healthy, DashboardGalaxyStatus.Healthy)]
|
||||
[InlineData(GalaxyCacheStatus.Stale, DashboardGalaxyStatus.Stale)]
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed class DashboardGroupRoleMapperTests
|
||||
/// <summary>Verifies full-DN match, leading-RDN fallback, case-insensitivity, and unmapped → empty.</summary>
|
||||
/// <param name="ldapGroup">The LDAP group name or distinguished name.</param>
|
||||
/// <param name="expectedRole">The expected role or null if no match.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("GwAdmin", DashboardRoles.Admin)]
|
||||
[InlineData("gwadmin", DashboardRoles.Admin)]
|
||||
@@ -61,6 +62,7 @@ public sealed class DashboardGroupRoleMapperTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that admin and viewer roles are both emitted when both groups are present.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapAsync_AdminPlusViewer_BothRolesEmitted()
|
||||
{
|
||||
@@ -75,6 +77,7 @@ public sealed class DashboardGroupRoleMapperTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty GroupToRole map yields no roles.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapAsync_EmptyMapping_ReturnsNoRoles()
|
||||
{
|
||||
@@ -86,12 +89,13 @@ public sealed class DashboardGroupRoleMapperTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task 1.7 (canonical roles): an LDAP user in the admin group must resolve
|
||||
/// Canonical roles: an LDAP user in the admin group must resolve
|
||||
/// to the canonical role value <c>"Administrator"</c> (not the legacy
|
||||
/// <c>"Admin"</c>), and the reader group to <c>"Viewer"</c>. Asserted with
|
||||
/// string LITERALS — independent of <see cref="DashboardRoles"/> — so a
|
||||
/// regression on the constant's value is caught here.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MapAsync_AdminGroup_ResolvesToCanonicalAdministratorValue()
|
||||
{
|
||||
@@ -105,7 +109,7 @@ public sealed class DashboardGroupRoleMapperTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task 1.7: the canonical admin value (<c>"Administrator"</c>) passes the
|
||||
/// The canonical admin value (<c>"Administrator"</c>) passes the
|
||||
/// admin-only gate while a <c>"Viewer"</c> fails it — asserted with literals,
|
||||
/// proving enforcement is bound to the new value and the legacy <c>"Admin"</c>
|
||||
/// string is no longer what authorizes admin actions.
|
||||
@@ -124,6 +128,7 @@ public sealed class DashboardGroupRoleMapperTests
|
||||
/// Verifies the extracted shared helper is the single source of truth: it
|
||||
/// produces the same roles the mapper does for the same inputs.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SharedHelper_MatchesMapperOutput()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
public sealed class DashboardHubsRegistrationTests
|
||||
{
|
||||
/// <summary>Verifies that dashboard build maps all three hubs and token endpoint.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Build_WhenDashboardEnabled_MapsAllThreeHubsAndTokenEndpoint()
|
||||
{
|
||||
@@ -27,6 +28,7 @@ public sealed class DashboardHubsRegistrationTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that dashboard build registers hub token service and connection factory.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Build_WhenDashboardEnabled_RegistersHubTokenServiceAndConnectionFactory()
|
||||
{
|
||||
|
||||
+12
-7
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
public sealed class DashboardSessionAdminServiceTests
|
||||
{
|
||||
/// <summary>Verifies that a viewer cannot close a session.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CloseSessionAsync_ViewerCannotManage()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an admin can close a session.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CloseSessionAsync_AdminClosesSession()
|
||||
{
|
||||
@@ -43,6 +45,7 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that closing a missing session returns a friendly error message.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CloseSessionAsync_WhenSessionMissing_ReportsFriendlyError()
|
||||
{
|
||||
@@ -62,6 +65,7 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a viewer cannot kill a worker.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KillWorkerAsync_ViewerCannotManage()
|
||||
{
|
||||
@@ -78,6 +82,7 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an admin can kill a worker.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KillWorkerAsync_AdminKillsWorker()
|
||||
{
|
||||
@@ -93,14 +98,11 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
Assert.Equal(1, sessionManager.KillCount);
|
||||
Assert.Equal("session-1", sessionManager.LastKilledSessionId);
|
||||
|
||||
// Tests-028: pin the literal reason string so a future caller-side change is a deliberate
|
||||
// test update rather than a silent drift. DashboardSessionAdminService passes a hard-coded
|
||||
// "dashboard-admin-kill" so the worker-exit metric (mxgateway.workers.killed) carries a
|
||||
// stable, machine-greppable reason tag.
|
||||
Assert.Equal("dashboard-admin-kill", sessionManager.LastKillReason);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that killing a worker with a blank session ID returns failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KillWorkerAsync_BlankSessionId_ReturnsFailure()
|
||||
{
|
||||
@@ -117,10 +119,11 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests-029: <c>CloseSessionAsync</c> has the same blank-session-id guard as
|
||||
/// <c>CloseSessionAsync</c> has the same blank-session-id guard as
|
||||
/// <c>KillWorkerAsync</c> but previously had no parallel test. Coverage was asymmetric.
|
||||
/// A guard-removal regression on the close path would slip through.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CloseSessionAsync_BlankSessionId_ReturnsFailure()
|
||||
{
|
||||
@@ -148,12 +151,13 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression for Server-050: an unexpected (non-<see cref="SessionManagerException"/>)
|
||||
/// Regression: an unexpected (non-<see cref="SessionManagerException"/>)
|
||||
/// exception from <c>CloseSessionAsync</c> — e.g. an <see cref="InvalidOperationException"/>
|
||||
/// or <see cref="IOException"/> surfaced from <c>RemoveSessionAsync</c>/<c>DisposeAsync</c> —
|
||||
/// must be converted to a friendly <see cref="DashboardSessionAdminResult.Fail(string)"/>
|
||||
/// rather than propagating raw into Blazor's error boundary.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CloseSessionAsync_WhenManagerThrowsUnexpected_ReturnsFriendlyFail()
|
||||
{
|
||||
@@ -173,8 +177,9 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression for Server-050: same friendly-fail contract for the Kill path.
|
||||
/// Regression: same friendly-fail contract for the Kill path.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KillWorkerAsync_WhenManagerThrowsUnexpected_ReturnsFriendlyFail()
|
||||
{
|
||||
|
||||
+19
-21
@@ -11,20 +11,14 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Server-042 regression: a transient failure inside
|
||||
/// A transient failure inside
|
||||
/// <see cref="IDashboardSnapshotService.WatchSnapshotsAsync"/> must not
|
||||
/// end the BackgroundService; the publisher must wait the configured
|
||||
/// reconnect delay and then re-open the subscription. Before the fix,
|
||||
/// the publisher exited on the first non-cancellation exception and
|
||||
/// the dashboard's snapshot stream went silent until process restart.
|
||||
///
|
||||
/// <para>Tests-031: the reconnect-gap measurement is bounded between the
|
||||
/// moment the first subscribe actually <c>throw</c>s and the moment the
|
||||
/// second subscribe begins. Measuring from <c>startedAt</c> (pre-<c>StartAsync</c>)
|
||||
/// baselined scheduling overhead into the budget and made the lower bound
|
||||
/// flaky on slow CI; recording <c>firstThrowAt</c> inside the fake removes
|
||||
/// that baseline so only the <c>Task.Delay(reconnectDelay)</c> contributes.</para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenSnapshotServiceThrowsOnce_ReconnectsAfterDelay()
|
||||
{
|
||||
@@ -60,12 +54,6 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
$"Expected at least 2 subscribe calls, got {snapshotService.SubscribeCount}.");
|
||||
Assert.True(hubContext.SendCount >= 1);
|
||||
|
||||
// Tests-031: the gap is measured from the moment the first subscribe
|
||||
// actually threw (inside the fake) to the moment the second subscribe
|
||||
// began (also inside the fake). This isolates the publisher's
|
||||
// Task.Delay(reconnectDelay) — no StartAsync / scheduling overhead in
|
||||
// the baseline. The 10ms slack absorbs Task.Delay's coarse Windows
|
||||
// timer quantum (~15ms) when the underlying scheduler wakes early.
|
||||
TimeSpan gap = secondSubscribeAt - firstThrowAt;
|
||||
Assert.True(gap >= reconnectDelay - TimeSpan.FromMilliseconds(10),
|
||||
$"Expected reconnect gap >= {reconnectDelay.TotalMilliseconds}ms; got {gap.TotalMilliseconds}ms.");
|
||||
@@ -75,6 +63,7 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
/// Sanity: a normal completion of WatchSnapshotsAsync (no exception)
|
||||
/// also reconnects after the delay — exits only on host shutdown.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenSnapshotServiceCompletes_ReconnectsAfterDelay()
|
||||
{
|
||||
@@ -114,7 +103,7 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
public int SubscribeCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tests-031: the wall-clock instant the first <c>WatchSnapshotsAsync</c> throws.
|
||||
/// The wall-clock instant the first <c>WatchSnapshotsAsync</c> throws.
|
||||
/// The reconnect-gap assertion is measured against this timestamp (NOT the
|
||||
/// pre-<c>StartAsync</c> wall clock) so scheduling overhead is not baselined
|
||||
/// into the lower bound.
|
||||
@@ -124,14 +113,13 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
/// <summary>Gets the wall-clock instant of the second subscription attempt.</summary>
|
||||
public DateTimeOffset? SecondSubscribeAt { get; private set; }
|
||||
|
||||
/// <summary>Gets the current snapshot.</summary>
|
||||
/// <inheritdoc />
|
||||
public DashboardSnapshot GetSnapshot()
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
|
||||
/// <summary>Watches for snapshot changes and yields them asynchronously.</summary>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -167,14 +155,13 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
/// <summary>Gets the number of subscription attempts.</summary>
|
||||
public int SubscribeCount { get; private set; }
|
||||
|
||||
/// <summary>Gets the current snapshot.</summary>
|
||||
/// <inheritdoc />
|
||||
public DashboardSnapshot GetSnapshot()
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
|
||||
/// <summary>Watches for snapshot changes and completes immediately.</summary>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CS1998 // async without await — IAsyncEnumerable contract requires async signature
|
||||
public async IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
@@ -211,35 +198,43 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
|
||||
/// <summary>Gets a client proxy excluding specified connections.</summary>
|
||||
/// <param name="excludedConnectionIds">Connection identifiers to exclude.</param>
|
||||
/// <returns>The recording client proxy shared by this fake.</returns>
|
||||
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => AllProxy;
|
||||
|
||||
/// <summary>Gets a client proxy for a specific connection.</summary>
|
||||
/// <param name="connectionId">The connection identifier.</param>
|
||||
/// <returns>The recording client proxy shared by this fake.</returns>
|
||||
public IClientProxy Client(string connectionId) => AllProxy;
|
||||
|
||||
/// <summary>Gets a client proxy for specified connections.</summary>
|
||||
/// <param name="connectionIds">The connection identifiers.</param>
|
||||
/// <returns>The recording client proxy shared by this fake.</returns>
|
||||
public IClientProxy Clients(IReadOnlyList<string> connectionIds) => AllProxy;
|
||||
|
||||
/// <summary>Gets a client proxy for a group.</summary>
|
||||
/// <param name="groupName">The group name.</param>
|
||||
/// <returns>The recording client proxy shared by this fake.</returns>
|
||||
public IClientProxy Group(string groupName) => AllProxy;
|
||||
|
||||
/// <summary>Gets a client proxy for a group excluding specified connections.</summary>
|
||||
/// <param name="groupName">The group name.</param>
|
||||
/// <param name="excludedConnectionIds">Connection identifiers to exclude.</param>
|
||||
/// <returns>The recording client proxy shared by this fake.</returns>
|
||||
public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => AllProxy;
|
||||
|
||||
/// <summary>Gets a client proxy for specified groups.</summary>
|
||||
/// <param name="groupNames">The group names.</param>
|
||||
/// <returns>The recording client proxy shared by this fake.</returns>
|
||||
public IClientProxy Groups(IReadOnlyList<string> groupNames) => AllProxy;
|
||||
|
||||
/// <summary>Gets a client proxy for a specific user.</summary>
|
||||
/// <param name="userId">The user identifier.</param>
|
||||
/// <returns>The recording client proxy shared by this fake.</returns>
|
||||
public IClientProxy User(string userId) => AllProxy;
|
||||
|
||||
/// <summary>Gets a client proxy for specified users.</summary>
|
||||
/// <param name="userIds">The user identifiers.</param>
|
||||
/// <returns>The recording client proxy shared by this fake.</returns>
|
||||
public IClientProxy Users(IReadOnlyList<string> userIds) => AllProxy;
|
||||
}
|
||||
|
||||
@@ -254,6 +249,7 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
/// <param name="method">The SignalR method name.</param>
|
||||
/// <param name="args">The method arguments.</param>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Interlocked.Increment(ref _sendCount);
|
||||
@@ -267,6 +263,7 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
/// <param name="connectionId">The connection identifier.</param>
|
||||
/// <param name="groupName">The group name.</param>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
@@ -274,6 +271,7 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
/// <param name="connectionId">The connection identifier.</param>
|
||||
/// <param name="groupName">The group name.</param>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ public sealed class DashboardSnapshotServiceTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression for Server-059: the shared library replaces the cache entry via
|
||||
/// The shared library replaces the cache entry via
|
||||
/// <c>previous with { ... }</c> at the <b>same</b> Sequence on a steady-state tick and on a
|
||||
/// refresh failure (Status → Unavailable, LastError set). The dashboard summary must reflect
|
||||
/// those per-tick-volatile fields, not a summary frozen at the last heavy-refresh sequence —
|
||||
@@ -356,7 +356,7 @@ public sealed class DashboardSnapshotServiceTests
|
||||
/// <summary>
|
||||
/// Verifies that a changed cache Sequence invalidates the memoized template breakdown and the
|
||||
/// next snapshot reflects the new object set. Guards against an inverted sequence check that
|
||||
/// would freeze the Galaxy section after its first load (Tests-041).
|
||||
/// would freeze the Galaxy section after its first load.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetSnapshot_WhenSequenceChanges_RecomputesTemplateBreakdown()
|
||||
@@ -405,6 +405,7 @@ public sealed class DashboardSnapshotServiceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that snapshot service refreshes API key summaries before each snapshot.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WatchSnapshotsAsync_RefreshesApiKeySummariesBeforeSnapshot()
|
||||
{
|
||||
@@ -441,6 +442,7 @@ public sealed class DashboardSnapshotServiceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that snapshot service reuses previous summaries when API key refresh fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WatchSnapshotsAsync_WhenApiKeyRefreshFails_ReusesPreviousSummaries()
|
||||
{
|
||||
@@ -484,6 +486,7 @@ public sealed class DashboardSnapshotServiceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that snapshot service disposes cleanly when subscriber cancels.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WatchSnapshotsAsync_WhenSubscriberCancels_DisposesCleanly()
|
||||
{
|
||||
@@ -575,31 +578,47 @@ public sealed class DashboardSnapshotServiceTests
|
||||
|
||||
private class FakeApiKeyAdminStore : IApiKeyAdminStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>Does nothing; the fake never persists created keys.</summary>
|
||||
/// <param name="record">API key record to create.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task CreateAsync(ApiKeyRecord record, CancellationToken ct)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Lists API keys; the base fake always returns an empty list.</summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>An empty list of API key summaries.</returns>
|
||||
public virtual Task<IReadOnlyList<ApiKeyListItem>> ListAsync(CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ApiKeyListItem>>([]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Does nothing; the fake never revokes keys.</summary>
|
||||
/// <param name="keyId">Key identifier to revoke.</param>
|
||||
/// <param name="whenUtc">Revocation timestamp.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns><see langword="false"/>, always.</returns>
|
||||
public Task<bool> RevokeAsync(string keyId, DateTimeOffset whenUtc, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Does nothing; the fake never rotates key secrets.</summary>
|
||||
/// <param name="keyId">Key identifier to rotate.</param>
|
||||
/// <param name="newSecretHash">New secret hash.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns><see langword="false"/>, always.</returns>
|
||||
public Task<bool> RotateAsync(string keyId, byte[] newSecretHash, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Does nothing; the fake never deletes keys.</summary>
|
||||
/// <param name="keyId">Key identifier to delete.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns><see langword="false"/>, always.</returns>
|
||||
public Task<bool> DeleteAsync(string keyId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
@@ -664,24 +683,16 @@ public sealed class DashboardSnapshotServiceTests
|
||||
int? processId,
|
||||
WorkerClientState state) : IWorkerClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the session identifier.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public string SessionId { get; } = sessionId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the process identifier.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public int? ProcessId { get; } = processId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current worker client state.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public WorkerClientState State { get; private set; } = state;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp of the last heartbeat.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.Parse("2026-04-26T10:02:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
@@ -699,24 +710,14 @@ public sealed class DashboardSnapshotServiceTests
|
||||
/// </summary>
|
||||
public int KillCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts the worker client asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
StartCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a worker command asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to invoke.</param>
|
||||
/// <param name="timeout">Command timeout.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Command reply.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<WorkerCommandReply> InvokeAsync(
|
||||
WorkerCommand command,
|
||||
TimeSpan timeout,
|
||||
@@ -725,11 +726,7 @@ public sealed class DashboardSnapshotServiceTests
|
||||
return Task.FromResult(new WorkerCommandReply());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads events from the worker asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Async enumerable of worker events.</returns>
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -737,12 +734,7 @@ public sealed class DashboardSnapshotServiceTests
|
||||
yield break;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down the worker client asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="timeout">Shutdown timeout.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completed task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -752,10 +744,7 @@ public sealed class DashboardSnapshotServiceTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the worker client.
|
||||
/// </summary>
|
||||
/// <param name="reason">Reason for termination.</param>
|
||||
/// <inheritdoc />
|
||||
public void Kill(string reason)
|
||||
{
|
||||
KillCount++;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
public sealed class HubTokenServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-039: a token whose data-protected payload has both
|
||||
/// A token whose data-protected payload has both
|
||||
/// <c>Name</c> and <c>NameIdentifier</c> null (the principal that
|
||||
/// minted the token had no identity claims) must be rejected by
|
||||
/// <see cref="HubTokenService.Validate"/>. The role claims alone are
|
||||
@@ -38,7 +38,7 @@ public sealed class HubTokenServiceTests
|
||||
/// <summary>
|
||||
/// Sanity check: a token minted from a principal with a Name claim
|
||||
/// validates and returns a principal carrying that identity. Pins
|
||||
/// that the Server-039 fix does not over-reject valid tokens.
|
||||
/// that the null-identity rejection above does not over-reject valid tokens.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_TokenWithName_ReturnsAuthenticatedPrincipal()
|
||||
|
||||
Reference in New Issue
Block a user