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:
@@ -8,7 +8,7 @@ using ZB.MOM.WW.MxGateway.Tests.Security.Authentication;
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Security.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Tests the Task 2.3 canonical audit plumbing: the gateway-owned
|
||||
/// Tests the canonical audit plumbing: the gateway-owned
|
||||
/// <see cref="SqliteCanonicalAuditStore"/> (round-trips canonical
|
||||
/// <see cref="AuditEvent"/>s through the new <c>audit_event</c> table), the best-effort
|
||||
/// <see cref="CanonicalAuditWriter"/>, and the <see cref="CanonicalForwardingApiKeyAuditStore"/>
|
||||
@@ -21,6 +21,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
private readonly List<TempDatabaseDirectory> _tempDirectories = [];
|
||||
|
||||
/// <summary>A canonical event with all fields populated round-trips through the store.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Store_InsertThenListRecent_RoundTripsAllFields()
|
||||
{
|
||||
@@ -61,6 +62,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Nullable canonical fields round-trip as null, not empty string.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Store_InsertWithNullOptionalFields_RoundTripsAsNull()
|
||||
{
|
||||
@@ -86,6 +88,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>ListRecent returns newest-first.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Store_ListRecent_ReturnsNewestFirst()
|
||||
{
|
||||
@@ -100,6 +103,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>The writer is best-effort: a faulting store does not surface to the caller.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Writer_WhenStoreFails_DoesNotThrow()
|
||||
{
|
||||
@@ -123,6 +127,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
/// A library event with a KeyId maps to a canonical Success event under category ApiKey,
|
||||
/// and the adapter maps it back to the original entry for the dashboard view.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Adapter_KeyedEvent_RoundTripsThroughCanonicalStore()
|
||||
{
|
||||
@@ -158,6 +163,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>The keyless library <c>init-db</c> event maps to Actor "system" and back to a null KeyId.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Adapter_InitDbKeylessEvent_MapsToSystemActor()
|
||||
{
|
||||
@@ -184,6 +190,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Any other keyless library event maps to Actor "cli" and back to a null KeyId.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Adapter_OtherKeylessEvent_MapsToCliActor()
|
||||
{
|
||||
@@ -207,6 +214,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>A <c>constraint-denied</c> library event maps to Outcome.Denied.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Adapter_ConstraintDeniedEvent_MapsToDeniedOutcome()
|
||||
{
|
||||
@@ -234,6 +242,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
|
||||
/// The adapter does NOT throw when the underlying write fails (it forwards through the
|
||||
/// best-effort writer), preserving the IApiKeyAuditStore caller's flow.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Adapter_WhenWriterFails_DoesNotThrow()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
|
||||
{
|
||||
private readonly List<TempDatabaseDirectory> _tempDirectories = [];
|
||||
/// <summary>Verifies that CreateKeyAsync creates an authenticating key and audits the action.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateKeyAsync_CreatesAuthenticatingKeyAndAudits()
|
||||
{
|
||||
@@ -52,6 +53,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ListKeysAsync does not print the raw secret.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ListKeysAsync_DoesNotPrintRawSecret()
|
||||
{
|
||||
@@ -82,6 +84,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RevokeKeyAsync causes the revoked key to fail verification and is audited.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RevokeKeyAsync_RevokedKeyFailsVerificationAndAudits()
|
||||
{
|
||||
@@ -117,6 +120,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RotateKeyAsync prints the new secret once and invalidates the old secret.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RotateKeyAsync_PrintsNewSecretOnceAndInvalidatesOldSecret()
|
||||
{
|
||||
@@ -154,6 +158,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that CreateKeyAsync prints the raw secret exactly once.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateKeyAsync_PrintsRawSecretExactlyOnce()
|
||||
{
|
||||
@@ -182,6 +187,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Verifies that API key constraints are persisted correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateKeyAsync_WithConstraints_PersistsConstraints()
|
||||
{
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ public sealed class ApiKeyAdminCommandLineParserTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-004 regression: a create-key command with a non-canonical scope
|
||||
/// A create-key command with a non-canonical scope
|
||||
/// string (e.g. CLAUDE.md's stale <c>invoke</c> instead of <c>invoke:read</c>)
|
||||
/// must be rejected at parse time rather than silently persisting an
|
||||
/// unusable scope the authorization resolver never matches.
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed class ApiKeyVerifierTests
|
||||
private static readonly ApiKeyOptions Options = new() { TokenPrefix = "mxgw" };
|
||||
|
||||
/// <summary>Verifies that VerifyAsync returns identity and scopes for a valid key.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidKey_ReturnsIdentityAndScopes()
|
||||
{
|
||||
@@ -38,6 +39,7 @@ public sealed class ApiKeyVerifierTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that VerifyAsync does not expose the raw secret in the result.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidKey_DoesNotExposeRawSecretInResult()
|
||||
{
|
||||
@@ -55,6 +57,7 @@ public sealed class ApiKeyVerifierTests
|
||||
|
||||
/// <summary>Verifies that VerifyAsync fails as missing/malformed for a malformed key.</summary>
|
||||
/// <param name="authorizationHeader">Authorization header value to test.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("Bearer mxgw_operator01")]
|
||||
@@ -72,6 +75,7 @@ public sealed class ApiKeyVerifierTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that VerifyAsync fails for an unknown key.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task VerifyAsync_UnknownKey_Fails()
|
||||
{
|
||||
@@ -86,6 +90,7 @@ public sealed class ApiKeyVerifierTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that VerifyAsync fails for a wrong secret (constant-time compare rejects it).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WrongSecret_Fails()
|
||||
{
|
||||
@@ -102,6 +107,7 @@ public sealed class ApiKeyVerifierTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that VerifyAsync fails for a revoked key.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task VerifyAsync_RevokedKey_Fails()
|
||||
{
|
||||
@@ -118,6 +124,7 @@ public sealed class ApiKeyVerifierTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that VerifyAsync fails closed when the pepper is missing.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task VerifyAsync_MissingPepper_Fails()
|
||||
{
|
||||
@@ -160,6 +167,7 @@ public sealed class ApiKeyVerifierTests
|
||||
private sealed class FakePepperProvider(string? pepper) : IApiKeyPepperProvider
|
||||
{
|
||||
/// <summary>Returns the configured pepper (or null to simulate an unavailable pepper).</summary>
|
||||
/// <returns>The configured pepper, or null to simulate an unavailable pepper.</returns>
|
||||
public string? GetPepper() => pepper;
|
||||
}
|
||||
|
||||
@@ -169,13 +177,19 @@ public sealed class ApiKeyVerifierTests
|
||||
/// <summary>Gets whether the key was marked as used.</summary>
|
||||
public bool MarkedUsed { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Returns the fake's single stored key if its ID matches, regardless of revocation.</summary>
|
||||
/// <param name="keyId">Key ID to look up.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The matching stored key record, or null if no key matches.</returns>
|
||||
public Task<ApiKeyRecord?> FindByKeyIdAsync(string keyId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(storedKey?.KeyId == keyId ? storedKey : null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Returns the fake's single stored key if its ID matches and it has not been revoked.</summary>
|
||||
/// <param name="keyId">Key ID to look up.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The matching, non-revoked stored key record, or null otherwise.</returns>
|
||||
public Task<ApiKeyRecord?> FindActiveByKeyIdAsync(string keyId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(
|
||||
@@ -184,7 +198,11 @@ public sealed class ApiKeyVerifierTests
|
||||
: null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Records that the stored key was used, exposed via <see cref="MarkedUsed"/> for assertions.</summary>
|
||||
/// <param name="keyId">Key ID that was used.</param>
|
||||
/// <param name="whenUtc">Timestamp of use (unused by the fake).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task MarkUsedAsync(string keyId, DateTimeOffset whenUtc, CancellationToken ct)
|
||||
{
|
||||
MarkedUsed = storedKey?.KeyId == keyId;
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that MigrateAsync initializes the database schema at the donor's version (2).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MigrateAsync_EmptyDatabase_InitializesCurrentSchema()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that MigrateAsync migrates and is idempotent.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MigrateAsync_ExistingVersionZeroDatabase_MigratesIdempotently()
|
||||
{
|
||||
@@ -61,6 +63,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that gateway startup fails with a newer schema version.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartAsync_NewerSchemaVersion_BlocksStartup()
|
||||
{
|
||||
@@ -83,6 +86,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
|
||||
/// Verifies that FindActiveByKeyIdAsync returns an active key, reading a row whose columns match
|
||||
/// the donor schema (peppered secret_hash BLOB, ordinal-sorted scopes JSON).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FindActiveByKeyIdAsync_ExistingActiveKey_ReturnsKey()
|
||||
{
|
||||
@@ -106,6 +110,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that FindActiveByKeyIdAsync returns null for a revoked key.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FindActiveByKeyIdAsync_RevokedKey_ReturnsNull()
|
||||
{
|
||||
@@ -129,6 +134,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
|
||||
/// <summary>
|
||||
/// Verifies that the audit store persists audit events.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ApiKeyAuditStore_AppendAsync_PersistsAuditEvent()
|
||||
{
|
||||
@@ -163,6 +169,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
|
||||
/// the auth database in WAL journal mode so concurrent readers and writers degrade
|
||||
/// gracefully instead of surfacing <c>SQLITE_BUSY</c> on the request path.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OpenConnectionAsync_EnablesWalJournalModeAndBusyTimeout()
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ internal sealed class TempDatabaseDirectory : IDisposable
|
||||
|
||||
/// <summary>Creates a new uniquely named temporary directory under the given prefix.</summary>
|
||||
/// <param name="prefix">Folder name placed under <c>%TEMP%</c> to group related test directories.</param>
|
||||
/// <returns>The created temporary directory handle.</returns>
|
||||
public static TempDatabaseDirectory Create(string prefix)
|
||||
{
|
||||
string path = System.IO.Path.Combine(
|
||||
@@ -35,12 +36,13 @@ internal sealed class TempDatabaseDirectory : IDisposable
|
||||
|
||||
/// <summary>Returns a database file path inside this temporary directory.</summary>
|
||||
/// <param name="fileName">Database file name; defaults to the gateway auth database name.</param>
|
||||
/// <returns>The full path to the database file.</returns>
|
||||
public string DatabasePath(string fileName = "gateway-auth.db")
|
||||
{
|
||||
return System.IO.Path.Combine(Path, fileName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Clears pooled SQLite connections and deletes the temporary directory.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;
|
||||
public sealed class ConstraintEnforcerTests
|
||||
{
|
||||
/// <summary>Verifies that read outside allowed subtree returns failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckReadTagAsync_WhenOutsideReadSubtree_ReturnsFailure()
|
||||
{
|
||||
@@ -35,6 +36,7 @@ public sealed class ConstraintEnforcerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write with high classification returns failure and audits.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckWriteHandleAsync_WhenClassificationTooHigh_ReturnsFailureAndAudits()
|
||||
{
|
||||
@@ -84,6 +86,7 @@ public sealed class ConstraintEnforcerTests
|
||||
}
|
||||
|
||||
/// <summary>A denial carrying a parseable correlation id stores it on the audit record.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RecordDenialAsync_WithGuidCorrelationId_StoresCorrelationId()
|
||||
{
|
||||
@@ -106,6 +109,7 @@ public sealed class ConstraintEnforcerTests
|
||||
/// A denial with a non-GUID correlation id leaves the typed audit correlation id null but
|
||||
/// still preserves the raw client correlation id in DetailsJson so it is not lost.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RecordDenialAsync_WithNonGuidCorrelationId_LeavesCorrelationIdNullButPreservesRawInDetails()
|
||||
{
|
||||
@@ -130,6 +134,7 @@ public sealed class ConstraintEnforcerTests
|
||||
}
|
||||
|
||||
/// <summary>A denial with no identity records the canonical "anonymous" actor.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RecordDenialAsync_WithoutIdentity_UsesAnonymousActor()
|
||||
{
|
||||
@@ -150,6 +155,7 @@ public sealed class ConstraintEnforcerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that historized-only constraint requires historized attribute.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckReadTagAsync_WithHistorizedOnly_RequiresRequestedAttributeToBeHistorized()
|
||||
{
|
||||
@@ -169,6 +175,7 @@ public sealed class ConstraintEnforcerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm-only constraint requires alarm attribute.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckReadTagAsync_WithAlarmOnly_RequiresRequestedAttributeToBeAlarm()
|
||||
{
|
||||
@@ -188,6 +195,7 @@ public sealed class ConstraintEnforcerTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that attribute-only constraint fails closed for object tag.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckReadTagAsync_WithAttributeOnlyConstraint_FailsClosedForObjectTag()
|
||||
{
|
||||
@@ -212,6 +220,7 @@ public sealed class ConstraintEnforcerTests
|
||||
/// Without the <c>[]</c> fallback in ResolveTarget the bare name misses the index and a
|
||||
/// read-constrained key gets a spurious tag_metadata denial for an AddItem it should allow.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckReadTagAsync_WithBareArrayName_ResolvesViaArraySuffixFallback()
|
||||
{
|
||||
@@ -236,6 +245,7 @@ public sealed class ConstraintEnforcerTests
|
||||
/// A bare non-array name that is genuinely absent from the index still resolves to null:
|
||||
/// the <c>[]</c> probe must not manufacture a false positive for a scalar/missing tag.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckReadTagAsync_WithMissingNonArrayName_StillFailsToResolve()
|
||||
{
|
||||
@@ -265,8 +275,9 @@ public sealed class ConstraintEnforcerTests
|
||||
/// <summary>
|
||||
/// The <c>[]</c>-suffix fallback widened *resolution* of a bare array name, not *authorization*:
|
||||
/// a bare array attribute that resolves through the fallback but is outside the key's read scope
|
||||
/// must still be denied with a <c>read_scope</c> failure (Server-058).
|
||||
/// must still be denied with a <c>read_scope</c> failure.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckReadTagAsync_WithBareArrayName_OutOfScope_StillDeniedReadScope()
|
||||
{
|
||||
@@ -289,8 +300,9 @@ public sealed class ConstraintEnforcerTests
|
||||
/// <summary>
|
||||
/// A write against an array handle whose registration carries the suffixed form ("Pump_001.Levels[]")
|
||||
/// resolves through <c>ResolveTarget</c> and is denied with a <c>write_scope</c> failure when the
|
||||
/// array attribute is outside the key's write scope (Server-058).
|
||||
/// array attribute is outside the key's write scope.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckWriteHandleAsync_WithSuffixedArrayRegistration_OutOfScope_StillDeniedWriteScope()
|
||||
{
|
||||
@@ -331,9 +343,9 @@ public sealed class ConstraintEnforcerTests
|
||||
/// <summary>
|
||||
/// A write against an in-scope array handle whose registration carries the suffixed form is still
|
||||
/// denied when the array attribute's <c>SecurityClassification</c> exceeds the key's
|
||||
/// <c>MaxWriteClassification</c>, resolved through <c>ResolveTarget</c> via the suffixed address
|
||||
/// (Server-058).
|
||||
/// <c>MaxWriteClassification</c>, resolved through <c>ResolveTarget</c> via the suffixed address.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CheckWriteHandleAsync_WithSuffixedArrayRegistration_ClassificationTooHigh_StillDenied()
|
||||
{
|
||||
@@ -490,10 +502,14 @@ public sealed class ConstraintEnforcerTests
|
||||
/// <summary>Gets the current cache entry.</summary>
|
||||
public GalaxyHierarchyCacheEntry Current { get; } = current;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>No-op refresh; the stub's <see cref="Current"/> entry never changes.</summary>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>No-op wait; the stub's <see cref="Current"/> entry is already loaded.</summary>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -502,7 +518,10 @@ public sealed class ConstraintEnforcerTests
|
||||
/// <summary>Gets the recorded canonical audit events.</summary>
|
||||
public List<AuditEvent> Events { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Records the audit event in <see cref="Events"/> for later assertions.</summary>
|
||||
/// <param name="auditEvent">The audit event to record.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Events.Add(auditEvent);
|
||||
|
||||
+14
@@ -23,6 +23,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;
|
||||
public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
{
|
||||
/// <summary>Verifies that missing API key returns unauthenticated status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnaryServerHandler_MissingApiKey_ReturnsUnauthenticated()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invalid API key error does not expose raw credentials.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnaryServerHandler_InvalidApiKey_DoesNotExposeRawCredentialInStatus()
|
||||
{
|
||||
@@ -59,6 +61,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that valid key without required scope returns permission denied.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnaryServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
|
||||
{
|
||||
@@ -77,6 +80,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that valid key with scope sets request identity for the handler.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnaryServerHandler_ValidApiKeyWithScope_SetsRequestIdentity()
|
||||
{
|
||||
@@ -103,6 +107,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that server stream handler requires proper scope.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ServerStreamingServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
|
||||
{
|
||||
@@ -122,6 +127,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that server stream handler allows streams with proper scope.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ServerStreamingServerHandler_ValidApiKeyWithScope_AllowsStream()
|
||||
{
|
||||
@@ -147,6 +153,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disabled authentication skips API key verification.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnaryServerHandler_AuthenticationDisabled_SkipsApiKeyVerification()
|
||||
{
|
||||
@@ -173,6 +180,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
/// that lacks the <c>session:open</c> scope, and asserts the interceptor denies the
|
||||
/// call with <see cref="StatusCode.PermissionDenied"/> before the service runs.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InterceptorComposedWithService_OpenSessionMissingScope_DeniesBeforeServiceRuns()
|
||||
{
|
||||
@@ -200,6 +208,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
/// that holds <c>session:open</c>, and asserts the service runs and observes the
|
||||
/// interceptor-supplied identity.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InterceptorComposedWithService_OpenSessionWithScope_RunsServiceWithIdentity()
|
||||
{
|
||||
@@ -226,6 +235,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
/// because the wrapped command is a write, confirming command-scope mapping is
|
||||
/// enforced through the full composition.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InterceptorComposedWithService_InvokeWriteCommandWithReadScope_DeniesBeforeServiceRuns()
|
||||
{
|
||||
@@ -261,6 +271,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
/// <see cref="GatewayScopes.InvokeWrite"/>. Ack is a write-shaped mutation against
|
||||
/// alarm state, so it carries the same scope as <c>MxCommandKind.Write</c>.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnaryServerHandler_AcknowledgeAlarmMissingScope_ReturnsPermissionDenied()
|
||||
{
|
||||
@@ -279,6 +290,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an API key holding <c>invoke:write</c> may call <c>AcknowledgeAlarm</c>.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UnaryServerHandler_AcknowledgeAlarmWithScope_RunsHandler()
|
||||
{
|
||||
@@ -305,6 +317,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
/// lack <see cref="GatewayScopes.EventsRead"/>. Active-alarm snapshots are part of the
|
||||
/// alarm/event surface and share the same scope as <c>StreamEvents</c>.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ServerStreamingServerHandler_QueryActiveAlarmsMissingScope_ReturnsPermissionDenied()
|
||||
{
|
||||
@@ -324,6 +337,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an API key holding <c>events:read</c> may call <c>QueryActiveAlarms</c>.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ServerStreamingServerHandler_QueryActiveAlarmsWithScope_RunsHandler()
|
||||
{
|
||||
|
||||
@@ -11,16 +11,19 @@ public sealed class KestrelTlsInspectorTests
|
||||
.AddInMemoryCollection(entries.ToDictionary(e => e.Key, e => (string?)e.Value))
|
||||
.Build();
|
||||
|
||||
/// <summary>An HTTPS endpoint with no certificate configured requires a generated certificate.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_True_WhenHttpsEndpointHasNoCertificate()
|
||||
=> Assert.True(KestrelTlsInspector.RequiresGeneratedCertificate(
|
||||
Config(("Kestrel:Endpoints:Http:Url", "https://0.0.0.0:5120"))));
|
||||
|
||||
/// <summary>When every configured endpoint is plaintext, no generated certificate is required.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_False_WhenAllEndpointsPlaintext()
|
||||
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
|
||||
Config(("Kestrel:Endpoints:Http:Url", "http://0.0.0.0:5120"))));
|
||||
|
||||
/// <summary>An HTTPS endpoint with its own certificate path configured does not require a generated certificate.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_False_WhenHttpsEndpointHasOwnCertificate()
|
||||
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
|
||||
@@ -28,10 +31,12 @@ public sealed class KestrelTlsInspectorTests
|
||||
("Kestrel:Endpoints:Http:Url", "https://0.0.0.0:5120"),
|
||||
("Kestrel:Endpoints:Http:Certificate:Path", @"C:\certs\real.pfx"))));
|
||||
|
||||
/// <summary>With no endpoints configured at all, no generated certificate is required.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_False_WhenNoEndpointsConfigured()
|
||||
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(Config()));
|
||||
|
||||
/// <summary>An HTTPS endpoint configured with only a certificate thumbprint does not require a generated certificate.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_False_WhenHttpsEndpointHasThumbprintOnly()
|
||||
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
|
||||
@@ -39,6 +44,7 @@ public sealed class KestrelTlsInspectorTests
|
||||
("Kestrel:Endpoints:Https:Url", "https://0.0.0.0:5120"),
|
||||
("Kestrel:Endpoints:Https:Certificate:Thumbprint", "AABBCCDDEEFF00112233445566778899AABBCCDD"))));
|
||||
|
||||
/// <summary>An HTTPS endpoint configured with only a certificate subject does not require a generated certificate.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_False_WhenHttpsEndpointHasSubjectOnly()
|
||||
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
|
||||
@@ -46,11 +52,13 @@ public sealed class KestrelTlsInspectorTests
|
||||
("Kestrel:Endpoints:Https:Url", "https://0.0.0.0:5120"),
|
||||
("Kestrel:Endpoints:Https:Certificate:Subject", "CN=myserver"))));
|
||||
|
||||
/// <summary>An uppercase HTTPS URL scheme is still recognized as HTTPS and requires a generated certificate.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_True_WhenHttpsUrlIsUppercase()
|
||||
=> Assert.True(KestrelTlsInspector.RequiresGeneratedCertificate(
|
||||
Config(("Kestrel:Endpoints:Https:Url", "HTTPS://0.0.0.0:5120"))));
|
||||
|
||||
/// <summary>An HTTPS endpoint covered by the Kestrel default certificate does not require a generated certificate.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_False_WhenKestrelDefaultCertificateConfigured()
|
||||
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
|
||||
@@ -58,6 +66,7 @@ public sealed class KestrelTlsInspectorTests
|
||||
("Kestrel:Endpoints:Https:Url", "https://0.0.0.0:5120"),
|
||||
("Kestrel:Certificates:Default:Path", @"C:\certs\default.pfx"))));
|
||||
|
||||
/// <summary>Among mixed endpoints, one HTTPS endpoint without a certificate still requires a generated certificate.</summary>
|
||||
[Fact]
|
||||
public void RequiresGeneratedCertificate_True_WhenMixedEndpointsAndOneHttpsHasNoCert()
|
||||
=> Assert.True(KestrelTlsInspector.RequiresGeneratedCertificate(
|
||||
|
||||
@@ -12,6 +12,7 @@ public sealed class SelfSignedCertificateProviderTests
|
||||
private static SelfSignedCertificateProvider CreateProvider(TlsOptions options, FakeTimeProvider time)
|
||||
=> new(options, NullLogger<SelfSignedCertificateProvider>.Instance, time);
|
||||
|
||||
/// <summary>Verifies that a generated certificate has the expected validity window, SANs (localhost, machine name, additional DNS names, loopback IPs), and the serverAuth EKU.</summary>
|
||||
[Fact]
|
||||
public void GenerateCertificate_HasExpectedSansEkuAndValidity()
|
||||
{
|
||||
@@ -39,6 +40,7 @@ public sealed class SelfSignedCertificateProviderTests
|
||||
o => o.Value == "1.3.6.1.5.5.7.3.1"); // serverAuth
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadOrCreate generates and persists a certificate on first call, then reuses the same persisted certificate (same thumbprint) on a subsequent call.</summary>
|
||||
[Fact]
|
||||
public void LoadOrCreate_GeneratesPersistsAndReuses_SameThumbprint()
|
||||
{
|
||||
@@ -58,6 +60,7 @@ public sealed class SelfSignedCertificateProviderTests
|
||||
finally { Directory.Delete(dir, recursive: true); }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadOrCreate regenerates the certificate (a new thumbprint) once the persisted certificate's validity window has elapsed.</summary>
|
||||
[Fact]
|
||||
public void LoadOrCreate_Regenerates_WhenPersistedCertExpired()
|
||||
{
|
||||
@@ -77,6 +80,7 @@ public sealed class SelfSignedCertificateProviderTests
|
||||
finally { Directory.Delete(dir, recursive: true); }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadOrCreate regenerates a valid certificate when the persisted PFX file is corrupt or unreadable.</summary>
|
||||
[Fact]
|
||||
public void LoadOrCreate_Regenerates_WhenPersistedFileCorrupt()
|
||||
{
|
||||
@@ -92,6 +96,7 @@ public sealed class SelfSignedCertificateProviderTests
|
||||
finally { Directory.Delete(dir, recursive: true); }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadOrCreate throws <see cref="InvalidOperationException"/> for an expired persisted certificate when regeneration is disabled.</summary>
|
||||
[Fact]
|
||||
public void LoadOrCreate_Throws_WhenExpiredAndRegenerateDisabled()
|
||||
{
|
||||
@@ -108,6 +113,7 @@ public sealed class SelfSignedCertificateProviderTests
|
||||
finally { Directory.Delete(dir, recursive: true); }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that LoadOrCreate throws <see cref="InvalidOperationException"/> when <c>SelfSignedCertPath</c> is blank.</summary>
|
||||
[Fact]
|
||||
public void LoadOrCreate_Throws_WhenSelfSignedCertPathBlank()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user