docs: strip task-tracking IDs from code comments and fill missing XML docs
Tracking IDs are review bookkeeping that rots in shipped source; the rationale they tagged is kept. Comment-only change (CommentChecker 458 -> 16 false positives).
This commit is contained in:
@@ -19,7 +19,7 @@ public sealed class AlarmWatchListResolverTests
|
||||
/// <summary>
|
||||
/// Resolves and projects to the target list. These cases assert composition rules, which
|
||||
/// live in <see cref="AlarmWatchListResolution.Targets"/>; the discovery-provenance fields
|
||||
/// the resolution also carries (GWC-37) are asserted by their own cases below.
|
||||
/// the resolution also carries are asserted by their own cases below.
|
||||
/// </summary>
|
||||
/// <param name="resolver">The resolver under test.</param>
|
||||
/// <param name="options">Alarm options to resolve against.</param>
|
||||
@@ -259,7 +259,7 @@ public sealed class AlarmWatchListResolverTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-37: swallowing the discovery failure is right for the resolver but leaves the
|
||||
/// Swallowing the discovery failure is right for the resolver but leaves the
|
||||
/// caller unable to tell "discovery produced nothing" from "nothing was configured", which
|
||||
/// is why the empty watch-list could sit unnoticed for a monitor's whole life. The
|
||||
/// resolution therefore reports whether discovery was attempted, whether it failed, and
|
||||
@@ -284,7 +284,7 @@ public sealed class AlarmWatchListResolverTests
|
||||
/// <summary>
|
||||
/// The complement of the case above: discovery disabled is not discovery failed. An empty
|
||||
/// watch-list here means the deployment did not ask for subtag coverage, which the monitor
|
||||
/// must not report as a degraded standby (GWC-37).
|
||||
/// must not report as a degraded standby.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
|
||||
@@ -13,7 +13,7 @@ using ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Alarms;
|
||||
|
||||
/// <summary>
|
||||
/// GWC-26 regression tests for the alarm monitor's startup ordering. Unlike the
|
||||
/// Regression tests for the alarm monitor's startup ordering. Unlike the
|
||||
/// sibling alarm-monitor tests, the session manager here hands the monitor a REAL
|
||||
/// <see cref="GatewaySession"/> that is driven to Ready with a dashboard mirror, so the
|
||||
/// distributor pump is already running when the monitor attaches — the production
|
||||
@@ -156,7 +156,7 @@ public sealed class GatewayAlarmMonitorAttachOrderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-35: an acknowledged cached alarm followed by an active snapshot with a newer
|
||||
/// An acknowledged cached alarm followed by an active snapshot with a newer
|
||||
/// original-raise timestamp is a new alarm instance, not an acknowledgement reversal. The
|
||||
/// reconcile repair must broadcast a Raise carrying the incoming instance identity.
|
||||
/// </summary>
|
||||
@@ -216,7 +216,7 @@ public sealed class GatewayAlarmMonitorAttachOrderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NEXT-03. A reconcile Raise repair applied while the matching live Raise is still
|
||||
/// A reconcile Raise repair applied while the matching live Raise is still
|
||||
/// buffered must not double-broadcast: the live transition carrying the same worker
|
||||
/// timestamp and resulting state the cache already holds is a duplicate and is suppressed.
|
||||
/// </summary>
|
||||
@@ -275,7 +275,7 @@ public sealed class GatewayAlarmMonitorAttachOrderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NEXT-03, the Clear leg. A reconcile Clear repair removes the cache entry, so the
|
||||
/// The Clear leg of the dedup. A reconcile Clear repair removes the cache entry, so the
|
||||
/// buffered live Clear is deduped through the tombstone keyed on the instance's original
|
||||
/// raise timestamp — and a genuinely new raise/clear cycle is never swallowed.
|
||||
/// </summary>
|
||||
@@ -490,7 +490,7 @@ public sealed class GatewayAlarmMonitorAttachOrderTests
|
||||
SourceProvider = AlarmProviderMode.Alarmmgr,
|
||||
};
|
||||
|
||||
// Snapshot carrying the worker-side identity markers the NEXT-03 dedup compares on.
|
||||
// Snapshot carrying the worker-side identity markers the dedup compares on.
|
||||
private static ActiveAlarmSnapshot SnapshotAt(
|
||||
AlarmConditionState state,
|
||||
Timestamp lastTransition,
|
||||
@@ -502,7 +502,7 @@ public sealed class GatewayAlarmMonitorAttachOrderTests
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
// Live transition with explicit worker timestamps, for driving the NEXT-03 dedup.
|
||||
// Live transition with explicit worker timestamps, for driving the dedup.
|
||||
private static MxEvent TransitionAt(
|
||||
ulong sequence,
|
||||
AlarmTransitionKind kind,
|
||||
|
||||
@@ -624,7 +624,7 @@ public sealed class GatewayAlarmMonitorProviderModeTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-37: a Galaxy Repository outage at startup used to leave one <c>LogWarning</c> and an
|
||||
/// A Galaxy Repository outage at startup used to leave one <c>LogWarning</c> and an
|
||||
/// empty subtag watch-list that stayed empty for the monitor's whole life — invisible until
|
||||
/// a failover found the standby covering nothing. The condition is now a standing verdict
|
||||
/// on the provider status, so it reaches the alarm feed, the dashboard badge and every
|
||||
@@ -661,7 +661,7 @@ public sealed class GatewayAlarmMonitorProviderModeTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-37: the failover edge is the moment the watch-list stops being theoretical, so the
|
||||
/// The failover edge is the moment the watch-list stops being theoretical, so the
|
||||
/// monitor re-resolves it there, hot-swaps the recovered list through
|
||||
/// <c>UpdateAlarmWatchList</c>, and publishes the acknowledged verdict on the same status
|
||||
/// frame the failover produces. The update must precede the status frame so the frame never
|
||||
@@ -748,6 +748,7 @@ public sealed class GatewayAlarmMonitorProviderModeTests
|
||||
}
|
||||
|
||||
/// <summary>Periodic reconciliation applies a recovered watch list once and suppresses an unchanged resolution on the next cadence.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WatchListRefresh_PeriodicRecovery_SendsExactlyOneMaterialUpdate()
|
||||
{
|
||||
@@ -774,6 +775,7 @@ public sealed class GatewayAlarmMonitorProviderModeTests
|
||||
}
|
||||
|
||||
/// <summary>A rejected refresh keeps the old fingerprint and degraded verdict so the same material change is retried.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task WatchListRefresh_WorkerRejects_RetainsFingerprintAndRetriesSameTargets()
|
||||
{
|
||||
@@ -937,7 +939,7 @@ public sealed class GatewayAlarmMonitorProviderModeTests
|
||||
/// <see cref="IAlarmWatchListResolver"/> that hands out a scripted sequence of
|
||||
/// resolutions — one per call, the last repeating — and counts the calls, so a test can
|
||||
/// drive "discovery was down at subscribe and back up by the failover" and assert that the
|
||||
/// second resolution happened at all (GWC-37).
|
||||
/// second resolution happened at all.
|
||||
/// </summary>
|
||||
/// <param name="script">Resolutions to return, in call order.</param>
|
||||
private sealed class ScriptedWatchListResolver(params AlarmWatchListResolution[] script) : IAlarmWatchListResolver
|
||||
@@ -997,6 +999,8 @@ public sealed class GatewayAlarmMonitorProviderModeTests
|
||||
_events.Writer.TryWrite(new WorkerEvent { Event = mxEvent });
|
||||
|
||||
/// <summary>Pushes a provider-mode change onto the worker event stream.</summary>
|
||||
/// <param name="mode">The provider mode being reported.</param>
|
||||
/// <param name="reason">The reason for the mode change.</param>
|
||||
public void EmitModeChange(AlarmProviderMode mode, string reason) => EmitEvent(new MxEvent
|
||||
{
|
||||
OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent
|
||||
@@ -1008,6 +1012,9 @@ public sealed class GatewayAlarmMonitorProviderModeTests
|
||||
});
|
||||
|
||||
/// <summary>Waits until at least the requested number of update commands have arrived.</summary>
|
||||
/// <param name="count">The minimum number of update commands to wait for.</param>
|
||||
/// <param name="timeout">The maximum time to wait.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task WaitForUpdateCountAsync(int count, TimeSpan timeout)
|
||||
{
|
||||
await WaitUntilAsync(() => UpdateCommands.Count >= count, timeout);
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Gateway-owned validation of the shared <see cref="GalaxyRepositoryOptions"/> (SEC-33): the Galaxy
|
||||
/// Gateway-owned validation of the shared <see cref="GalaxyRepositoryOptions"/>: the Galaxy
|
||||
/// package binds the options but ships no validator, so the gateway rejects a persisted snapshot
|
||||
/// whose path is blank, invalid, or non-rooted on the running host, and supplies a rooted per-OS
|
||||
/// default when the shipped config leaves the path blank.
|
||||
@@ -109,7 +109,7 @@ public sealed class GalaxyRepositoryOptionsValidatorTests
|
||||
/// <summary>
|
||||
/// Verifies the gateway supplies a rooted per-OS default when the shipped config leaves
|
||||
/// SnapshotCachePath blank, so the removed appsettings literal is not needed and validation
|
||||
/// passes on any host (SEC-33). Resolving the options triggers the registered validator.
|
||||
/// passes on any host. Resolving the options triggers the registered validator.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Build_DefaultsBlankSnapshotCachePathToRootedDefault()
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The projection is hand-written member by member, so an option that exists and validates can
|
||||
/// still be invisible on the settings page — which is exactly what happened to the two SEC-25
|
||||
/// still be invisible on the settings page — which is exactly what happened to the two
|
||||
/// dashboard-ACL options. These tests pin the dashboard-tag members to the options they come
|
||||
/// from; the redaction assertions pin the opposite invariant, that the two secret-bearing members
|
||||
/// are the only ones masked.
|
||||
|
||||
@@ -86,7 +86,7 @@ public sealed class GatewayOptionsTests
|
||||
["MxGateway:Events:QueueCapacity"] = "256",
|
||||
["MxGateway:Dashboard:Enabled"] = "false",
|
||||
["MxGateway:Protocol:MaxGrpcMessageBytes"] = "8388608",
|
||||
// The frame-headroom rule is two-sided (IPC-33): the worker-frame maximum must sit
|
||||
// The frame-headroom rule is two-sided: the worker-frame maximum must sit
|
||||
// between one and two 64 KiB envelope reserves above the gRPC cap, so moving the
|
||||
// gRPC cap without moving this one no longer validates. Kept at one reserve above,
|
||||
// the same shape as the shipped default.
|
||||
@@ -175,7 +175,7 @@ public sealed class GatewayOptionsTests
|
||||
/// but only the binder decides whether that instance is populated in place or replaced by a
|
||||
/// default-comparer one. Asserting the comparer on a hand-constructed <see cref="DashboardOptions"/>
|
||||
/// would prove nothing about the configured path; a mis-cased LDAP group name from the directory
|
||||
/// would then silently grant no tags, and the SEC-25 ACL would deny with no diagnostic.
|
||||
/// would then silently grant no tags, and the dashboard session ACL would deny with no diagnostic.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void DashboardOptions_GroupToTag_BindsCaseInsensitively()
|
||||
|
||||
@@ -778,7 +778,7 @@ public sealed class GatewayOptionsValidatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies rooting is host-meaningful (SEC-33): a Windows drive-qualified literal fails on a
|
||||
/// Verifies rooting is host-meaningful: a Windows drive-qualified literal fails on a
|
||||
/// Unix host (where it is not rooted) rather than being blessed and written as a junk-named
|
||||
/// relative file. On Windows the same literal is genuinely rooted and passes.
|
||||
/// </summary>
|
||||
@@ -805,7 +805,7 @@ public sealed class GatewayOptionsValidatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies MxGateway:Events:QueueCapacity above int.MaxValue/2 fails (GWC-24 rider): the value
|
||||
/// Verifies MxGateway:Events:QueueCapacity above int.MaxValue/2 fails: the value
|
||||
/// flows into WorkerClient as checked(2 * EventChannelCapacity), which would otherwise overflow.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -1078,7 +1078,7 @@ public sealed class GatewayOptionsValidatorTests
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Ldap:ServiceAccountPassword is required when LDAP login is enabled."));
|
||||
|
||||
// SEC-36: the message must steer the operator to the two supported out-of-band channels
|
||||
// The message must steer the operator to the two supported out-of-band channels
|
||||
// (dev user-secrets, deployed env var) so a blanked/unresolved credential never gets
|
||||
// "fixed" by re-committing a value.
|
||||
Assert.Contains("dotnet user-secrets set", failure);
|
||||
@@ -1258,7 +1258,7 @@ public sealed class GatewayOptionsValidatorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the headroom rule's upper half (IPC-33): a worker-frame maximum more than two
|
||||
/// Verifies the headroom rule's upper half: a worker-frame maximum more than two
|
||||
/// envelope reserves above the gRPC cap is rejected. The worker budgets a DrainEvents reply at
|
||||
/// the frame maximum less two reserves and the gateway returns that same reply through
|
||||
/// <c>Invoke</c>, so a wider gap lets the worker pack a reply the pipe carries and the gRPC send
|
||||
|
||||
@@ -18,6 +18,10 @@ namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
|
||||
/// </summary>
|
||||
public sealed class PreHostSecretExpansionTests
|
||||
{
|
||||
/// <summary>
|
||||
/// A seeded secret reference in configuration resolves to its plaintext.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExpandConfiguration_SeededReference_ResolvesToPlaintext()
|
||||
{
|
||||
@@ -37,6 +41,7 @@ public sealed class PreHostSecretExpansionTests
|
||||
/// <c>${secret:ldap/mxgateway/bind}</c>. Proves that exact reference at that exact key resolves to
|
||||
/// the seeded plaintext through the same pre-host expander the host runs.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExpandConfiguration_LdapBindReference_ResolvesToSeededPassword()
|
||||
{
|
||||
@@ -52,6 +57,10 @@ public sealed class PreHostSecretExpansionTests
|
||||
Assert.Equal(bindPassword, config["MxGateway:Ldap:ServiceAccountPassword"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A reference to an absent secret throws instead of leaving the token in place.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExpandConfiguration_MissingReference_ThrowsFailClosed()
|
||||
{
|
||||
@@ -81,8 +90,11 @@ public sealed class PreHostSecretExpansionTests
|
||||
_envVarName = envVarName;
|
||||
}
|
||||
|
||||
/// <summary>Gets the secret resolver from the fixture's DI graph.</summary>
|
||||
public ISecretResolver Resolver => _provider.GetRequiredService<ISecretResolver>();
|
||||
|
||||
/// <summary>Creates a fixture with a unique SQLite store and master-key environment variable.</summary>
|
||||
/// <returns>The migrated fixture.</returns>
|
||||
public static SecretsFixture Create()
|
||||
{
|
||||
string unique = Guid.NewGuid().ToString("N");
|
||||
@@ -111,8 +123,13 @@ public sealed class PreHostSecretExpansionTests
|
||||
return new SecretsFixture(provider, sqlitePath, envVarName);
|
||||
}
|
||||
|
||||
// Seals a plaintext value under a fresh DEK and upserts it — the same store/cipher pair the
|
||||
// CLI 'secret set' verb uses.
|
||||
/// <summary>
|
||||
/// Seals a plaintext value under a fresh DEK and upserts it, using the same store/cipher pair
|
||||
/// the CLI 'secret set' verb uses.
|
||||
/// </summary>
|
||||
/// <param name="name">The secret name.</param>
|
||||
/// <param name="plaintext">The plaintext value to store.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task SeedAsync(string name, string plaintext)
|
||||
{
|
||||
var secretName = new SecretName(name);
|
||||
@@ -122,11 +139,15 @@ public sealed class PreHostSecretExpansionTests
|
||||
await store.UpsertAsync(sealed_, default);
|
||||
}
|
||||
|
||||
/// <summary>Builds an in-memory configuration from the given key/value pairs.</summary>
|
||||
/// <param name="entries">The configuration entries.</param>
|
||||
/// <returns>The built configuration root.</returns>
|
||||
public IConfigurationRoot BuildConfig(params (string Key, string Value)[] entries) =>
|
||||
new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(entries.ToDictionary(e => e.Key, e => (string?)e.Value))
|
||||
.Build();
|
||||
|
||||
/// <summary>Disposes the service provider and clears the test environment variable.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_provider.Dispose();
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Renders <see cref="MainLayout"/> and asserts which policy-gated links the side rail emits —
|
||||
/// Secrets (<c>secrets:manage</c>) and, since SEC-40, Alarms
|
||||
/// Secrets (<c>secrets:manage</c>) and Alarms
|
||||
/// (<see cref="DashboardAuthenticationDefaults.AlarmsPolicy"/>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
@@ -100,7 +100,7 @@ public sealed class SecretsNavRenderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-40's nav half: an anonymous caller — the read-only localhost path — is not offered the
|
||||
/// The nav half: an anonymous caller — the read-only localhost path — is not offered the
|
||||
/// Alarms link, because the page and hub now refuse them. Same reasoning as the Secrets
|
||||
/// negative above: this is the assertion that distinguishes a gate from an inert one.
|
||||
/// </summary>
|
||||
@@ -114,7 +114,7 @@ public sealed class SecretsNavRenderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The control for that negative: an authenticated Viewer still gets the Alarms link. SEC-40
|
||||
/// The control for that negative: an authenticated Viewer still gets the Alarms link. The Alarms gate
|
||||
/// excludes the environment bypasses, not Viewers — alarm metadata stays galaxy-wide for a
|
||||
/// real dashboard role because the central monitor carries no per-session provenance.
|
||||
/// </summary>
|
||||
@@ -135,7 +135,7 @@ public sealed class SecretsNavRenderTests
|
||||
{
|
||||
options.AddSecretsAuthorization();
|
||||
// The rail's Alarms AuthorizeView resolves this by name; an unregistered policy throws
|
||||
// rather than denying, so it must be present for any rail render (SEC-40).
|
||||
// rather than denying, so it must be present for any rail render.
|
||||
options.AddPolicy(
|
||||
DashboardAuthenticationDefaults.AlarmsPolicy,
|
||||
policy => policy.AddRequirements(AuthenticatedDashboardRoleRequirement.AnyDashboardRole));
|
||||
@@ -163,6 +163,7 @@ public sealed class SecretsNavRenderTests
|
||||
// vacuously.
|
||||
private sealed class StubAuthenticationStateProvider(string[] roles) : AuthenticationStateProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
ClaimsIdentity identity = roles.Length == 0
|
||||
@@ -181,6 +182,7 @@ public sealed class SecretsNavRenderTests
|
||||
// under static rendering.
|
||||
private sealed class StubNavigationManager : NavigationManager
|
||||
{
|
||||
/// <summary>Initializes the stub with a fixed localhost base URI.</summary>
|
||||
public StubNavigationManager() => Initialize("https://localhost/", "https://localhost/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ using HtmlRenderer = Microsoft.AspNetCore.Components.Web.HtmlRenderer;
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Renders <see cref="SettingsPage"/> and asserts the two SEC-25 dashboard-ACL options —
|
||||
/// Renders <see cref="SettingsPage"/> and asserts the two dashboard-ACL options —
|
||||
/// <c>Dashboard:GroupToTag</c> and <c>Dashboard:UntaggedSessionVisibility</c> — reach the page.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
|
||||
@@ -16,7 +16,7 @@ using AuthenticationOptions = ZB.MOM.WW.MxGateway.Server.Configuration.Authentic
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Covers SEC-40: the alarm feed requires a real authenticated dashboard role, so the read-only
|
||||
/// Covers the rule that the alarm feed requires a real authenticated dashboard role, so the read-only
|
||||
/// environment bypasses (<c>Authentication:Mode=Disabled</c>, anonymous localhost) no longer
|
||||
/// receive galaxy-wide alarm metadata.
|
||||
/// </summary>
|
||||
@@ -65,7 +65,7 @@ public sealed class AlarmFeedAuthorizationTests
|
||||
|
||||
/// <summary>
|
||||
/// An authenticated Viewer — the ordinary LDAP-logged-in dashboard user — does satisfy the
|
||||
/// alarm requirement: SEC-40 excludes the environment bypasses, not Viewers. Alarm metadata
|
||||
/// alarm requirement: the rule excludes the environment bypasses, not Viewers. Alarm metadata
|
||||
/// stays galaxy-wide for them because the monitor is gateway-central and carries no
|
||||
/// per-session provenance to tag-scope by.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="AlarmsHubPublisher"/> honours
|
||||
/// <c>MxGateway:Dashboard:ShowTagValues</c> (TST-16): the alarm value fields of
|
||||
/// <c>MxGateway:Dashboard:ShowTagValues</c>: the alarm value fields of
|
||||
/// both value-bearing payload arms are stripped from the copy broadcast to
|
||||
/// browser clients when the flag is off, present when it is on, and the source
|
||||
/// <see cref="AlarmFeedMessage"/> — shared with the gRPC <c>StreamAlarms</c>
|
||||
@@ -286,22 +286,48 @@ public sealed class AlarmsHubPublisherTests
|
||||
/// <summary>Gets the capturing client proxy shared by this fake.</summary>
|
||||
public CapturingClientProxy GroupProxy { get; } = new();
|
||||
|
||||
/// <summary>Gets the shared capturing proxy for all clients.</summary>
|
||||
public IClientProxy All => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy, ignoring the exclusions.</summary>
|
||||
/// <param name="excludedConnectionIds">Connection ids to exclude.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for one connection.</summary>
|
||||
/// <param name="connectionId">The target connection id.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Client(string connectionId) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for several connections.</summary>
|
||||
/// <param name="connectionIds">The target connection ids.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Clients(IReadOnlyList<string> connectionIds) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for one group.</summary>
|
||||
/// <param name="groupName">The target group name.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Group(string groupName) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for a group, ignoring the exclusions.</summary>
|
||||
/// <param name="groupName">The target group name.</param>
|
||||
/// <param name="excludedConnectionIds">Connection ids to exclude.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for several groups.</summary>
|
||||
/// <param name="groupNames">The target group names.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Groups(IReadOnlyList<string> groupNames) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for one user.</summary>
|
||||
/// <param name="userId">The target user id.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy User(string userId) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for several users.</summary>
|
||||
/// <param name="userIds">The target user ids.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Users(IReadOnlyList<string> userIds) => GroupProxy;
|
||||
}
|
||||
|
||||
@@ -342,9 +368,19 @@ public sealed class AlarmsHubPublisherTests
|
||||
|
||||
private sealed class NoopGroupManager : IGroupManager
|
||||
{
|
||||
/// <summary>Ignores the membership change and completes synchronously.</summary>
|
||||
/// <param name="connectionId">The connection being added.</param>
|
||||
/// <param name="groupName">The group the connection joins.</param>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Ignores the removal and completes synchronously.</summary>
|
||||
/// <param name="connectionId">The connection being removed.</param>
|
||||
/// <param name="groupName">The group the connection leaves.</param>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ public sealed class DashboardBrowseAndAlarmModelTests
|
||||
/// <summary>
|
||||
/// An explicitly-degraded status whose mode is still Alarmmgr (the
|
||||
/// <c>Degraded || Mode==Subtag</c> guard's second, independent branch) must map to the
|
||||
/// amber badge, but not to the failover label: GWC-37 gives that combination a meaning —
|
||||
/// amber badge, but not to the failover label: that combination has its own meaning —
|
||||
/// the alarm manager is serving and the subtag standby behind it covers nothing — so
|
||||
/// reporting "Subtag monitoring (degraded)" would name the wrong live provider.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="DashboardEventBroadcaster"/> honours
|
||||
/// <c>MxGateway:Dashboard:ShowTagValues</c> (SEC-25): tag values are stripped
|
||||
/// <c>MxGateway:Dashboard:ShowTagValues</c>: tag values are stripped
|
||||
/// from the mirrored copy when the flag is off, present when it is on, and the
|
||||
/// shared source event is never mutated. Also verifies the viewer gate — the
|
||||
/// mirror does no work at all for a session nobody is watching.
|
||||
@@ -325,22 +325,48 @@ public sealed class DashboardEventBroadcasterTests
|
||||
/// <summary>Gets the capturing client proxy shared by this fake.</summary>
|
||||
public CapturingClientProxy GroupProxy { get; } = new();
|
||||
|
||||
/// <summary>Gets the shared capturing proxy for all connections.</summary>
|
||||
public IClientProxy All => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy, ignoring exclusions.</summary>
|
||||
/// <param name="excludedConnectionIds">Ignored.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for any connection.</summary>
|
||||
/// <param name="connectionId">Ignored.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Client(string connectionId) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for any connection set.</summary>
|
||||
/// <param name="connectionIds">Ignored.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Clients(IReadOnlyList<string> connectionIds) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for any group.</summary>
|
||||
/// <param name="groupName">Ignored.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Group(string groupName) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for any group, ignoring exclusions.</summary>
|
||||
/// <param name="groupName">Ignored.</param>
|
||||
/// <param name="excludedConnectionIds">Ignored.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for any group set.</summary>
|
||||
/// <param name="groupNames">Ignored.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Groups(IReadOnlyList<string> groupNames) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for any user.</summary>
|
||||
/// <param name="userId">Ignored.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy User(string userId) => GroupProxy;
|
||||
|
||||
/// <summary>Returns the shared capturing proxy for any user set.</summary>
|
||||
/// <param name="userIds">Ignored.</param>
|
||||
/// <returns>The shared capturing proxy.</returns>
|
||||
public IClientProxy Users(IReadOnlyList<string> userIds) => GroupProxy;
|
||||
}
|
||||
|
||||
@@ -367,9 +393,19 @@ public sealed class DashboardEventBroadcasterTests
|
||||
|
||||
private sealed class NoopGroupManager : IGroupManager
|
||||
{
|
||||
/// <summary>Ignores the group membership change.</summary>
|
||||
/// <param name="connectionId">Ignored.</param>
|
||||
/// <param name="groupName">Ignored.</param>
|
||||
/// <param name="cancellationToken">Ignored.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Ignores the group membership change.</summary>
|
||||
/// <param name="connectionId">Ignored.</param>
|
||||
/// <param name="groupName">Ignored.</param>
|
||||
/// <param name="cancellationToken">Ignored.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public sealed class DashboardLiveDataServiceTests
|
||||
/// <summary>
|
||||
/// Verifies a tag already in the advise set is not subscribed again on a later read.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WhenTagAlreadySubscribed_DoesNotResubscribe()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public sealed class DashboardLiveDataServiceTests
|
||||
/// Verifies the advise set is capped: subscribing past the cap unadvises the
|
||||
/// least-recently-read tag on the worker and leaves the re-read tag advised.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_PastCap_EvictsLeastRecentlyReadTag()
|
||||
{
|
||||
@@ -78,6 +80,7 @@ public sealed class DashboardLiveDataServiceTests
|
||||
/// than the cap keeps them all (a read never evicts a tag it is about to return),
|
||||
/// and the next read that subscribes anything squeezes the overshoot back out.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WithMoreDistinctTagsThanCap_KeepsThemAllThenSelfCorrects()
|
||||
{
|
||||
@@ -108,6 +111,7 @@ public sealed class DashboardLiveDataServiceTests
|
||||
/// touches nearly the whole advise set evicts only the untouched remainder, ends over
|
||||
/// the cap, and the following read trims it back.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WhenTouchedTagsFillTheCap_EvictsOnlyUntouchedTags()
|
||||
{
|
||||
@@ -135,6 +139,7 @@ public sealed class DashboardLiveDataServiceTests
|
||||
/// Verifies a tag the worker failed to advise still occupies a slot but is evicted
|
||||
/// without any unsubscribe command — there is no item handle to unadvise.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WhenAdviseFailed_EvictsTagWithoutUnsubscribing()
|
||||
{
|
||||
@@ -165,6 +170,7 @@ public sealed class DashboardLiveDataServiceTests
|
||||
/// Verifies a failed unadvise of an evicted tag does not fail the read, and the
|
||||
/// evicted tag is dropped from tracking anyway.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WhenEvictionUnsubscribeFails_StillCompletesRead()
|
||||
{
|
||||
@@ -190,10 +196,11 @@ public sealed class DashboardLiveDataServiceTests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the <c>/browse</c> live-value seam honours
|
||||
/// <c>MxGateway:Dashboard:ShowTagValues</c> (TST-16): with the flag off — the
|
||||
/// <c>MxGateway:Dashboard:ShowTagValues</c>: with the flag off — the
|
||||
/// default — the value text the page renders is the redaction placeholder,
|
||||
/// never the formatted tag value.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WhenShowTagValuesFalse_RedactsValueTextButKeepsMetadata()
|
||||
{
|
||||
@@ -220,6 +227,7 @@ public sealed class DashboardLiveDataServiceTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies the formatted value is served when the flag is on.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WhenShowTagValuesTrue_ServesFormattedValue()
|
||||
{
|
||||
@@ -243,6 +251,7 @@ public sealed class DashboardLiveDataServiceTests
|
||||
/// withheld, and its diagnostic still reaches the panel. Redacting it would
|
||||
/// contradict the error the same row displays.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WhenReadFailedAndShowTagValuesFalse_LeavesPlaceholderUnredacted()
|
||||
{
|
||||
@@ -489,7 +498,8 @@ public sealed class DashboardLiveDataServiceTests
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Completes immediately because the fake holds no resources.</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
private BulkSubscribeReply Subscribe(IEnumerable<string> tagAddresses)
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Covers <see cref="DashboardSessionAcl"/>, the single decision both dashboard subscribe seams
|
||||
/// consult (SEC-25 / TST-15).
|
||||
/// consult.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every branch is asserted in its denying direction as well as its allowing one, because the
|
||||
@@ -77,6 +77,7 @@ public sealed class DashboardSessionAclTests
|
||||
}
|
||||
|
||||
/// <summary>A blank session id is denied without consulting anything.</summary>
|
||||
/// <param name="sessionId">A blank or whitespace-only session id.</param>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
@@ -146,8 +147,8 @@ public sealed class DashboardSessionAclTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A principal carrying no tag claims — the anonymous-localhost / empty-grant Viewer of
|
||||
/// SEC-02 — sees a tagged session never, and an untagged one only when the operator opted
|
||||
/// A principal carrying no tag claims — the anonymous-localhost / empty-grant Viewer —
|
||||
/// sees a tagged session never, and an untagged one only when the operator opted
|
||||
/// into <see cref="UntaggedSessionVisibility.AllViewers"/>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -179,7 +180,7 @@ public sealed class DashboardSessionAclTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-41, the load-bearing regression: a cookie-style principal whose tag claim was stamped
|
||||
/// The load-bearing regression: a cookie-style principal whose tag claim was stamped
|
||||
/// at login under an older <c>GroupToTag</c> map is DENIED once that map no longer grants the
|
||||
/// tag to any group the principal belongs to. The auth cookie survives the restart that
|
||||
/// rebinds options, so trusting the stamped claim let a config-revoked grant live on for the
|
||||
@@ -224,7 +225,7 @@ public sealed class DashboardSessionAclTests
|
||||
/// <summary>
|
||||
/// Re-resolution is ordinal-ignore-case on both sides — the group key against the map (which
|
||||
/// <see cref="DashboardGroupTagMapping"/> owns) and the resolved tag against the session's tag
|
||||
/// set. Pinned because the pre-SEC-41 claim scan got the second comparison from
|
||||
/// set. Pinned because the earlier claim scan got the second comparison from
|
||||
/// <c>Session.Tags</c>'s own comparer, and the re-resolution arm probes the grant set in the
|
||||
/// opposite direction.
|
||||
/// </summary>
|
||||
@@ -294,7 +295,7 @@ public sealed class DashboardSessionAclTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a dashboard principal. <paramref name="groups"/> is the SEC-41 discriminator: a
|
||||
/// Builds a dashboard principal. <paramref name="groups"/> is the discriminator: a
|
||||
/// principal WITH group claims is re-resolved against the current map (the cookie seam), one
|
||||
/// WITHOUT falls back to its stamped tag claims (the hub-token seam). Existing cases pass
|
||||
/// tags only, so they continue to exercise the fallback arm exactly as before.
|
||||
|
||||
@@ -7,7 +7,7 @@ using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Covers the ACL gate on <see cref="EventsHub.SubscribeSession"/> (SEC-25 / TST-15).
|
||||
/// Covers the ACL gate on <see cref="EventsHub.SubscribeSession"/> .
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The denial assertions are the load-bearing ones — before the gate existed every caller was
|
||||
@@ -131,7 +131,11 @@ public sealed class EventsHubTests
|
||||
/// <summary>Gets the (connection id, group name) pairs added, in order.</summary>
|
||||
public List<(string ConnectionId, string GroupName)> Added { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Records the group membership and completes synchronously.</summary>
|
||||
/// <param name="connectionId">The connection being added.</param>
|
||||
/// <param name="groupName">The group the connection joins.</param>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Added.Add((connectionId, groupName));
|
||||
@@ -139,7 +143,11 @@ public sealed class EventsHubTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Ignores the removal and completes synchronously.</summary>
|
||||
/// <param name="connectionId">The connection being removed.</param>
|
||||
/// <param name="groupName">The group the connection leaves.</param>
|
||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task RemoveFromGroupAsync(
|
||||
string connectionId,
|
||||
string groupName,
|
||||
|
||||
@@ -129,6 +129,8 @@ public sealed class EventsHubViewerRegistryTests
|
||||
}
|
||||
|
||||
/// <summary>Blank connection or session ids are ignored rather than tracked.</summary>
|
||||
/// <param name="connectionId">The hub connection id, possibly blank.</param>
|
||||
/// <param name="sessionId">The session id, possibly blank.</param>
|
||||
[Theory]
|
||||
[InlineData("", "session-1")]
|
||||
[InlineData(" ", "session-1")]
|
||||
|
||||
@@ -178,7 +178,7 @@ public sealed class HubTokenServiceTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The dashboard visibility grant (SEC-25) survives the mint/validate round-trip: tags are
|
||||
/// The dashboard visibility grant survives the mint/validate round-trip: tags are
|
||||
/// resolved from the caller's LDAP-group claims through <c>Dashboard:GroupToTag</c> at
|
||||
/// <see cref="HubTokenService.Issue(ClaimsPrincipal)"/> and rehydrated as
|
||||
/// <see cref="DashboardAuthenticationDefaults.DashboardTagClaimType"/> claims on the principal
|
||||
|
||||
@@ -16,7 +16,7 @@ using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Covers the ACL gate on the session-details page's in-process subscribe seam (SEC-25 / TST-15).
|
||||
/// Covers the ACL gate on the session-details page's in-process subscribe seam.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -214,7 +214,7 @@ public sealed class SessionDetailsPageEventAclTests
|
||||
/// <inheritdoc />
|
||||
public ChannelReader<MxEvent> Reader => _channel.Reader;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Marks the subscription as released and completes its channel.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
|
||||
@@ -81,7 +81,7 @@ public sealed class GatewayApplicationTests
|
||||
{
|
||||
// Bind an ephemeral port (:0) — xUnit runs test collections in parallel, so any
|
||||
// started-host test must avoid a fixed port to prevent a bind collision. The auth SQLite
|
||||
// store path is isolated to a per-process temp file by TestHostEnvironmentInitializer (SEC-33).
|
||||
// store path is isolated to a per-process temp file by TestHostEnvironmentInitializer.
|
||||
await using WebApplication app = GatewayApplication.Build(["--urls=http://127.0.0.1:0"]);
|
||||
await app.StartAsync();
|
||||
try
|
||||
@@ -266,7 +266,7 @@ public sealed class GatewayApplicationTests
|
||||
{
|
||||
// Bind an ephemeral port (:0) — xUnit runs test collections in parallel, so any
|
||||
// WebApplication-building test must avoid a fixed port to prevent a bind collision. The auth
|
||||
// store path is isolated by TestHostEnvironmentInitializer (SEC-33), so startup opens a
|
||||
// store path is isolated by TestHostEnvironmentInitializer, so startup opens a
|
||||
// writable store and the injected misconfiguration is what fails validation.
|
||||
await using WebApplication app = GatewayApplication.Build(
|
||||
[$"--{key}={value}", "--urls=http://127.0.0.1:0"]);
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end reconnect/replay tests through the real gRPC <c>StreamEvents</c> path via the
|
||||
/// fake worker harness (TST-01, server half). A single subscriber detaches (cancel + await
|
||||
/// fake worker harness (server half). A single subscriber detaches (cancel + await
|
||||
/// the stream task so its lease is fully disposed), the session is retained by detach-grace
|
||||
/// while the distributor pump keeps appending worker events to the replay ring, and a second
|
||||
/// stream reconnects with <see cref="StreamEventsRequest.AfterWorkerSequence"/>. Covers both
|
||||
@@ -233,7 +233,7 @@ public sealed class GatewayEndToEndReconnectReplayTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-25 regression, end to end: when age eviction has emptied the replay ring, the
|
||||
/// Regression, end to end: when age eviction has emptied the replay ring, the
|
||||
/// <c>ReplayGap</c> sentinel carries the next deliverable sequence (highest observed + 1),
|
||||
/// so a client applying the documented
|
||||
/// <c>after_worker_sequence = oldest_available_sequence - 1</c> formula on its follow-up
|
||||
@@ -305,7 +305,7 @@ public sealed class GatewayEndToEndReconnectReplayTests
|
||||
Assert.NotNull(sentinel.ReplayGap);
|
||||
Assert.Equal(staleCursor, sentinel.ReplayGap.RequestedAfterSequence);
|
||||
|
||||
// The whole point of GWC-25: the sentinel anchors the resume at the next sequence that
|
||||
// The whole point of the fix: the sentinel anchors the resume at the next sequence that
|
||||
// can still be delivered, not at 0.
|
||||
Assert.Equal(highestSeen + 1, sentinel.ReplayGap.OldestAvailableSequence);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class GalaxyRepositoryHostWiringTests
|
||||
/// Verifies the full production authz-filtering chain end-to-end.
|
||||
/// Phase 1: an identity carrying no <c>BrowseSubtrees</c> pushed → <c>BrowseChildren</c>
|
||||
/// returns both children (unconstrained). That, not an absent identity, is production's
|
||||
/// unscoped case since GWC-38 — with <c>Authentication:Mode=ApiKey</c> an absent identity
|
||||
/// unscoped case — with <c>Authentication:Mode=ApiKey</c> an absent identity
|
||||
/// now denies, which the isolated provider test pins.
|
||||
/// Phase 2: identity with <c>BrowseSubtrees = ["NonExistent"]</c>
|
||||
/// pushed via <see cref="GatewayRequestIdentityAccessor"/> → the real
|
||||
|
||||
@@ -52,7 +52,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies OpenSession forwards the calling key's dashboard-visibility tags, so the
|
||||
/// session's tags are derived from the owning API key rather than the wire request (SEC-25).
|
||||
/// session's tags are derived from the owning API key rather than the wire request.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class MxAccessGrpcMapperTests
|
||||
|
||||
/// <summary>
|
||||
/// The command-only overload exists so <c>Invoke</c> does not deep-clone the whole request just to
|
||||
/// overwrite and discard its command (GWC-29). It must still perform the one clone that keeps the
|
||||
/// overwrite and discard its command. It must still perform the one clone that keeps the
|
||||
/// worker-bound graph unaliased from the caller-owned gRPC command, and must produce the same
|
||||
/// <see cref="WorkerCommand"/> as the request overload.
|
||||
/// </summary>
|
||||
@@ -145,7 +145,7 @@ public sealed class MxAccessGrpcMapperTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies MapEvent transfers ownership of the inner MxEvent (GWC-07 / IPC-05): the
|
||||
/// Verifies MapEvent transfers ownership of the inner MxEvent: the
|
||||
/// returned reference is the same instance carried by the WorkerEvent, not a clone. The
|
||||
/// WorkerEvent is discarded after mapping and the distributor pump is its single consumer,
|
||||
/// so moving the inner event out is safe and avoids a per-event deep copy.
|
||||
|
||||
@@ -669,7 +669,7 @@ public sealed class GatewaySessionTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-27: <see cref="GatewaySession.AttachInternalEventSubscriber"/> must refuse to
|
||||
/// <see cref="GatewaySession.AttachInternalEventSubscriber"/> must refuse to
|
||||
/// attach before the session is Ready. Without the gate the attach would construct and
|
||||
/// start the distributor against a not-yet-Ready worker; the pump source throws
|
||||
/// <c>SessionNotReady</c>, every subscriber is completed with that error, and the
|
||||
@@ -720,7 +720,7 @@ public sealed class GatewaySessionTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-34, the disposal-side twin of the GWC-27 premature-attach poison: an attach that
|
||||
/// The disposal-side twin of the premature-attach poison: an attach that
|
||||
/// passed its readiness gate and then lost the CPU until the session was torn down used to
|
||||
/// find <c>_eventDistributor</c> null, read that as "not created yet" and build a
|
||||
/// REPLACEMENT distributor. Its pump could never start (the started flag is a one-way latch)
|
||||
|
||||
@@ -843,7 +843,7 @@ public sealed class SessionEventDistributorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-25: when age eviction has emptied the replay ring, a resume behind the highest
|
||||
/// When age eviction has emptied the replay ring, a resume behind the highest
|
||||
/// observed sequence still reports a gap, and the reported oldest-available sequence is
|
||||
/// the next sequence that can possibly be delivered (highest seen + 1) — never <c>0</c>,
|
||||
/// which would make the client's documented <c>oldest - 1</c> resume formula wrap to
|
||||
@@ -889,7 +889,7 @@ public sealed class SessionEventDistributorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-25: the same next-deliverable-sequence rule applies when retention is disabled
|
||||
/// The same next-deliverable-sequence rule applies when retention is disabled
|
||||
/// outright (replay capacity 0) — the ring is always empty, so a behind cursor must still
|
||||
/// get a usable resume anchor rather than <c>0</c>.
|
||||
/// </summary>
|
||||
@@ -924,7 +924,7 @@ public sealed class SessionEventDistributorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-25 regression: a client that applies the documented
|
||||
/// Regression: a client that applies the documented
|
||||
/// <c>after_worker_sequence = oldest_available_sequence - 1</c> formula to an empty-ring
|
||||
/// ReplayGap resumes with a live watermark equal to the highest observed sequence, so the
|
||||
/// caller's live filter passes every subsequent event. Under the pre-fix <c>0</c> the
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class SessionManagerTests
|
||||
/// Verifies the pipe name stays short enough that its Unix-domain-socket path
|
||||
/// (TMPDIR + "CoreFxPipe_" + name) fits the 104-byte macOS sun_path limit under the
|
||||
/// default per-user TMPDIR (~49 chars), and keeps the pid + session-guid uniqueness
|
||||
/// contract (NEXT-01).
|
||||
/// contract.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
@@ -110,7 +110,7 @@ public sealed class SessionManagerTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a session inherits the owning API key's dashboard-visibility tags (SEC-25),
|
||||
/// Verifies a session inherits the owning API key's dashboard-visibility tags,
|
||||
/// compared ordinal-ignore-case so a differently cased grant still matches.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
|
||||
+4
-1
@@ -24,7 +24,10 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncLifetime
|
||||
|
||||
private readonly List<IWorkerTaskLauncher> _launchers = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Completes immediately; no asynchronous setup is required before each test.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -36,10 +36,10 @@ public sealed class WorkerClientTests
|
||||
|
||||
/// <summary>
|
||||
/// The <c>GatewayHello</c> carries the negotiated worker-frame maximum so the worker adopts the
|
||||
/// configured limit instead of its own default (IPC-02); a regression to sending <c>0</c> means
|
||||
/// configured limit instead of its own default; a regression to sending <c>0</c> means
|
||||
/// "older gateway, use default" to the worker and would silently downgrade the negotiated limit.
|
||||
/// The adoption half is asserted only in the Windows-only worker suite, so the gateway half is
|
||||
/// pinned here, in the portable suite (TST-28). Both the default and an override are covered so
|
||||
/// pinned here, in the portable suite. Both the default and an override are covered so
|
||||
/// the assertion tracks configuration rather than a constant.
|
||||
/// </summary>
|
||||
/// <param name="maxMessageBytes">Configured worker-frame maximum to negotiate.</param>
|
||||
@@ -164,7 +164,7 @@ public sealed class WorkerClientTests
|
||||
TestTimeout,
|
||||
CancellationToken.None);
|
||||
|
||||
// The timeout also emits a WorkerCancel for the abandoned correlation (PERF-07), which sits
|
||||
// The timeout also emits a WorkerCancel for the abandoned correlation, which sits
|
||||
// ahead of the second command on the FIFO pipe; skip it rather than mistaking it for the
|
||||
// command this assertion is about.
|
||||
WorkerEnvelope secondCommand = await ReadNextCommandAsync(pipePair, timedOutCommand.CorrelationId);
|
||||
@@ -180,7 +180,7 @@ public sealed class WorkerClientTests
|
||||
/// <summary>
|
||||
/// A command timeout abandons the gateway-side wait, but the worker keeps the correlation on
|
||||
/// its single STA queue and would still run it — so the gateway forwards a <c>WorkerCancel</c>
|
||||
/// for the abandoned correlation id (PERF-07). Without it, a client that retries after a
|
||||
/// for the abandoned correlation id. Without it, a client that retries after a
|
||||
/// timeout stacks work the worker still intends to execute. Asserted on the wire because the
|
||||
/// cancel is protocol behavior the worker depends on, not an internal detail.
|
||||
/// </summary>
|
||||
@@ -223,7 +223,7 @@ public sealed class WorkerClientTests
|
||||
/// <summary>
|
||||
/// Caller cancellation abandons the correlation exactly as a gateway timeout does — a client
|
||||
/// deadline shorter than the gateway's command timeout, or a disconnect, is the common
|
||||
/// abandonment path — so it forwards the same <c>WorkerCancel</c> (IPC-34). Without it a
|
||||
/// abandonment path — so it forwards the same <c>WorkerCancel</c>. Without it a
|
||||
/// retry storm of deadline-cancelled calls leaves the worker's single STA working through
|
||||
/// commands nobody is waiting for while live callers queue behind them. Twin of
|
||||
/// <see cref="InvokeAsync_WhenCommandTimesOut_SendsWorkerCancelForThatCorrelation"/>, driven
|
||||
@@ -271,7 +271,7 @@ public sealed class WorkerClientTests
|
||||
|
||||
/// <summary>
|
||||
/// The cancel is forwarded for a caller that abandoned its command, never for the client's own
|
||||
/// teardown (IPC-34): once the stop token has fired the outbound channel is being completed and
|
||||
/// teardown: once the stop token has fired the outbound channel is being completed and
|
||||
/// every pending command is failed anyway, so a cancel would at best be pointless and at worst
|
||||
/// a write racing teardown. Asserted on the wire — the guard in the cancellation arm is the
|
||||
/// code-level expression of it, and the completed outbound channel is the second line of
|
||||
@@ -330,8 +330,7 @@ public sealed class WorkerClientTests
|
||||
/// A <c>WorkerFault</c> arriving in place of a handshake frame is the worker reporting a
|
||||
/// structured startup failure — most often MXAccess COM creation — not a protocol problem, so
|
||||
/// the gateway surfaces <see cref="WorkerClientErrorCode.WorkerFaulted"/> with the fault's
|
||||
/// category and diagnostic rather than a generic protocol violation naming only the body type
|
||||
/// (IPC-35). This is the window where the information matters most: first session open on a
|
||||
/// category and diagnostic rather than a generic protocol violation naming only the body type. This is the window where the information matters most: first session open on a
|
||||
/// freshly deployed host, where the message reaches the operator log and the client's
|
||||
/// <c>OpenFailed</c> detail.
|
||||
/// </summary>
|
||||
@@ -368,8 +367,7 @@ public sealed class WorkerClientTests
|
||||
/// <summary>
|
||||
/// The envelope <c>sequence</c> is a monotonic per-sender counter (gateway.md), so the values
|
||||
/// observed on the pipe must be strictly increasing in wire order. Stamping the sequence when
|
||||
/// the envelope is constructed lets two concurrent invokes stamp 1 and 2 but enqueue 2 then 1
|
||||
/// (GWC-28); stamping on the single-consumer write loop makes wire order and sequence order the
|
||||
/// the envelope is constructed lets two concurrent invokes stamp 1 and 2 but enqueue 2 then 1; stamping on the single-consumer write loop makes wire order and sequence order the
|
||||
/// same thing by construction.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
@@ -710,12 +708,12 @@ public sealed class WorkerClientTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-32: disposing a still-<c>Ready</c> client while the worker is emitting events must be
|
||||
/// Disposing a still-<c>Ready</c> client while the worker is emitting events must be
|
||||
/// an ordinary teardown, not a fault. <c>DisposeAsync</c> completes the same three channels
|
||||
/// <c>MarkClosed</c> and <c>SetFaulted</c> do, so it must enter a terminal state first;
|
||||
/// otherwise the read loop — already inside <c>DispatchEnvelope</c> when the staging channel
|
||||
/// is completed, or reading a stream that is torn down underneath it — misdiagnoses the
|
||||
/// completed channel as the GWC-24 sustained-backpressure overflow and raises a spurious
|
||||
/// completed channel as the sustained-backpressure overflow and raises a spurious
|
||||
/// <c>ProtocolViolation</c> fault, a <c>worker-event-staging</c> overflow metric and a
|
||||
/// redundant kill during an intentional dispose. The event channel is sized far above the
|
||||
/// burst so a genuine overflow is impossible, and <c>DisposeAsync</c> awaits its background
|
||||
@@ -726,7 +724,7 @@ public sealed class WorkerClientTests
|
||||
public async Task DisposeAsync_WhileWorkerIsEmittingEvents_ClosesWithoutFaultingOrOverflowing()
|
||||
{
|
||||
// The burst is capped at EmittedEventCeiling and the staging channel holds 2 x capacity, so
|
||||
// even if every emitted event were staged and none consumed the GWC-24 bound is unreachable:
|
||||
// even if every emitted event were staged and none consumed the staging bound is unreachable:
|
||||
// an overflow metric in this test can only be the dispose-race misdiagnosis.
|
||||
const int capacity = 2048;
|
||||
using GatewayMetrics metrics = new();
|
||||
@@ -1007,13 +1005,13 @@ public sealed class WorkerClientTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-24: the staging channel between the read loop and the event writer is bounded at
|
||||
/// The staging channel between the read loop and the event writer is bounded at
|
||||
/// 2 × <see cref="WorkerClientOptions.EventChannelCapacity"/>, so a consumer that drains
|
||||
/// slower than the worker produces faults the client at the bound instead of growing
|
||||
/// gateway memory silently. The full-mode timeout here is far longer than the test could
|
||||
/// ever wait, which proves the fault came from the staging bound and not from the timed
|
||||
/// write in <c>EnqueueWorkerEventAsync</c>. A command reply interleaved before the fault
|
||||
/// must still complete: the read loop never blocks behind events (the GWC-04 guarantee).
|
||||
/// must still complete: the read loop never blocks behind events (the read-loop guarantee).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
@@ -1094,7 +1092,7 @@ public sealed class WorkerClientTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-24: the worker event queue-depth gauge counts staged *and* queued events, so a
|
||||
/// The worker event queue-depth gauge counts staged *and* queued events, so a
|
||||
/// backlog held in the staging channel is visible rather than invisible. Depth is
|
||||
/// incremented at staging and decremented when the consumer reads, so the single counter
|
||||
/// reports total undelivered events and returns to zero once drained.
|
||||
@@ -1148,7 +1146,7 @@ public sealed class WorkerClientTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-33: a faulted client can remain retained for diagnostics, but its abandoned event
|
||||
/// A faulted client can remain retained for diagnostics, but its abandoned event
|
||||
/// backlog must stop contributing to the gateway-wide queue-depth gauge as soon as the
|
||||
/// client enters a terminal state rather than waiting for disposal.
|
||||
/// </summary>
|
||||
|
||||
@@ -57,7 +57,7 @@ public sealed class WorkerFrameProtocolTests
|
||||
|
||||
/// <summary>
|
||||
/// One reader instance reads many frames in sequence. The reader reuses a single length-prefix
|
||||
/// scratch buffer across calls (GWC-30), so varying the payload length frame to frame proves the
|
||||
/// scratch buffer across calls, so varying the payload length frame to frame proves the
|
||||
/// reused buffer is fully overwritten each time rather than carrying a stale prefix forward.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
|
||||
@@ -19,11 +19,11 @@ public sealed class GatewayMetricsTests
|
||||
metrics.CommandFailed("WriteSecured", "AuthorizationFailed", TimeSpan.FromMilliseconds(12));
|
||||
metrics.EventReceived("session-1", "OnDataChange");
|
||||
metrics.EventReceived("session-1", "OnDataChange");
|
||||
// PERF-02: the worker queue-depth gauge sums one live source per worker client, so the two
|
||||
// The worker queue-depth gauge sums one live source per worker client, so the two
|
||||
// registrations below stand in for two concurrent sessions holding 3 and 4 events.
|
||||
using IDisposable workerDepthSourceA = metrics.RegisterWorkerEventQueueDepthSource(static () => 3);
|
||||
using IDisposable workerDepthSourceB = metrics.RegisterWorkerEventQueueDepthSource(static () => 4);
|
||||
// GWC-15: the gRPC stream queue-depth gauge sums live backlog sources at collection time
|
||||
// The gRPC stream queue-depth gauge sums live backlog sources at collection time
|
||||
// rather than tracking a pushed running total. Register a source reporting 3.
|
||||
using IDisposable backlogSource = metrics.RegisterEventStreamBacklogSource(static () => 3);
|
||||
metrics.QueueOverflow("session-events");
|
||||
@@ -58,7 +58,7 @@ public sealed class GatewayMetricsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PERF-02: the worker queue-depth gauge sums every registered source rather than holding a
|
||||
/// The worker queue-depth gauge sums every registered source rather than holding a
|
||||
/// single pushed scalar, so concurrent sessions add up instead of overwriting one another,
|
||||
/// and a disposed registration (a worker client going away) drops out of the sum. Disposal
|
||||
/// is idempotent because a client's dispose path can run twice.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.ProjectStructure;
|
||||
|
||||
/// <summary>Pins the structure of the windev worker CI script.</summary>
|
||||
public sealed class WindevCiContractTests
|
||||
{
|
||||
/// <summary>Verifies live mode runs the Windows test tiers in the documented order.</summary>
|
||||
[Fact]
|
||||
public void LiveMode_RunsWindowsTiersInDocumentedOrder()
|
||||
{
|
||||
|
||||
+1
@@ -238,6 +238,7 @@ public sealed class ApiKeyAdminCommandLineParserTests
|
||||
/// Verifies an empty tag segment is rejected rather than dropped: a stray comma must not
|
||||
/// silently persist a grant the operator did not write.
|
||||
/// </summary>
|
||||
/// <param name="tags">The raw dashboard tag list containing an empty segment.</param>
|
||||
[Theory]
|
||||
[InlineData("team-a,,team-b")]
|
||||
[InlineData("team-a, ")]
|
||||
|
||||
@@ -100,7 +100,7 @@ public sealed class CachingApiKeyVerifierTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-34 window 3: an <see cref="IApiKeyCacheInvalidator.Invalidate"/> that lands while a
|
||||
/// Window 3: an <see cref="IApiKeyCacheInvalidator.Invalidate"/> that lands while a
|
||||
/// verification is in flight in the inner library must discard that verification's repopulation,
|
||||
/// so the very next request re-verifies (revoke takes effect immediately, not after the TTL).
|
||||
/// </summary>
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the two-layer API-key failure limiter (SEC-31 / SEC-32): composite
|
||||
/// Unit tests for the two-layer API-key failure limiter: composite
|
||||
/// <c>(transport peer, key id)</c> partitions, the cross-peer per-key-id aggregate, probe
|
||||
/// admission, the per-peer key-id partition cap, and the eviction preference order.
|
||||
/// </summary>
|
||||
@@ -58,7 +58,7 @@ public sealed class ApiKeyFailureLimiterTests
|
||||
|
||||
/// <summary>
|
||||
/// The composite partition binds a throttle to the failing address: the same key id presented
|
||||
/// from a different transport peer is unaffected. This is the structural half of the SEC-31 fix.
|
||||
/// from a different transport peer is unaffected. This is the structural half of the per-address throttle.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompositePartition_ThrottleDoesNotFollowKeyIdToAnotherPeer()
|
||||
@@ -102,7 +102,7 @@ public sealed class ApiKeyFailureLimiterTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-38: a valid key id still feeds and consults its cross-peer aggregate when each
|
||||
/// A valid key id still feeds and consults its cross-peer aggregate when each
|
||||
/// attacking peer has exhausted its 32 dedicated key-id partitions and therefore records
|
||||
/// the peer layer through the fallback bucket.
|
||||
/// </summary>
|
||||
@@ -372,7 +372,7 @@ public sealed class ApiKeyFailureLimiterTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-39: a request with no effective key id does not own the peer's shared fallback
|
||||
/// A request with no effective key id does not own the peer's shared fallback
|
||||
/// partition. Resetting after such a request must leave other fallback failures tracked.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -395,7 +395,7 @@ public sealed class ApiKeyFailureLimiterTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-32: a spray of unique junk partitions (junk tokens resolve to the sender's fallback
|
||||
/// A spray of unique junk partitions (junk tokens resolve to the sender's fallback
|
||||
/// partition, one per address) must not evict a partition that is currently throttled — the
|
||||
/// LRU cap bounds memory, it must not be a reset button for the block.
|
||||
/// </summary>
|
||||
@@ -420,7 +420,7 @@ public sealed class ApiKeyFailureLimiterTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-32: one address may mint at most <see cref="ApiKeyFailureLimiter.MaxKeyIdPartitionsPerPeer"/>
|
||||
/// One address may mint at most <see cref="ApiKeyFailureLimiter.MaxKeyIdPartitionsPerPeer"/>
|
||||
/// key-id partitions; the overflow collapses into that address's fallback partition, which then
|
||||
/// throttles the address wholesale instead of letting the spray mint unbounded state.
|
||||
/// </summary>
|
||||
@@ -445,7 +445,7 @@ public sealed class ApiKeyFailureLimiterTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-32 eviction preference: with the map at capacity a new failure evicts an entry whose
|
||||
/// Eviction preference: with the map at capacity a new failure evicts an entry whose
|
||||
/// window has fully expired rather than an entry that is still counting.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
|
||||
+3
-3
@@ -32,8 +32,8 @@ public sealed class GatewayBrowseScopeProviderTests
|
||||
|
||||
/// <summary>
|
||||
/// An authenticated key with no BrowseSubtrees constraint is legitimately unscoped: the
|
||||
/// empty list is what the library reads as "no scoping". This is the state GWC-38 required
|
||||
/// to stay distinguishable from an absent identity, so it is asserted alongside its
|
||||
/// empty list is what the library reads as "no scoping". This is the state that must
|
||||
/// stay distinguishable from an absent identity, so it is asserted alongside its
|
||||
/// denying twin below rather than left implicit.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -49,7 +49,7 @@ public sealed class GatewayBrowseScopeProviderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GWC-38: with API-key authentication enabled, an absent ambient identity means the
|
||||
/// With API-key authentication enabled, an absent ambient identity means the
|
||||
/// interceptor-ordering invariant is broken — the request's browse scope cannot be
|
||||
/// established. The provider must DENY rather than resolve to the full hierarchy, because
|
||||
/// a silently-unscoped browse is an invisible authorization bypass. The status code is
|
||||
|
||||
+5
-5
@@ -365,7 +365,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-31: once an attacking peer has exceeded the failure limit for a key id, the interceptor
|
||||
/// Once an attacking peer has exceeded the failure limit for a key id, the interceptor
|
||||
/// short-circuits with <see cref="StatusCode.ResourceExhausted"/> BEFORE calling the verifier, so
|
||||
/// an online guessing loop stops spending a store read per attempt. The composite
|
||||
/// <c>(peer, key id)</c> partition keeps that bound per attacking address.
|
||||
@@ -407,7 +407,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-31 (the lockout inversion): an attacker who floods failures for a victim's key id from its
|
||||
/// Lockout inversion: an attacker who floods failures for a victim's key id from its
|
||||
/// own address must not deny that key to the legitimate holder. The holder presents the correct
|
||||
/// secret from a different transport peer and authenticates on the first attempt — the verifier
|
||||
/// is reached and the RPC succeeds.
|
||||
@@ -454,7 +454,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-31 layer 2: failures for one key id sprayed across more distinct peers than
|
||||
/// Layer 2: failures for one key id sprayed across more distinct peers than
|
||||
/// <c>ApiKeyFailureAggregateLimit</c> put that key id into probe mode globally, so a
|
||||
/// rotating-source attacker gets at most one verifier call per probe interval.
|
||||
/// </summary>
|
||||
@@ -515,7 +515,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-31: the success-reset path stays reachable while throttled. A throttled partition admits
|
||||
/// The success-reset path stays reachable while throttled. A throttled partition admits
|
||||
/// one probe per interval; the correct secret rides that slot, authenticates, and fully clears
|
||||
/// both limiter layers, so the next wrong attempt is <see cref="StatusCode.Unauthenticated"/>
|
||||
/// rather than <see cref="StatusCode.ResourceExhausted"/>.
|
||||
@@ -572,7 +572,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SEC-32: only a validly shaped <c>mxgw_<keyId>_<secret></c> token mints a key-id
|
||||
/// Only a validly shaped <c>mxgw_<keyId>_<secret></c> token mints a key-id
|
||||
/// partition. Junk tokens of varied shapes all collapse onto the sender's transport-peer fallback
|
||||
/// partition, so a spray cannot mint one tracked entry per invented token.
|
||||
/// </summary>
|
||||
|
||||
@@ -61,6 +61,7 @@ public sealed class ChannelWorkerClient(string sessionId, ChannelReader<WorkerEv
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Completes immediately because the fake holds no resources.</summary>
|
||||
/// <returns>A completed value task.</returns>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ internal static class TestHostEnvironmentInitializer
|
||||
Environment.SetEnvironmentVariable("Secrets__SqlitePath", secretsPath);
|
||||
}
|
||||
|
||||
// Starting the full host eagerly opens the auth SQLite store. Since SEC-33 the shipped
|
||||
// Starting the full host eagerly opens the auth SQLite store. The shipped
|
||||
// appsettings.json no longer carries an Authentication:SqlitePath, and the CommonApplicationData
|
||||
// code default resolves under an unwritable /usr/share on macOS. Point every host-building test at
|
||||
// a per-process temp store (same pattern as Secrets__SqlitePath above) so host-start tests are
|
||||
|
||||
Reference in New Issue
Block a user