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:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -21,7 +21,7 @@ using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
/// <summary>
/// HTTP-pipeline tests for the #23 M8 audit endpoints (<see cref="AuditEndpoints"/>).
/// HTTP-pipeline tests for the audit endpoints (<see cref="AuditEndpoints"/>).
///
/// <para>
/// ManagementService authenticates each request by hand (HTTP Basic → LDAP →
@@ -35,7 +35,7 @@ public class AuditEndpointsTests
{
private const string BasicCredential = "auditor:password";
// C3 (Task 2.5): canonical ZB.MOM.WW.Audit.AuditEvent via the shared factory;
// Canonical ZB.MOM.WW.Audit.AuditEvent via the shared factory;
// the endpoint serializes the canonical record (eventId + occurredAtUtc top-level,
// domain fields out of DetailsJson) so these tests assert on the JSON/CSV output.
private static AuditEvent SampleEvent(Guid? id = null, DateTime? occurredAt = null) =>
@@ -74,7 +74,7 @@ public class AuditEndpointsTests
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
}
// Substituted LDAP bind — the shared ILdapAuthService is the seam now (Task 1.2).
// Substituted LDAP bind — the shared ILdapAuthService is the seam now.
var ldap = Substitute.For<ILdapAuthService>();
ldap.AuthenticateAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(ldapSucceeds
@@ -127,6 +127,8 @@ public class AuditEndpointsTests
// /api/audit/query
// ─────────────────────────────────────────────────────────────────────
/// <summary>Verifies that a query with valid parameters returns a JSON page of rows.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Query_ValidParams_ReturnsJsonPage()
{
@@ -152,6 +154,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a query with a cursor returns the next page.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Query_WithCursor_ReturnsNextPage()
{
@@ -196,6 +200,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a query without the operational-audit permission returns 403.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Query_WithoutOperationalAudit_Returns403()
{
@@ -209,6 +215,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a query without credentials returns 401.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Query_WithoutCredentials_Returns401()
{
@@ -220,10 +228,12 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a query by a viewer is allowed.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Query_ViewerRole_IsAllowed()
{
// Viewer (post Task 1.7 home of the former AuditReadOnly role) satisfies
// Viewer (the home of the former AuditReadOnly role) satisfies
// OperationalAudit (read) — query must succeed.
var (client, _, host) = await BuildHostAsync(roles: new[] { "Viewer" });
using (host)
@@ -237,6 +247,8 @@ public class AuditEndpointsTests
// /api/audit/export
// ─────────────────────────────────────────────────────────────────────
/// <summary>Verifies that a CSV export streams its content with a CSV content type.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Export_Csv_StreamsContent_WithCsvContentType()
{
@@ -265,6 +277,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that an export defaults to CSV when the format is omitted.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Export_Csv_DefaultsWhenFormatOmitted()
{
@@ -280,6 +294,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a JSON Lines export streams one record per line.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Export_Jsonl_StreamsOnePerLine()
{
@@ -317,9 +333,9 @@ public class AuditEndpointsTests
// ─────────────────────────────────────────────────────────────────────
// CSV export — formula-injection neutralization + column alignment
// (arch-review 2026-08-19 P6-7)
// ─────────────────────────────────────────────────────────────────────
/// <summary>Verifies that the CSV header has 24 columns in the same order as the export row properties.</summary>
[Fact]
public void CsvHeader_Has24Columns_SameOrderAsAuditExportRowProperties()
{
@@ -341,6 +357,7 @@ public class AuditEndpointsTests
}, columns);
}
/// <summary>Verifies that a formatted CSV row includes the execution id, parent execution id and source node.</summary>
[Fact]
public void FormatCsvRow_IncludesExecutionParentExecutionAndSourceNode()
{
@@ -363,6 +380,10 @@ public class AuditEndpointsTests
Assert.Equal("site-a-node-a", fields[9]); // SourceNode
}
/// <summary>Verifies that a free-text field starting with a formula character is prefixed with a single quote in the CSV export.</summary>
/// <param name="malicious">The free-text value that starts with a formula character.</param>
/// <param name="expectedPrefix">The expected start of the exported cell.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
[Theory]
[InlineData("=HYPERLINK(\"http://evil\",\"click\")", "'=HYPERLINK(")]
[InlineData("+cmd|'/c calc'!A1", "'+cmd|")]
@@ -371,7 +392,7 @@ public class AuditEndpointsTests
[InlineData("\tsneaky", "'\tsneaky")]
public async Task Export_Csv_FreeTextLeadingFormulaCharacter_IsPrefixedWithQuote(string malicious, string expectedPrefix)
{
// CWE-1236: Target is inbound-API-caller-controlled free text. A raw
// CSV formula injection: Target is inbound-API-caller-controlled free text. A raw
// leading =, +, -, @, or TAB must never reach the CSV cell unescaped.
var evt = ScadaBridgeAuditEventFactory.Create(
channel: AuditChannel.ApiInbound,
@@ -400,6 +421,7 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the error message, response summary and extra columns are neutralized against formula injection.</summary>
[Fact]
public void FormatCsvRow_ErrorMessageResponseSummaryAndExtra_AreNeutralized()
{
@@ -422,6 +444,7 @@ public class AuditEndpointsTests
Assert.Equal("'@evil", fields[22]); // Extra
}
/// <summary>Verifies that negative durations and date columns are never neutralized.</summary>
[Fact]
public void FormatCsvRow_NegativeDurationAndDateColumns_NeverNeutralized()
{
@@ -445,6 +468,7 @@ public class AuditEndpointsTests
Assert.Equal("200", fields[15]); // HttpStatus, unprefixed
}
/// <summary>Verifies that a benign free-text field is not prefixed.</summary>
[Fact]
public void FormatCsvRow_BenignFreeTextField_IsNotPrefixed()
{
@@ -462,6 +486,7 @@ public class AuditEndpointsTests
Assert.Equal("plant-a-external-system", fields[13]); // Target
}
/// <summary>Verifies that the CSV header columns match the property order of the export row.</summary>
[Fact]
public void CsvHeader_ColumnsMatchAuditExportRowPropertyOrder()
{
@@ -481,6 +506,8 @@ public class AuditEndpointsTests
Assert.Equal(propertyNames, AuditEndpoints.CsvHeader.Split(','));
}
/// <summary>Verifies that a Parquet export returns 501.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Export_Parquet_Returns501()
{
@@ -497,12 +524,14 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that an export without the audit-export permission returns 403.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Export_WithoutAuditExport_Returns403()
{
// Viewer (former AuditReadOnly) grants read (OperationalAudit) but NOT
// bulk export (AuditExport) — the export endpoint must 403. This is the
// preserved half-SoD after the Task 1.7 AuditReadOnly→Viewer collapse.
// preserved half-SoD after the AuditReadOnly→Viewer collapse.
var (client, _, host) = await BuildHostAsync(roles: new[] { "Viewer" });
using (host)
{
@@ -511,6 +540,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that an export with an unsupported format returns 400.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Export_UnsupportedFormat_Returns400()
{
@@ -526,6 +557,7 @@ public class AuditEndpointsTests
// Query-string parsing units
// ─────────────────────────────────────────────────────────────────────
/// <summary>Verifies that paging parsing clamps the page size to the maximum.</summary>
[Fact]
public void ParsePaging_ClampsPageSizeToMax()
{
@@ -540,6 +572,7 @@ public class AuditEndpointsTests
Assert.Equal(AuditEndpoints.MaxPageSize, paging.PageSize);
}
/// <summary>Verifies that repeated filter parameters parse into multi-value lists.</summary>
[Fact]
public void ParseFilter_RepeatedParams_ParseIntoMultiValueLists()
{
@@ -562,6 +595,7 @@ public class AuditEndpointsTests
Assert.Equal(new[] { "plant-a", "plant-b" }, filter.SourceSiteIds);
}
/// <summary>Verifies that a single filter parameter parses into a one-element list.</summary>
[Fact]
public void ParseFilter_SingleParam_ParsesIntoOneElementList()
{
@@ -594,6 +628,7 @@ public class AuditEndpointsTests
// removed. The Guid/date dimensions stay lax — see ParseFilter's remarks.
// ========================================================================
/// <summary>Verifies that an unrecognised enum value in a filter throws.</summary>
[Fact]
public void ParseFilter_UnrecognisedEnumValue_Throws()
{
@@ -608,6 +643,7 @@ public class AuditEndpointsTests
Assert.Contains("channel", ex.Message);
}
/// <summary>Verifies that an unrecognised status throws an error naming the valid values.</summary>
[Fact]
public void ParseFilter_UnrecognisedStatus_ThrowsNamingTheValidSet()
{
@@ -624,6 +660,7 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that lowercase enum values are accepted in a filter.</summary>
[Fact]
public void ParseFilter_LowercaseEnumValues_AreAccepted()
{
@@ -644,6 +681,7 @@ public class AuditEndpointsTests
Assert.Equal(new[] { AuditStatus.Delivered, AuditStatus.Parked }, filter.Statuses);
}
/// <summary>Verifies that a numeric enum value in a filter is rejected.</summary>
[Fact]
public void ParseFilter_NumericEnumValue_IsRejected()
{
@@ -658,6 +696,7 @@ public class AuditEndpointsTests
Assert.Throws<AuditFilterParseException>(() => AuditEndpoints.ParseFilter(query));
}
/// <summary>Verifies that an execution id filter parses into a single GUID value.</summary>
[Fact]
public void ParseFilter_ExecutionId_ParsesIntoSingleValueGuid()
{
@@ -674,6 +713,7 @@ public class AuditEndpointsTests
Assert.Equal(executionId, filter.ExecutionId);
}
/// <summary>Verifies that an unparseable execution id filter is silently dropped.</summary>
[Fact]
public void ParseFilter_UnparseableExecutionId_IsDroppedSilently()
{
@@ -690,6 +730,7 @@ public class AuditEndpointsTests
Assert.Null(filter.ExecutionId);
}
/// <summary>Verifies that a parent execution id filter parses into a single GUID value.</summary>
[Fact]
public void ParseFilter_ParentExecutionId_ParsesIntoSingleValueGuid()
{
@@ -706,6 +747,7 @@ public class AuditEndpointsTests
Assert.Equal(parentExecutionId, filter.ParentExecutionId);
}
/// <summary>Verifies that an unparseable parent execution id filter is silently dropped.</summary>
[Fact]
public void ParseFilter_UnparseableParentExecutionId_IsDroppedSilently()
{
@@ -722,6 +764,8 @@ public class AuditEndpointsTests
Assert.Null(filter.ParentExecutionId);
}
/// <summary>Verifies that repeated channel parameters reach the repository as a multi-value filter.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Query_RepeatedChannelParams_ReachRepositoryAsMultiValueFilter()
{
@@ -744,6 +788,7 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a half-supplied paging cursor is dropped.</summary>
[Fact]
public void ParsePaging_HalfSuppliedCursor_IsDropped()
{
@@ -762,9 +807,10 @@ public class AuditEndpointsTests
}
// ─────────────────────────────────────────────────────────────────────
// ApplySiteScope (Management-019)
// ApplySiteScope
// ─────────────────────────────────────────────────────────────────────
/// <summary>Verifies that a system-wide user's filter is returned unchanged by site scoping.</summary>
[Fact]
public void ApplySiteScope_SystemWideUser_ReturnsFilterUnchanged()
{
@@ -781,6 +827,7 @@ public class AuditEndpointsTests
Assert.Same(filter, result);
}
/// <summary>Verifies that an empty caller filter is restricted to the permitted sites for a scoped user.</summary>
[Fact]
public void ApplySiteScope_ScopedUser_EmptyCallerFilter_RestrictedToPermittedSet()
{
@@ -798,6 +845,7 @@ public class AuditEndpointsTests
Assert.Equal(new[] { "plant-a", "plant-b" }, result.SourceSiteIds!.OrderBy(s => s).ToArray());
}
/// <summary>Verifies that an explicit in-scope filter is kept unchanged for a scoped user.</summary>
[Fact]
public void ApplySiteScope_ScopedUser_ExplicitInScopeFilter_KeptVerbatim()
{
@@ -811,6 +859,7 @@ public class AuditEndpointsTests
Assert.Equal(new[] { "plant-a" }, result!.SourceSiteIds);
}
/// <summary>Verifies that an explicit out-of-scope filter yields null for a scoped user.</summary>
[Fact]
public void ApplySiteScope_ScopedUser_ExplicitOutOfScopeFilter_ReturnsNull()
{
@@ -825,6 +874,7 @@ public class AuditEndpointsTests
Assert.Null(result);
}
/// <summary>Verifies that a mixed filter is reduced to its in-scope sites for a scoped user.</summary>
[Fact]
public void ApplySiteScope_ScopedUser_MixedInAndOutOfScopeFilter_IntersectedToInScopeOnly()
{
@@ -907,6 +957,8 @@ public class AuditEndpointsTests
FirstOccurredAtUtc: new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
LastOccurredAtUtc: new DateTime(2026, 5, 20, 10, 1, 0, DateTimeKind.Utc));
/// <summary>Verifies that a scoped viewer's permitted sites are passed to the repository by the tree endpoint.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Tree_ScopedViewer_ThreadsPermittedSitesToRepository()
{
@@ -921,6 +973,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the tree endpoint returns a JSON array for a valid execution id.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Tree_ValidExecutionId_ReturnsJsonArray()
{
@@ -950,6 +1004,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the tree endpoint returns an empty array when the repository returns nothing.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Tree_RepoReturnsEmpty_ReturnsEmptyArray()
{
@@ -969,6 +1025,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the tree endpoint returns 400 when the execution id is missing.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Tree_MissingExecutionId_Returns400()
{
@@ -981,6 +1039,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the tree endpoint returns 400 when the execution id is invalid.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Tree_InvalidExecutionId_Returns400()
{
@@ -995,6 +1055,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the tree endpoint returns 403 without the operational-audit permission.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Tree_WithoutOperationalAudit_Returns403()
{
@@ -1007,6 +1069,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the tree endpoint returns 401 without credentials.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Tree_WithoutCredentials_Returns401()
{
@@ -1019,6 +1083,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the tree endpoint is allowed for a viewer.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Tree_ViewerRole_IsAllowed()
{
@@ -1032,7 +1098,7 @@ public class AuditEndpointsTests
}
// ─────────────────────────────────────────────────────────────────────
// POST /api/audit/backfill-source-node (M5.6 T5)
// POST /api/audit/backfill-source-node
// ─────────────────────────────────────────────────────────────────────
private static async Task<(HttpClient Client, IAuditLogRepository Repo, IHost Host)> BuildHostWithBackfillAsync(
@@ -1100,6 +1166,8 @@ public class AuditEndpointsTests
return request;
}
/// <summary>Verifies that an administrator's source-node backfill returns 200 with the updated row count.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BackfillSourceNode_AdminRole_Returns200WithRowCount()
{
@@ -1120,6 +1188,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a viewer's source-node backfill returns 403.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BackfillSourceNode_ViewerRole_Returns403()
{
@@ -1135,6 +1205,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a source-node backfill without credentials returns 401.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BackfillSourceNode_NoCredentials_Returns401()
{
@@ -1150,6 +1222,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a source-node backfill without a before date returns 400.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BackfillSourceNode_MissingBefore_Returns400()
{
@@ -1165,6 +1239,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a source-node backfill with an invalid before date returns 400.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BackfillSourceNode_InvalidBeforeDate_Returns400()
{
@@ -1179,6 +1255,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that a custom sentinel and batch size are passed to the repository.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BackfillSourceNode_CustomSentinelAndBatch_PassedToRepo()
{
@@ -1200,6 +1278,8 @@ public class AuditEndpointsTests
}
}
/// <summary>Verifies that the sentinel defaults to unknown when it is omitted.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BackfillSourceNode_DefaultSentinel_IsUnknown_WhenOmitted()
{