docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
+160
-13
@@ -16,7 +16,7 @@ using ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// M4 Bundle D (D1) — verifies <see cref="AuditWriteMiddleware"/> emits exactly one
|
||||
/// Verifies <see cref="AuditWriteMiddleware"/> emits exactly one
|
||||
/// <see cref="AuditChannel.ApiInbound"/> row per request via
|
||||
/// <see cref="ICentralAuditWriter"/> covering all outcome shapes:
|
||||
/// success (InboundRequest/Delivered), client/server error (InboundRequest/Failed),
|
||||
@@ -25,6 +25,10 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests.Middleware;
|
||||
/// </summary>
|
||||
public class AuditWriteMiddlewareTests
|
||||
{
|
||||
/// <summary>Verifies that only the peer that serves a relayed request writes the local failure audit row.</summary>
|
||||
/// <param name="scenario">The scenario selector.</param>
|
||||
/// <param name="localRows">The expected number of locally written audit rows.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(0, 1)]
|
||||
[InlineData(1, 1)]
|
||||
@@ -58,19 +62,35 @@ public class AuditWriteMiddlewareTests
|
||||
if (localRows > 0) Assert.NotNull(writer.Events[0].ExecutionId);
|
||||
}
|
||||
|
||||
private sealed class StandbyGate : IActiveNodeGate { public bool IsActiveNode => false; }
|
||||
private sealed class StandbyGate : IActiveNodeGate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsActiveNode => false;
|
||||
}
|
||||
private sealed class ResultForwarder(IResult result) : IStandbyRequestForwarder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsConfigured => true;
|
||||
/// <inheritdoc />
|
||||
public Task<IResult> ForwardAsync(HttpContext context, CancellationToken cancellationToken) => Task.FromResult(result);
|
||||
}
|
||||
private sealed class InboundOptions(InboundApiOptions value) : IOptionsMonitor<InboundApiOptions>
|
||||
{
|
||||
/// <summary>Gets the fixed options instance.</summary>
|
||||
public InboundApiOptions CurrentValue => value;
|
||||
/// <summary>Returns the fixed options instance regardless of name.</summary>
|
||||
/// <param name="name">The named-options key, ignored.</param>
|
||||
/// <returns>The fixed options instance.</returns>
|
||||
public InboundApiOptions Get(string? name) => value;
|
||||
/// <summary>Does nothing; the fake never raises change notifications.</summary>
|
||||
/// <param name="listener">The change listener, ignored.</param>
|
||||
/// <returns>Always null.</returns>
|
||||
public IDisposable? OnChange(Action<InboundApiOptions, string?> listener) => null;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that skipping body capture suppresses the exception text in the audit row.</summary>
|
||||
/// <param name="escaped">Whether the exception text contains characters that need escaping.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
@@ -100,11 +120,14 @@ public class AuditWriteMiddlewareTests
|
||||
/// </summary>
|
||||
private sealed class RecordingAuditWriter : ICentralAuditWriter
|
||||
{
|
||||
// C3 (Task 2.5): store the decomposed row view so assertions keep
|
||||
// Store the decomposed row view so assertions keep
|
||||
// reading the ScadaBridge domain fields as typed properties.
|
||||
/// <summary>Gets the recorded audit rows.</summary>
|
||||
public List<AuditRowProjection.AuditRowValues> Events { get; } = new();
|
||||
/// <summary>Gets or sets an optional callback invoked on each write.</summary>
|
||||
public Func<AuditEvent, Task>? OnWrite { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
|
||||
{
|
||||
lock (Events)
|
||||
@@ -170,8 +193,11 @@ public class AuditWriteMiddlewareTests
|
||||
/// </summary>
|
||||
private sealed class StubAuditActorAccessor : IAuditActorAccessor
|
||||
{
|
||||
/// <summary>Initializes the accessor with a fixed actor.</summary>
|
||||
/// <param name="currentActor">The actor string to return, or null for none.</param>
|
||||
public StubAuditActorAccessor(string? currentActor) => CurrentActor = currentActor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? CurrentActor { get; }
|
||||
}
|
||||
|
||||
@@ -185,12 +211,21 @@ public class AuditWriteMiddlewareTests
|
||||
{
|
||||
private readonly AuditLogOptions _value;
|
||||
|
||||
/// <summary>Initializes the monitor with a fixed options value.</summary>
|
||||
/// <param name="value">The options value returned on every read.</param>
|
||||
public StaticAuditLogOptionsMonitor(AuditLogOptions value) => _value = value;
|
||||
|
||||
/// <summary>Gets the fixed options instance.</summary>
|
||||
public AuditLogOptions CurrentValue => _value;
|
||||
|
||||
/// <summary>Returns the fixed options instance regardless of name.</summary>
|
||||
/// <param name="name">The named-options key, ignored.</param>
|
||||
/// <returns>The fixed options instance.</returns>
|
||||
public AuditLogOptions Get(string? name) => _value;
|
||||
|
||||
/// <summary>Does nothing; the fake never raises change notifications.</summary>
|
||||
/// <param name="listener">The change listener, ignored.</param>
|
||||
/// <returns>Always null.</returns>
|
||||
public IDisposable? OnChange(Action<AuditLogOptions, string?> listener) => null;
|
||||
}
|
||||
|
||||
@@ -198,6 +233,8 @@ public class AuditWriteMiddlewareTests
|
||||
// 1. Happy path — InboundRequest/Delivered/HttpStatus 200
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a successful request emits one inbound-request event with Delivered status and HTTP 200.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Pipeline_Success_EmitsOneEvent_KindInboundRequest_StatusDelivered_HttpStatus200()
|
||||
{
|
||||
@@ -226,6 +263,8 @@ public class AuditWriteMiddlewareTests
|
||||
// 2. 400 — script/validation failure path
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a 400 response emits an event with Failed status and HTTP 400.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Pipeline_400_EmitsEvent_Status_Failed_HttpStatus400()
|
||||
{
|
||||
@@ -251,6 +290,8 @@ public class AuditWriteMiddlewareTests
|
||||
// 3. 401 — auth failure path
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a 401 response emits an inbound-auth-failure event with Failed status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Pipeline_401_EmitsEvent_KindInboundAuthFailure_StatusFailed()
|
||||
{
|
||||
@@ -273,6 +314,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Null(evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a 403 response emits an inbound-auth-failure event with Failed status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Pipeline_403_EmitsEvent_KindInboundAuthFailure_StatusFailed()
|
||||
{
|
||||
@@ -296,6 +339,8 @@ public class AuditWriteMiddlewareTests
|
||||
// 4. 500 — handler threw OR returned 500
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a 500 response emits an event with Failed status.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Pipeline_500_EmitsEvent_Status_Failed()
|
||||
{
|
||||
@@ -315,6 +360,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(500, evt.HttpStatus);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a throwing pipeline emits a Failed event and rethrows.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Pipeline_Throws_EmitsEvent_Status_Failed_And_Rethrows()
|
||||
{
|
||||
@@ -340,6 +387,8 @@ public class AuditWriteMiddlewareTests
|
||||
// message), and the middleware lifts it onto the row.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a script error detail stashed on the items lands on the audit row.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ScriptErrorDetail_StashedOnItems_LandsOnTheRow()
|
||||
{
|
||||
@@ -360,6 +409,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(detail, evt.ErrorDetail);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that no stashed error detail leaves the row error detail null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NoErrorDetailStashed_LeavesErrorDetailNull()
|
||||
{
|
||||
@@ -377,6 +428,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Null(evt.ErrorDetail);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the error detail of a thrown exception wins over the stashed item.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ThrownException_ErrorDetail_WinsOverTheStashedItem()
|
||||
{
|
||||
@@ -408,6 +461,8 @@ public class AuditWriteMiddlewareTests
|
||||
// HttpContext.Items.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the API key name resolved from the HTTP context is recorded as the actor.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ApiKeyName_Resolved_From_HttpContext_AsActor()
|
||||
{
|
||||
@@ -437,6 +492,8 @@ public class AuditWriteMiddlewareTests
|
||||
// null — never echo an unauthenticated principal back.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the authenticated user from the accessor is recorded as the actor when there is no API key stash.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthenticatedUser_FromAccessor_RecordedAsActor_WhenNoApiKeyStash()
|
||||
{
|
||||
@@ -459,6 +516,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal("alice", evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the API key stash takes precedence over the authenticated principal.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ApiKeyStash_TakesPrecedence_OverAuthenticatedPrincipal()
|
||||
{
|
||||
@@ -483,6 +542,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal("integration-svc", evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an auth failure keeps the actor null even when the accessor resolves a principal.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuthFailure_KeepsActorNull_EvenWhenAccessorResolvesPrincipal()
|
||||
{
|
||||
@@ -506,6 +567,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Null(evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a request with no API key and no authenticated principal leaves the actor null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NoApiKey_NoAuthenticatedPrincipal_LeavesActorNull()
|
||||
{
|
||||
@@ -532,6 +595,8 @@ public class AuditWriteMiddlewareTests
|
||||
// 6. Writer failure must NEVER alter the HTTP response
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a throwing audit writer leaves a successful HTTP response unchanged.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuditWriter_Throws_HttpResponse_Unchanged_Success_Stays_Success()
|
||||
{
|
||||
@@ -553,6 +618,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(200, ctx.Response.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a throwing audit writer leaves a failed HTTP response unchanged.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuditWriter_Throws_OnFailedRequest_HttpResponse_Unchanged()
|
||||
{
|
||||
@@ -576,6 +643,8 @@ public class AuditWriteMiddlewareTests
|
||||
// 7. Provenance — RemoteIp + User-Agent surface in Extra JSON
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the remote IP and user agent appear in the Extra JSON.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RemoteIp_And_UserAgent_AppearInExtra()
|
||||
{
|
||||
@@ -605,6 +674,8 @@ public class AuditWriteMiddlewareTests
|
||||
// JsonDocument.Parse) still see the full payload.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the request body is buffered and stashed on the request summary.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestBody_IsBuffered_AndStashed_OnRequestSummary()
|
||||
{
|
||||
@@ -631,12 +702,14 @@ public class AuditWriteMiddlewareTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Execution id — Audit Log #23: each inbound row carries a fresh
|
||||
// Execution id — each inbound row carries a fresh
|
||||
// per-request execution id so inbound rows are correlatable. The inbound
|
||||
// row's CorrelationId stays null — CorrelationId is purely the
|
||||
// per-operation-lifecycle id and an inbound request is a one-shot.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an inbound row carries a non-null execution id and a null correlation id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InboundRow_CarriesNonNull_ExecutionId_And_NullCorrelationId()
|
||||
{
|
||||
@@ -658,6 +731,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Null(evt.CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that separate requests get distinct execution ids.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SeparateRequests_GetDistinct_ExecutionIds()
|
||||
{
|
||||
@@ -676,7 +751,7 @@ public class AuditWriteMiddlewareTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// ParentExecutionId — Audit Log #23 (ParentExecutionId feature, T3): the
|
||||
// ParentExecutionId — the
|
||||
// inbound request's ExecutionId is minted ONCE, early, and stashed on
|
||||
// HttpContext.Items so the endpoint handler can carry it onto the routed
|
||||
// RouteToCallRequest as ParentExecutionId. The inbound row that the
|
||||
@@ -684,6 +759,8 @@ public class AuditWriteMiddlewareTests
|
||||
// NEVER set.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the inbound execution id is stashed on the HTTP items before the endpoint runs.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InboundExecutionId_IsStashedOnHttpItems_BeforeEndpointRuns()
|
||||
{
|
||||
@@ -705,6 +782,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.NotEqual(Guid.Empty, stashed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the inbound row execution id equals the early-minted stashed id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InboundRow_ExecutionId_Equals_TheEarlyMintedStashedId()
|
||||
{
|
||||
@@ -727,6 +806,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(stashedDuringHandler, evt.ExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the inbound row's own parent execution id stays null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InboundRow_OwnParentExecutionId_StaysNull()
|
||||
{
|
||||
@@ -747,6 +828,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Null(evt.ParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the duration in milliseconds is recorded.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DurationMs_IsRecorded()
|
||||
{
|
||||
@@ -769,13 +852,15 @@ public class AuditWriteMiddlewareTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Response body capture — Audit Log #23 (inbound full-response feature).
|
||||
// Until the M5-deferred work landed, ResponseSummary was always null.
|
||||
// Response body capture (inbound full-response feature).
|
||||
// Until response capture was implemented, ResponseSummary was always null.
|
||||
// These tests pin the new contract: the middleware wraps Response.Body,
|
||||
// runs the pipeline, copies the buffered bytes back to the real stream,
|
||||
// and stashes a UTF-8 string copy on ResponseSummary.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the response body is captured on the response summary.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResponseBody_IsCaptured_OnResponseSummary()
|
||||
{
|
||||
@@ -795,6 +880,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(responseJson, evt.ResponseSummary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the response body is forwarded to the original stream so downstream readers see it.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResponseBody_IsForwardedToOriginalStream_DownstreamReadersSeeIt()
|
||||
{
|
||||
@@ -817,6 +904,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(responseJson, Encoding.UTF8.GetString(captured.ToArray()));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty response body leaves the response summary null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResponseBody_Empty_LeavesResponseSummaryNull()
|
||||
{
|
||||
@@ -837,6 +926,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(204, evt.HttpStatus);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that when the handler throws, the body written up to the throw is captured.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResponseBody_OnHandlerThrow_BodyCapturedUpToTheThrow()
|
||||
{
|
||||
@@ -870,6 +961,8 @@ public class AuditWriteMiddlewareTests
|
||||
// real client still see every byte.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a request body above the inbound maximum is truncated to the cap and flagged as payload truncated.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestBody_AboveInboundMaxBytes_TruncatedToCap_PayloadTruncatedTrue()
|
||||
{
|
||||
@@ -909,6 +1002,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.True(evt.PayloadTruncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a response body above the inbound maximum is truncated to the cap, the client still receives all bytes, and the payload is flagged as truncated.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResponseBody_AboveInboundMaxBytes_TruncatedToCap_ClientStillReceivesAllBytes_PayloadTruncatedTrue()
|
||||
{
|
||||
@@ -945,7 +1040,7 @@ public class AuditWriteMiddlewareTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// InboundAPI-018: asynchronously faulted audit-write tasks must be
|
||||
// Asynchronously faulted audit-write tasks must be
|
||||
// observed (logged) rather than silently dropped — but must still NOT
|
||||
// alter the user-facing HTTP response (alog.md §13).
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -954,10 +1049,11 @@ public class AuditWriteMiddlewareTests
|
||||
/// Test-only writer whose <see cref="WriteAsync"/> returns a Task that
|
||||
/// faults AFTER an asynchronous boundary, so the throw happens after
|
||||
/// <see cref="AuditWriteMiddleware"/>'s synchronous try/catch can see it —
|
||||
/// exactly the fire-and-forget bug InboundAPI-018 closes.
|
||||
/// exactly the fire-and-forget bug this test guards against.
|
||||
/// </summary>
|
||||
private sealed class AsyncFaultingAuditWriter : ICentralAuditWriter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
|
||||
{
|
||||
return FaultAsync();
|
||||
@@ -980,12 +1076,27 @@ public class AuditWriteMiddlewareTests
|
||||
/// </summary>
|
||||
private sealed class RecordingLogger : Microsoft.Extensions.Logging.ILogger<AuditWriteMiddleware>
|
||||
{
|
||||
/// <summary>Gets the captured log entries.</summary>
|
||||
public List<(Microsoft.Extensions.Logging.LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new();
|
||||
|
||||
/// <summary>Returns no scope; the fake logger does not track scopes.</summary>
|
||||
/// <typeparam name="TState">The type of the scope state.</typeparam>
|
||||
/// <param name="state">The scope state, ignored.</param>
|
||||
/// <returns>Always null or a no-op scope.</returns>
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||
|
||||
/// <summary>Reports every log level as enabled.</summary>
|
||||
/// <param name="logLevel">The log level being queried.</param>
|
||||
/// <returns>Always true.</returns>
|
||||
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) => true;
|
||||
|
||||
/// <summary>Records the log entry for later assertions.</summary>
|
||||
/// <typeparam name="TState">The type of the log state.</typeparam>
|
||||
/// <param name="logLevel">The severity of the entry.</param>
|
||||
/// <param name="eventId">The event identifier.</param>
|
||||
/// <param name="state">The log state.</param>
|
||||
/// <param name="exception">The associated exception, if any.</param>
|
||||
/// <param name="formatter">Formats the state and exception into the message.</param>
|
||||
public void Log<TState>(
|
||||
Microsoft.Extensions.Logging.LogLevel logLevel,
|
||||
Microsoft.Extensions.Logging.EventId eventId,
|
||||
@@ -1002,10 +1113,13 @@ public class AuditWriteMiddlewareTests
|
||||
private sealed class NullScope : IDisposable
|
||||
{
|
||||
public static readonly NullScope Instance = new();
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an asynchronous audit writer fault is observed as a warning and does not alter the response.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AuditWriter_AsyncFault_IsObserved_AsWarning_AndDoesNotAlterResponse()
|
||||
{
|
||||
@@ -1056,12 +1170,15 @@ public class AuditWriteMiddlewareTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// InboundAPI-019 — bodyless requests skip EnableBuffering so the
|
||||
// Bodyless requests skip EnableBuffering so the
|
||||
// FileBufferingReadStream allocation is avoided on GET/HEAD/DELETE
|
||||
// and any request whose Content-Length is 0. The audit row still emits
|
||||
// with a null RequestSummary, mirroring the bodyless-POST contract.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a bodyless HTTP method skips enabling buffering so the request stream is not replaced.</summary>
|
||||
/// <param name="method">The bodyless HTTP method under test.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("GET")]
|
||||
[InlineData("HEAD")]
|
||||
@@ -1102,6 +1219,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Null(evt.RequestSummary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a bodyless POST with content length zero skips enabling buffering.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task BodylessPost_ContentLengthZero_SkipsEnableBuffering()
|
||||
{
|
||||
@@ -1135,6 +1254,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Null(evt.RequestSummary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a POST with a body still enables buffering and captures the request summary.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task PostWithBody_StillEnablesBuffering_AndCapturesRequestSummary()
|
||||
{
|
||||
@@ -1161,13 +1282,15 @@ public class AuditWriteMiddlewareTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// M5.3 (T7) Increment 1: Request headers in Extra JSON
|
||||
// Increment 1: Request headers in Extra JSON
|
||||
// Request headers are captured into the Extra JSON object alongside the
|
||||
// existing remoteIp / userAgent fields. Sensitive headers (e.g.
|
||||
// Authorization, X-Api-Key) are redacted to "<redacted>" using the same
|
||||
// HeaderRedactList as ScadaBridgeAuditRedactor.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that request headers appear in the Extra JSON under the request headers key.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestHeaders_AppearInExtra_UnderRequestHeadersKey()
|
||||
{
|
||||
@@ -1197,6 +1320,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal("custom-value", customVal.GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Authorization header is redacted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestHeaders_AuthorizationHeader_IsRedacted()
|
||||
{
|
||||
@@ -1224,6 +1349,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal("<redacted>", authVal.GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the X-API-Key header is redacted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestHeaders_XApiKeyHeader_IsRedacted()
|
||||
{
|
||||
@@ -1249,6 +1376,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal("<redacted>", keyVal.GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an entry on the custom redact list is redacted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestHeaders_CustomRedactListEntry_IsRedacted()
|
||||
{
|
||||
@@ -1284,6 +1413,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal("safe-value", headers.GetProperty("X-Safe-Header").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that header redaction is case-insensitive.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestHeaders_Redaction_IsCaseInsensitive()
|
||||
{
|
||||
@@ -1312,7 +1443,7 @@ public class AuditWriteMiddlewareTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// M5.3 (T7) Increment 2: AuditInboundCeilingHits counter
|
||||
// Increment 2: AuditInboundCeilingHits counter
|
||||
// When request OR response exceeds InboundMaxBytes, the middleware
|
||||
// increments IAuditInboundCeilingHitsCounter once per request.
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -1324,7 +1455,9 @@ public class AuditWriteMiddlewareTests
|
||||
private sealed class RecordingCeilingHitsCounter : ZB.MOM.WW.ScadaBridge.AuditLog.Central.IAuditInboundCeilingHitsCounter
|
||||
{
|
||||
private int _count;
|
||||
/// <summary>Gets the number of recorded increments.</summary>
|
||||
public int Count => Volatile.Read(ref _count);
|
||||
/// <inheritdoc />
|
||||
public void Increment() => Interlocked.Increment(ref _count);
|
||||
}
|
||||
|
||||
@@ -1341,6 +1474,8 @@ public class AuditWriteMiddlewareTests
|
||||
actorAccessor: null,
|
||||
ceilingHitsCounter: counter);
|
||||
|
||||
/// <summary>Verifies that a request body above the inbound maximum increments the ceiling-hits counter.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RequestBody_AboveInboundMaxBytes_IncrementsCeilingHitsCounter()
|
||||
{
|
||||
@@ -1367,6 +1502,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.True(evt.PayloadTruncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a response body above the inbound maximum increments the ceiling-hits counter.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ResponseBody_AboveInboundMaxBytes_IncrementsCeilingHitsCounter()
|
||||
{
|
||||
@@ -1394,6 +1531,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.True(evt.PayloadTruncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a normal request within the cap does not increment the ceiling-hits counter.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NormalRequest_WithinCap_DoesNotIncrementCeilingHitsCounter()
|
||||
{
|
||||
@@ -1418,7 +1557,7 @@ public class AuditWriteMiddlewareTests
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// M5.3 (T7) Increment 3: SkipBodyCapture per-method opt-out
|
||||
// Increment 3: SkipBodyCapture per-method opt-out
|
||||
// A target with SkipBodyCapture=true produces an audit row with
|
||||
// headers/metadata but empty/omitted body. A normal target still captures.
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -1444,6 +1583,8 @@ public class AuditWriteMiddlewareTests
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with body capture skipped, an audit row is still emitted but its body is null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SkipBodyCapture_True_AuditRowEmitted_ButBodyIsNull()
|
||||
{
|
||||
@@ -1489,6 +1630,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(200, evt.HttpStatus);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with body capture skipped, the ceiling-hits counter is not incremented.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SkipBodyCapture_True_CeilingHitsCounter_NotIncremented()
|
||||
{
|
||||
@@ -1526,6 +1669,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal(0, counter.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with body capture not skipped, a normal target still captures the body.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SkipBodyCapture_False_NormalTarget_StillCapturesBody()
|
||||
{
|
||||
@@ -1561,6 +1706,8 @@ public class AuditWriteMiddlewareTests
|
||||
Assert.Equal("{\"result\":1}", evt.ResponseSummary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with no override, the default target still captures the body.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SkipBodyCapture_NoOverride_DefaultTarget_StillCapturesBody()
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ using System.Security.Claims;
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// M4 Bundle D (D2) — verifies the production pipeline order from
|
||||
/// Verifies the production pipeline order from
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.Host.Program.cs</c> for the inbound API:
|
||||
/// <c>UseAuthentication → UseAuthorization → UseAuditWriteMiddleware → endpoint</c>.
|
||||
///
|
||||
@@ -42,7 +42,10 @@ public class MiddlewareOrderTests
|
||||
/// </summary>
|
||||
private sealed class OrderingRecorder
|
||||
{
|
||||
/// <summary>Gets the stages recorded so far, in order.</summary>
|
||||
public List<string> Stages { get; } = new();
|
||||
/// <summary>Appends a stage name to the recorded list.</summary>
|
||||
/// <param name="stage">The name of the stage that ran.</param>
|
||||
public void Record(string stage)
|
||||
{
|
||||
lock (Stages) { Stages.Add(stage); }
|
||||
@@ -51,9 +54,11 @@ public class MiddlewareOrderTests
|
||||
|
||||
private sealed class RecordingAuditWriter : ICentralAuditWriter
|
||||
{
|
||||
// C3 (Task 2.5): store the decomposed row view so assertions keep
|
||||
// Store the decomposed row view so assertions keep
|
||||
// reading the ScadaBridge domain fields as typed properties.
|
||||
/// <summary>Gets the audit events written, as decomposed row views.</summary>
|
||||
public List<AuditRowProjection.AuditRowValues> Events { get; } = new();
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
|
||||
{
|
||||
lock (Events) { Events.Add(evt.AsRow()); }
|
||||
@@ -61,6 +66,8 @@ public class MiddlewareOrderTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the audit-write middleware runs after authentication and before script execution.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Middleware_Pipeline_PlacesAuditWriteAfterAuth_BeforeScriptExecution()
|
||||
{
|
||||
@@ -99,6 +106,8 @@ public class MiddlewareOrderTests
|
||||
Assert.Equal("test-actor", evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the pipeline records an authentication failure without reaching the endpoint.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Middleware_Pipeline_Records_AuthFailure_Beyond_Endpoint()
|
||||
{
|
||||
@@ -225,12 +234,17 @@ public class MiddlewareOrderTests
|
||||
/// </summary>
|
||||
private sealed class AlwaysAuthenticatedHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
/// <summary>Initializes the handler with the standard authentication-handler dependencies.</summary>
|
||||
/// <param name="options">The authentication scheme options monitor.</param>
|
||||
/// <param name="logger">The logger factory.</param>
|
||||
/// <param name="encoder">The URL encoder.</param>
|
||||
public AlwaysAuthenticatedHandler(
|
||||
Microsoft.Extensions.Options.IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
Microsoft.Extensions.Logging.ILoggerFactory logger,
|
||||
System.Text.Encodings.Web.UrlEncoder encoder)
|
||||
: base(options, logger, encoder) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "framework-user") }, "TestScheme");
|
||||
@@ -249,12 +263,21 @@ public class MiddlewareOrderTests
|
||||
{
|
||||
private readonly AuditLogOptions _value;
|
||||
|
||||
/// <summary>Initializes the monitor with a fixed options value.</summary>
|
||||
/// <param name="value">The options value to return.</param>
|
||||
public StaticAuditLogOptionsMonitor(AuditLogOptions value) => _value = value;
|
||||
|
||||
/// <summary>Gets the fixed options value.</summary>
|
||||
public AuditLogOptions CurrentValue => _value;
|
||||
|
||||
/// <summary>Returns the fixed options value regardless of name.</summary>
|
||||
/// <param name="name">The named-options name, ignored.</param>
|
||||
/// <returns>The fixed options value.</returns>
|
||||
public AuditLogOptions Get(string? name) => _value;
|
||||
|
||||
/// <summary>Ignores the listener because the value never changes.</summary>
|
||||
/// <param name="listener">The change listener, ignored.</param>
|
||||
/// <returns>Always null.</returns>
|
||||
public IDisposable? OnChange(Action<AuditLogOptions, string?> listener) => null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user