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:
@@ -27,6 +27,7 @@ public class ArchitecturalConstraintTests
|
||||
return Path.GetDirectoryName(csproj)!;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Commons project file declares none of the forbidden package references.</summary>
|
||||
[Fact]
|
||||
public void Csproj_ShouldNotHaveForbiddenPackageReferences()
|
||||
{
|
||||
@@ -51,6 +52,7 @@ public class ArchitecturalConstraintTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Commons assembly references none of the forbidden assemblies.</summary>
|
||||
[Fact]
|
||||
public void Assembly_ShouldNotReferenceForbiddenAssemblies()
|
||||
{
|
||||
@@ -68,6 +70,7 @@ public class ArchitecturalConstraintTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Commons contains no service or actor implementations.</summary>
|
||||
[Fact]
|
||||
public void Commons_ShouldNotContainServiceOrActorImplementations()
|
||||
{
|
||||
@@ -99,6 +102,7 @@ public class ArchitecturalConstraintTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that no source file calls <c>ToLocalTime</c>, since all timestamps are UTC.</summary>
|
||||
[Fact]
|
||||
public void SourceFiles_ShouldNotContainToLocalTime()
|
||||
{
|
||||
@@ -112,6 +116,7 @@ public class ArchitecturalConstraintTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that no entity class carries Entity Framework attributes.</summary>
|
||||
[Fact]
|
||||
public void AllEntities_ShouldHaveNoEfAttributes()
|
||||
{
|
||||
@@ -145,6 +150,7 @@ public class ArchitecturalConstraintTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the retired <c>DebugStreamEvent</c> type does not exist.</summary>
|
||||
[Fact]
|
||||
public void DebugStreamEvent_ShouldNotExist()
|
||||
{
|
||||
@@ -154,6 +160,7 @@ public class ArchitecturalConstraintTests
|
||||
Assert.Null(type);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that every enum has a singular name.</summary>
|
||||
[Fact]
|
||||
public void AllEnums_ShouldBeSingularNamed()
|
||||
{
|
||||
@@ -168,6 +175,7 @@ public class ArchitecturalConstraintTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that every message type is a record.</summary>
|
||||
[Fact]
|
||||
public void AllMessageTypes_ShouldBeRecords()
|
||||
{
|
||||
@@ -187,6 +195,7 @@ public class ArchitecturalConstraintTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the key entities use <see cref="DateTimeOffset"/> for their timestamp properties.</summary>
|
||||
[Fact]
|
||||
public void KeyEntities_ShouldHaveDateTimeOffsetTimestamps()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Audit;
|
||||
/// </summary>
|
||||
public class ScriptTraceEnumTests
|
||||
{
|
||||
/// <summary>Verifies that each script trace kind fits the central audit columns.</summary>
|
||||
/// <param name="kind">The script trace audit kind under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditKind.RouteCall)]
|
||||
[InlineData(AuditKind.ScriptRun)]
|
||||
@@ -22,6 +24,7 @@ public class ScriptTraceEnumTests
|
||||
Assert.Equal($"ScriptTrace.{kind}", AuditFieldBuilders.BuildAction(AuditChannel.ScriptTrace, kind));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the script trace values are appended after the cluster values so existing ordinals stay stable.</summary>
|
||||
[Fact]
|
||||
public void ScriptTrace_IsAppendedAfterCluster_SoExistingOrdinalsAreStable()
|
||||
{
|
||||
|
||||
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// Final review M1: the ONE parser behind the graph node's exception subtitle and the step
|
||||
/// The single parser behind the graph node's exception subtitle and the step
|
||||
/// detail panel's exception block. Before it, the panel showed the OUTERMOST exception of a
|
||||
/// wrapped chain while the node showed the innermost — the two now agree by construction.
|
||||
/// </summary>
|
||||
public class ExceptionHeaderTests
|
||||
{
|
||||
/// <summary>Verifies that a plain detail splits the type from the message and shortens the type only when asked.</summary>
|
||||
[Fact]
|
||||
public void Parse_PlainDetail_SplitsTypeFromMessage_AndShortensOnlyWhenAsked()
|
||||
{
|
||||
@@ -18,6 +19,7 @@ public class ExceptionHeaderTests
|
||||
Assert.Equal(("System.InvalidOperationException", "Recipe 42 rejected"), ExceptionHeader.Parse(detail, shortenType: false));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a wrapped exception chain resolves to the innermost exception.</summary>
|
||||
[Fact]
|
||||
public void Parse_WrappedChain_TakesTheINNERMOSTException()
|
||||
{
|
||||
@@ -28,6 +30,7 @@ public class ExceptionHeaderTests
|
||||
Assert.Equal(("C.Innermost", "the real one"), ExceptionHeader.Parse(detail, shortenType: false));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a generic type keeps its arity and arguments verbatim and only the head is shortened.</summary>
|
||||
[Fact]
|
||||
public void Parse_GenericType_KeepsTheArityAndArgumentsVerbatim_ShorteningOnlyTheHead()
|
||||
{
|
||||
@@ -37,6 +40,7 @@ public class ExceptionHeaderTests
|
||||
ExceptionHeader.Parse("RecipeException`1[System.String]: msg", shortenType: true));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the chain marker is matched after trimming, so a trailing marker is not treated as a chain.</summary>
|
||||
[Fact]
|
||||
public void Parse_ChainMarkerIsMatchedAfterTrimming_SoATrailingMarkerIsNotAChain()
|
||||
{
|
||||
@@ -46,12 +50,14 @@ public class ExceptionHeaderTests
|
||||
Assert.Equal(("Outer", "outer --->"), ExceptionHeader.Parse("A.Outer: outer ---> ", shortenType: true));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a line without a colon is treated as all type and no message.</summary>
|
||||
[Fact]
|
||||
public void Parse_LineWithoutAColon_IsAllTypeAndNoMessage()
|
||||
{
|
||||
Assert.Equal(("SomethingWentWrong", string.Empty), ExceptionHeader.Parse("SomethingWentWrong", shortenType: true));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CRLF detail has the carriage return trimmed off the first line.</summary>
|
||||
[Fact]
|
||||
public void Parse_CrLfDetail_TrimsTheCarriageReturnOffTheFirstLine()
|
||||
{
|
||||
@@ -59,6 +65,8 @@ public class ExceptionHeaderTests
|
||||
ExceptionHeader.Parse("System.TimeoutException: took too long\r\n at X\r\n", shortenType: true));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing or blank detail returns null.</summary>
|
||||
/// <param name="detail">The exception detail text to parse.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
|
||||
|
||||
public class FlowEnumContractTests
|
||||
{
|
||||
/// <summary>Verifies that the enum names and numeric values remain stable.</summary>
|
||||
[Fact]
|
||||
public void EnumNamesAndValues_RemainStable()
|
||||
{
|
||||
@@ -15,6 +16,7 @@ public class FlowEnumContractTests
|
||||
Assert.Equal(Enumerable.Range(0, 7), Enum.GetValues<FlowStepOutcome>().Select(x => (int)x));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the kind rank follows the display order independent of the enum's numeric value.</summary>
|
||||
[Fact]
|
||||
public void KindRank_UsesDisplayOrderIndependentOfEnumValue()
|
||||
{
|
||||
|
||||
@@ -51,6 +51,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
|
||||
private static FlowStep StepOf(FlowGraph graph, FlowStepKind kind) => Assert.Single(graph.Steps, s => s.Kind == kind);
|
||||
|
||||
/// <summary>Verifies that a log line carrying a step event id attaches to that step.</summary>
|
||||
[Fact]
|
||||
public void LogLine_WithStepEventId_AttachesToThatStep()
|
||||
{
|
||||
@@ -65,6 +66,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.Null(StepOf(graph, FlowStepKind.ScriptRun).Logs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a log line without a step id attaches to the execution anchor.</summary>
|
||||
[Fact]
|
||||
public void LogLine_WithoutStepId_AttachesToTheExecutionAnchor()
|
||||
{
|
||||
@@ -76,6 +78,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.Null(StepOf(graph, FlowStepKind.AttributeWrite).Logs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a central log line attaches to the inbound row rather than the response.</summary>
|
||||
[Fact]
|
||||
public void CentralLine_AttachesToInboundRow_NotResponse()
|
||||
{
|
||||
@@ -87,6 +90,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.Null(StepOf(graph, FlowStepKind.Response).Logs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script-entry log line attaches to the route-call root.</summary>
|
||||
[Fact]
|
||||
public void ScriptEntryLine_AttachesToRouteCallRoot()
|
||||
{
|
||||
@@ -97,6 +101,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.Single(StepOf(graph, FlowStepKind.RouteCall).Logs!);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a log line with an unknown step id falls back to the anchor.</summary>
|
||||
[Fact]
|
||||
public void LogLine_UnknownStepId_FallsBackToAnchor()
|
||||
{
|
||||
@@ -108,6 +113,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.Equal(0, graph.UnattachedLogLines);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a log line with no anchor falls back to the first step.</summary>
|
||||
[Fact]
|
||||
public void LogLine_NoAnchor_FallsBackToFirstStep()
|
||||
{
|
||||
@@ -119,6 +125,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.Equal(0, graph.UnattachedLogLines);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a log line whose execution has no step is counted as unattached.</summary>
|
||||
[Fact]
|
||||
public void LogLine_NoStepInExecution_IsCountedUnattached()
|
||||
{
|
||||
@@ -130,6 +137,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.All(graph.Steps, s => Assert.True(s.Logs is null or { Count: 0 }));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building from log lines only returns an empty graph with the unattached count.</summary>
|
||||
[Fact]
|
||||
public void Build_LinesOnly_ReturnsEmptyGraphWithUnattachedCount()
|
||||
{
|
||||
@@ -141,6 +149,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.True(graph.LogsTruncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that lines within the same millisecond are ordered by sequence with null sequences last.</summary>
|
||||
[Fact]
|
||||
public void Lines_OrderBySeqWithinSameMillisecond_NullSeqLast()
|
||||
{
|
||||
@@ -160,6 +169,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.Equal(new[] { "one", "two", "three", "null" }, logs.Select(l => l.Message).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that log lines never become graph steps.</summary>
|
||||
[Fact]
|
||||
public void LogLines_NeverBecomeSteps()
|
||||
{
|
||||
@@ -177,6 +187,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.DoesNotContain(graph.Edges, e => lineIds.Contains(e.FromEventId) || lineIds.Contains(e.ToEventId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the builder passes the logs-truncated flag through to the graph.</summary>
|
||||
[Fact]
|
||||
public void Build_PassesLogsTruncatedThrough()
|
||||
{
|
||||
@@ -188,6 +199,7 @@ public class FlowGraphBuilderLogLineTests
|
||||
Assert.False(graph.Truncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a log line on a folded outbound step attaches by the first row id.</summary>
|
||||
[Fact]
|
||||
public void LogLine_OnFoldedOutboundStep_AttachesByFirstRowId()
|
||||
{
|
||||
|
||||
@@ -52,6 +52,7 @@ public class FlowGraphBuilderTests
|
||||
return [inbound, route, wait, run, write2, write1, cascade];
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an inferred device cascade edge is drawn dashed.</summary>
|
||||
[Fact]
|
||||
public void InferredDeviceCascade_IsDashed()
|
||||
{
|
||||
@@ -65,6 +66,9 @@ public class FlowGraphBuilderTests
|
||||
Assert.True(edge.External);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a folded node uses the latest error detail from its rows.</summary>
|
||||
/// <param name="initialDetail">The error detail on the initial row.</param>
|
||||
/// <param name="finalDetail">The error detail on the final row.</param>
|
||||
[Theory]
|
||||
[InlineData(null, "terminal stack")]
|
||||
[InlineData("old stack", null)]
|
||||
@@ -81,6 +85,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(finalDetail, step.Lifecycle[1].ErrorDetail);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Delmia call shape projects the expected lanes and edges plus a synthesized response.</summary>
|
||||
[Fact]
|
||||
public void DelmiaShape_ProjectsLanes_Edges_AndSynthesizedResponse()
|
||||
{
|
||||
@@ -142,6 +147,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.All(graph.Steps, s => Assert.Equal(FlowStepOutcome.Ok, s.Outcome));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building the graph is deterministic and the synthesized response id is stable.</summary>
|
||||
[Fact]
|
||||
public void Build_IsDeterministic_ResponseIdIsStable()
|
||||
{
|
||||
@@ -152,6 +158,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(a.Edges, b.Edges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the truncated flag passes through to the built graph.</summary>
|
||||
[Fact]
|
||||
public void Truncated_PassesThrough()
|
||||
{
|
||||
@@ -160,6 +167,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Empty(FlowGraphBuilder.Build(Array.Empty<AuditEvent>()).Steps);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unrecognized step kind projects to an unknown node without throwing.</summary>
|
||||
[Fact]
|
||||
public void UnknownKind_ProjectsToUnknown_NeverThrows()
|
||||
{
|
||||
@@ -190,6 +198,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Empty(graph.Edges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a failed script run with a timeout message is classified as timed out.</summary>
|
||||
[Fact]
|
||||
public void FailedScriptRun_WithTimeoutMessage_IsTimedOut()
|
||||
{
|
||||
@@ -205,6 +214,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Single(graph.Edges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the script trace extra payload round-trips as camelCase JSON and ignores unknown members.</summary>
|
||||
[Fact]
|
||||
public void ScriptTraceExtra_RoundTrips_CamelCase_AndIgnoresUnknownMembers()
|
||||
{
|
||||
@@ -219,7 +229,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Null(ScriptTraceExtra.TryParse("nope"));
|
||||
}
|
||||
|
||||
// ---- Outbound calls (2026-09-06 plan, Task 2) --------------------------------------------
|
||||
// ---- Outbound calls (2026-09-06 plan) --------------------------------------------
|
||||
|
||||
private static AuditEvent SiteRunRow(int ms, Guid executionId = default) => ScadaBridgeAuditEventFactory.Create(
|
||||
AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
||||
@@ -240,6 +250,7 @@ public class FlowGraphBuilderTests
|
||||
executionId: SiteRun, sourceSiteId: "site-a", sourceInstanceId: "M1", errorMessage: error, durationMs: durationMs,
|
||||
requestSummary: request, responseSummary: response);
|
||||
|
||||
/// <summary>Verifies that the four rows of a cached API call fold into a single asynchronous node.</summary>
|
||||
[Fact]
|
||||
public void CachedApiCall_FourRows_FoldIntoOneAsyncNode()
|
||||
{
|
||||
@@ -286,6 +297,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(3, graph.Edges.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a synchronous API call sits in the chain without an asynchronous edge.</summary>
|
||||
[Fact]
|
||||
public void SyncApiCall_SitsInChain_NoAsyncEdge()
|
||||
{
|
||||
@@ -309,6 +321,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.DoesNotContain(graph.Edges, e => e.FromEventId == graph.Steps[1].EventId && e.ToEventId == graph.Steps[3].EventId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two synchronous calls with no correlation id are not folded together.</summary>
|
||||
[Fact]
|
||||
public void TwoSyncCalls_NullCorrelation_AreNotFoldedTogether()
|
||||
{
|
||||
@@ -326,6 +339,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(2, graph.Edges.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a notification send and its parked central delivery fold into one parked notify node.</summary>
|
||||
[Fact]
|
||||
public void NotifySend_PlusCentralDeliverParked_FoldsIntoOneParkedNotifyNode()
|
||||
{
|
||||
@@ -354,6 +368,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(notify.EventId, async.ToEventId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cached call with only a submit row is pending and has no duration.</summary>
|
||||
[Fact]
|
||||
public void CachedSubmitOnly_IsPending_WithNoDuration()
|
||||
{
|
||||
@@ -374,6 +389,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(graph.Steps[0].EventId, async.FromEventId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the response links from the last step of the inbound execution rather than the last cascaded step.</summary>
|
||||
[Fact]
|
||||
public void ResponseLinksFromTheInboundExecutionsLastStep_NotTheLastCascadedStep()
|
||||
{
|
||||
@@ -385,6 +401,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(FlowStepKind.RouteCall, graph.Steps[1].Kind);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the response falls back to the inbound row when no other row shares its execution.</summary>
|
||||
[Fact]
|
||||
public void ResponseFallsBackToTheInboundRow_WhenNothingElseSharesItsExecution()
|
||||
{
|
||||
@@ -422,6 +439,7 @@ public class FlowGraphBuilderTests
|
||||
return [inbound, route, run, write];
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with a site clock running ahead, site steps still follow their route call and precede the response.</summary>
|
||||
[Fact]
|
||||
public void SiteClockAhead_SiteStepsStillFollowTheirRouteCall_AndPrecedeResponse()
|
||||
{
|
||||
@@ -440,6 +458,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(T0.AddMilliseconds(900), graph.Steps[4].OccurredAtUtc.UtcDateTime);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with a site clock running behind, site steps do not sort ahead of the inbound request.</summary>
|
||||
[Fact]
|
||||
public void SiteClockBehind_SiteStepsDoNotSortAheadOfTheInboundRequest()
|
||||
{
|
||||
@@ -456,6 +475,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(events.Select(e => e.EventId), graph.Steps.Take(4).Select(s => s.EventId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with a site clock running behind, a trigger cascade spawns from the last parent step rather than the first.</summary>
|
||||
[Fact]
|
||||
public void SiteClockBehind_TriggerCascade_SpawnsFromTheLastParentStep_NotTheFirst()
|
||||
{
|
||||
@@ -477,6 +497,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(run.EventId, spawn.ToEventId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that on a shared clock a trigger cascade spawns from the write that fired it rather than the parent's last step.</summary>
|
||||
[Fact]
|
||||
public void SameClock_TriggerCascade_SpawnsFromTheWriteThatFiredIt_NotTheParentsLastStep()
|
||||
{
|
||||
@@ -502,6 +523,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(cascade.EventId, spawn.ToEventId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with simultaneous writes a trigger cascade spawns from the write of the trigger attribute rather than the last write.</summary>
|
||||
[Fact]
|
||||
public void SimultaneousWrites_TriggerCascade_SpawnsFromTheWriteOfTheTriggerAttribute_NotTheLastWrite()
|
||||
{
|
||||
@@ -524,6 +546,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(cascade.EventId, spawn.ToEventId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a trigger cascade whose attribute the parent never wrote falls back to time-based placement.</summary>
|
||||
[Fact]
|
||||
public void TriggerCascade_WhoseAttributeTheParentNeverWrote_FallsBackToTheTimeRule()
|
||||
{
|
||||
@@ -543,6 +566,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(writeA.EventId, spawn.FromEventId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a folded node takes the emission index of its earliest emitted row.</summary>
|
||||
[Fact]
|
||||
public void FoldedNode_TakesTheEmissionIndexOfItsEarliestEmittedRow()
|
||||
{
|
||||
@@ -562,8 +586,9 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(new[] { FlowStepKind.ApiCall, FlowStepKind.ScriptRun }, graph.Steps.Select(s => s.Kind));
|
||||
}
|
||||
|
||||
// ---- Review findings on Task 2 (2026-09-06) --------------------------------------------
|
||||
// ---- Review findings on outbound calls (2026-09-06) --------------------------------------------
|
||||
|
||||
/// <summary>Verifies that leading asynchronous steps hang off the spawner and are never orphaned.</summary>
|
||||
[Fact]
|
||||
public void LeadingAsyncSteps_HangOffTheSpawner_NeverOrphaned()
|
||||
{
|
||||
@@ -591,6 +616,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(3, graph.Edges.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that when an execution has only notify rows, all of them link from the spawner.</summary>
|
||||
[Fact]
|
||||
public void OnlyNotifyRows_InAnExecution_AllLinkFromTheSpawner()
|
||||
{
|
||||
@@ -606,6 +632,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.All(graph.Edges, e => { Assert.True(e.Async); Assert.True(e.Spawn); Assert.Equal(graph.Steps[0].EventId, e.FromEventId); });
|
||||
}
|
||||
|
||||
/// <summary>Verifies that folding is independent of row order and ties are broken by lifecycle rank.</summary>
|
||||
[Fact]
|
||||
public void Fold_IsOrderIndependent_AndTiesBreakByLifecycleRank()
|
||||
{
|
||||
@@ -628,6 +655,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(sa.Lifecycle!.Select(r => r.Kind), sb.Lifecycle!.Select(r => r.Kind));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cached step whose submit row is missing is still treated as asynchronous.</summary>
|
||||
[Fact]
|
||||
public void CachedStep_WhoseSubmitRowIsMissing_IsStillAsync()
|
||||
{
|
||||
@@ -642,6 +670,7 @@ public class FlowGraphBuilderTests
|
||||
Assert.Single(graph.Edges, e => e.Async && e.ToEventId == api.EventId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that edges on the external lane never count as cluster boundary crossings.</summary>
|
||||
[Fact]
|
||||
public void ExternalLaneEdges_NeverCountAsClusterBoundaryCrossings()
|
||||
{
|
||||
@@ -660,8 +689,9 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(graph.Steps[1].EventId, into.FromEventId);
|
||||
}
|
||||
|
||||
// ---- Script entry point (2026-09-08 plan, Task 6) -----------------------------------------
|
||||
// ---- Script entry point (2026-09-08 plan) -----------------------------------------
|
||||
|
||||
/// <summary>Verifies that a route-call root without an inbound row builds a central-to-site graph with no response.</summary>
|
||||
[Fact]
|
||||
public void RouteCallRoot_WithoutInboundRow_BuildsCentralToSite_AndNoResponse()
|
||||
{
|
||||
|
||||
+11
-1
@@ -5,12 +5,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger step-logs plan (2026-09-09) Task 1: the Commons log-line vocabulary —
|
||||
/// Flow Debugger step-logs plan (2026-09-09): the Commons log-line vocabulary —
|
||||
/// exception detail on a step (D3), and the <c>ScriptTrace.ScriptLog</c> row projecting
|
||||
/// onto a <see cref="FlowLogLine"/> rather than a node (D1).
|
||||
/// </summary>
|
||||
public class FlowStepProjectorLogLineTests
|
||||
{
|
||||
/// <summary>Verifies that projecting an audit event carries its error detail onto the step.</summary>
|
||||
[Fact]
|
||||
public void Project_CarriesErrorDetail()
|
||||
{
|
||||
@@ -23,6 +24,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.StartsWith("System.InvalidOperationException", step.ErrorDetail);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the projected error detail is null when the audit event has none.</summary>
|
||||
[Fact]
|
||||
public void Project_ErrorDetail_IsNullWhenAbsent()
|
||||
{
|
||||
@@ -31,6 +33,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.Null(FlowStepProjector.Project(evt).ErrorDetail);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script log row projects to the unknown kind and never to a step kind of its own.</summary>
|
||||
[Fact]
|
||||
public void ScriptLog_ProjectsToUnknownKind_NeverItsOwnStepKind()
|
||||
{
|
||||
@@ -40,6 +43,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.False(Enum.GetNames<FlowStepKind>().Contains("ScriptLog"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a row is a log line only for the script trace channel with the script log kind.</summary>
|
||||
[Fact]
|
||||
public void IsLogLine_TrueOnlyForScriptTraceScriptLog()
|
||||
{
|
||||
@@ -50,6 +54,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.False(FlowStepProjector.IsLogLine(run));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that rows on other channels are not log lines.</summary>
|
||||
[Fact]
|
||||
public void IsLogLine_FalseForOtherChannels()
|
||||
{
|
||||
@@ -58,6 +63,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.False(FlowStepProjector.IsLogLine(outbound));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that converting a row to a log line reads its level, source and exception.</summary>
|
||||
[Fact]
|
||||
public void ToLogLine_ReadsLevelSourceAndException()
|
||||
{
|
||||
@@ -81,6 +87,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.Equal(3, line.Seq);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that converting a row to a log line defaults the level and source when the extra payload is missing.</summary>
|
||||
[Fact]
|
||||
public void ToLogLine_DefaultsLevelAndSourceWhenExtraMissing()
|
||||
{
|
||||
@@ -97,6 +104,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.Null(line.Seq);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the log members of the script trace extra round-trip through JSON.</summary>
|
||||
[Fact]
|
||||
public void ScriptTraceExtra_LogMembers_RoundTripThroughJson()
|
||||
{
|
||||
@@ -111,6 +119,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.Equal(12, parsed.Seq);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a flow step's logs and error detail default to null.</summary>
|
||||
[Fact]
|
||||
public void FlowStep_LogsAndErrorDetail_DefaultToNull()
|
||||
{
|
||||
@@ -124,6 +133,7 @@ public class FlowStepProjectorLogLineTests
|
||||
Assert.Single((step with { Logs = [line] }).Logs!);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the log truncation members of a flow graph default to off.</summary>
|
||||
[Fact]
|
||||
public void FlowGraph_LogTruncationMembers_DefaultToOff()
|
||||
{
|
||||
|
||||
@@ -6,13 +6,14 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger outbound-calls plan (2026-09-06) Task 0: the Commons vocabulary — External lane,
|
||||
/// Flow Debugger outbound-calls plan (2026-09-06): the Commons vocabulary — External lane,
|
||||
/// the three outbound kinds, Pending/Parked outcomes, the lifecycle row, and additive edge/step shapes.
|
||||
/// </summary>
|
||||
public class FlowStepProjectorTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
/// <summary>Verifies that the vocabulary has an External lane, three outbound kinds, and Pending and Parked outcomes.</summary>
|
||||
[Fact]
|
||||
public void Vocabulary_HasExternalLane_ThreeOutboundKinds_AndPendingParkedOutcomes()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public class FlowStepProjectorTests
|
||||
Assert.True(Enum.IsDefined(FlowStepOutcome.Parked));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a flow step can be constructed without a lifecycle and defaults it to null.</summary>
|
||||
[Fact]
|
||||
public void FlowStep_WithoutLifecycle_StillConstructs_AndDefaultsToNull()
|
||||
{
|
||||
@@ -39,6 +41,7 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal("Submitted", withLifecycle.Lifecycle![0].Status);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Async and Spawn flags of a flow edge default to false.</summary>
|
||||
[Fact]
|
||||
public void FlowEdge_AsyncAndSpawn_DefaultFalse()
|
||||
{
|
||||
@@ -51,8 +54,9 @@ public class FlowStepProjectorTests
|
||||
Assert.True(new FlowEdge(a, b, false, false, Async: false, Spawn: true).Spawn);
|
||||
}
|
||||
|
||||
// ---- Task 1: outbound rows project onto the External lane ---------------------------------
|
||||
// ---- Outbound rows project onto the External lane ---------------------------------
|
||||
|
||||
/// <summary>Verifies that a delivered synchronous API call projects to an Ok API-call step on the External lane with a sync subtitle.</summary>
|
||||
[Fact]
|
||||
public void SyncApiCall_Delivered_IsApiCall_External_Ok_SyncSubtitle()
|
||||
{
|
||||
@@ -73,6 +77,7 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal(212, step.DurationMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cached submit on the database outbound channel projects to a pending database-call step with a cached subtitle.</summary>
|
||||
[Fact]
|
||||
public void CachedSubmit_InDbOutbound_IsDbCall_Pending_CachedSubtitle()
|
||||
{
|
||||
@@ -87,6 +92,7 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal("Submitted · cached", step.Subtitle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a database write with a rows-affected extra has a subtitle combining the operation and the row count.</summary>
|
||||
[Fact]
|
||||
public void DbWrite_WithRowsAffectedExtra_SubtitleIsOpAndRows()
|
||||
{
|
||||
@@ -105,6 +111,7 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal("read · 1 row", FlowStepProjector.Project(read).Subtitle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a submitted notification send projects to a pending notify step.</summary>
|
||||
[Fact]
|
||||
public void NotifySend_Submitted_IsNotify_Pending()
|
||||
{
|
||||
@@ -120,6 +127,9 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal("Submitted", step.Subtitle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each outbound audit status maps to the expected flow step outcome.</summary>
|
||||
/// <param name="status">The outbound audit status.</param>
|
||||
/// <param name="expected">The expected flow step outcome.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Parked, FlowStepOutcome.Parked)]
|
||||
[InlineData(AuditStatus.Discarded, FlowStepOutcome.Parked)]
|
||||
@@ -139,6 +149,7 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal($"{status} · cached", step.Subtitle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that script-trace outcomes are unchanged, so a failed status stays failed and other statuses are Ok.</summary>
|
||||
[Fact]
|
||||
public void ScriptTraceOutcome_Unchanged_FailedStaysFailed_OtherStatusesOk()
|
||||
{
|
||||
@@ -150,6 +161,9 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal(FlowStepOutcome.Failed, FlowStepProjector.Project(failed).Outcome);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building a database subtitle never throws on odd extra values and yields the expected text.</summary>
|
||||
/// <param name="extra">The extra payload of the audit row.</param>
|
||||
/// <param name="expected">The expected subtitle.</param>
|
||||
[Theory]
|
||||
[InlineData("{\"op\":\"write\",\"rowsAffected\":2147483648}", "write")]
|
||||
[InlineData("{\"op\":\"write\",\"rowsAffected\":1.5}", "write")]
|
||||
@@ -163,6 +177,7 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal(expected, FlowStepProjector.Project(evt).Subtitle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that projection carries the audit row's correlation id onto the step.</summary>
|
||||
[Fact]
|
||||
public void Project_CarriesTheAuditRowsCorrelationId()
|
||||
{
|
||||
@@ -173,6 +188,7 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal(corr, FlowStepProjector.Project(evt).CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null correlation id on the audit row leaves the step's correlation id null.</summary>
|
||||
[Fact]
|
||||
public void Project_NullCorrelation_LeavesCorrelationIdNull()
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ public class FlowTraceCollectorTests
|
||||
AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
||||
target: "S", requestSummary: details);
|
||||
|
||||
/// <summary>Verifies that add, when beyond cap, keeps first n and flags truncated.</summary>
|
||||
[Fact]
|
||||
public void Add_BeyondCap_KeepsFirstN_AndFlagsTruncated()
|
||||
{
|
||||
@@ -39,6 +40,7 @@ public class FlowTraceCollectorTests
|
||||
Assert.Equal("499", snap[499].Target);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that snapshot, when is a copy, in emission order.</summary>
|
||||
[Fact]
|
||||
public void Snapshot_IsACopy_InEmissionOrder()
|
||||
{
|
||||
@@ -52,6 +54,8 @@ public class FlowTraceCollectorTests
|
||||
Assert.Equal(new[] { "1", "2" }, c.Snapshot().Select(e => e.Target));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent adds, when never throw, and never exceed cap.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ConcurrentAdds_NeverThrow_AndNeverExceedCap()
|
||||
{
|
||||
@@ -66,6 +70,7 @@ public class FlowTraceCollectorTests
|
||||
Assert.True(c.Truncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that default cap, when is 500, and non-positive cap is rejected.</summary>
|
||||
[Fact]
|
||||
public void DefaultCap_Is500_AndNonPositiveCapIsRejected()
|
||||
{
|
||||
@@ -73,6 +78,7 @@ public class FlowTraceCollectorTests
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new FlowTraceCollector(0));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that complete, when closes the collector, later adds are ignored and never truncate.</summary>
|
||||
[Fact]
|
||||
public void Complete_ClosesTheCollector_LaterAddsAreIgnored_AndNeverTruncate()
|
||||
{
|
||||
@@ -198,6 +204,9 @@ public class FlowTraceCollectorTests
|
||||
/// <summary>Upper-cases <c>DetailsJson</c> so a test can see the filtered copy land.</summary>
|
||||
private sealed class UpperRedactor : IAuditRedactor
|
||||
{
|
||||
/// <summary>Applies the upper-casing redaction.</summary>
|
||||
/// <param name="rawEvent">The raw event.</param>
|
||||
/// <returns>The event with upper-cased details.</returns>
|
||||
public AuditEvent Apply(AuditEvent rawEvent)
|
||||
=> rawEvent with { DetailsJson = rawEvent.DetailsJson?.ToUpperInvariant() };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests;
|
||||
|
||||
public class DeploymentFetchTokenTests
|
||||
{
|
||||
/// <summary>Verifies that generate, returns url safe token of adequate length.</summary>
|
||||
[Fact]
|
||||
public void Generate_ReturnsUrlSafeTokenOfAdequateLength()
|
||||
{
|
||||
@@ -14,6 +15,7 @@ public class DeploymentFetchTokenTests
|
||||
Assert.DoesNotContain('/', token);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that generate, two calls return different values.</summary>
|
||||
[Fact]
|
||||
public void Generate_TwoCallsReturnDifferentValues()
|
||||
{
|
||||
@@ -23,6 +25,7 @@ public class DeploymentFetchTokenTests
|
||||
Assert.NotEqual(a, b);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constant time equals, same token, returns true.</summary>
|
||||
[Fact]
|
||||
public void ConstantTimeEquals_SameToken_ReturnsTrue()
|
||||
{
|
||||
@@ -31,6 +34,7 @@ public class DeploymentFetchTokenTests
|
||||
Assert.True(DeploymentFetchToken.ConstantTimeEquals(token, token));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constant time equals, token with suffix, returns false.</summary>
|
||||
[Fact]
|
||||
public void ConstantTimeEquals_TokenWithSuffix_ReturnsFalse()
|
||||
{
|
||||
@@ -39,6 +43,7 @@ public class DeploymentFetchTokenTests
|
||||
Assert.False(DeploymentFetchToken.ConstantTimeEquals(token, token + "x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constant time equals, both empty, returns true.</summary>
|
||||
[Fact]
|
||||
public void ConstantTimeEquals_BothEmpty_ReturnsTrue()
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ public class SiteCallTests
|
||||
IngestedAtUtc = new DateTime(2026, 5, 23, 12, 0, 1, DateTimeKind.Utc),
|
||||
};
|
||||
|
||||
/// <summary>Verifies that a site call carries its source node.</summary>
|
||||
[Fact]
|
||||
public void SiteCall_carries_SourceNode()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ public class EntityConventionTests
|
||||
.Where(t => t.IsClass && !t.IsAbstract && t.Namespace != null
|
||||
&& t.Namespace.Contains(".Entities."));
|
||||
|
||||
/// <summary>Verifies that no entity class carries Entity Framework attributes.</summary>
|
||||
[Fact]
|
||||
public void AllEntities_ShouldHaveNoEfAttributes()
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public class EntityConventionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that every timestamp property on an entity is a DateTimeOffset.</summary>
|
||||
[Fact]
|
||||
public void AllTimestampProperties_ShouldBeDateTimeOffset()
|
||||
{
|
||||
@@ -68,6 +70,7 @@ public class EntityConventionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that collection navigation properties on entities are declared as ICollection.</summary>
|
||||
[Fact]
|
||||
public void NavigationProperties_ShouldBeICollection()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Entities;
|
||||
/// </summary>
|
||||
public class ExternalSystemDefinitionTests
|
||||
{
|
||||
/// <summary>Verifies that the timeout seconds default to zero, meaning the gateway default is used.</summary>
|
||||
[Fact]
|
||||
public void ExternalSystemDefinition_TimeoutSeconds_DefaultsToZeroMeaningUseGatewayDefault()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Entities;
|
||||
|
||||
public class NativeAlarmSourceEntityTests
|
||||
{
|
||||
/// <summary>Verifies that a template native alarm source is constructed with its required name.</summary>
|
||||
[Fact]
|
||||
public void TemplateNativeAlarmSource_RequiresName()
|
||||
{
|
||||
@@ -13,6 +14,7 @@ public class NativeAlarmSourceEntityTests
|
||||
Assert.False(s.IsLocked);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a flattened configuration has an empty native alarm source list by default.</summary>
|
||||
[Fact]
|
||||
public void FlattenedConfiguration_HasNativeAlarmSourcesDefaultEmpty()
|
||||
{
|
||||
@@ -20,6 +22,7 @@ public class NativeAlarmSourceEntityTests
|
||||
Assert.Empty(f.NativeAlarmSources);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a resolved native alarm source has a default source template value.</summary>
|
||||
[Fact]
|
||||
public void ResolvedNativeAlarmSource_DefaultsSourceTemplate()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Entities;
|
||||
/// </summary>
|
||||
public class NotificationEntityTests
|
||||
{
|
||||
/// <summary>Verifies that the constructor sets the expected default values.</summary>
|
||||
[Fact]
|
||||
public void Constructor_SetsDefaults()
|
||||
{
|
||||
@@ -21,10 +22,11 @@ public class NotificationEntityTests
|
||||
Assert.Equal("SiteA", n.SourceSiteId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that OriginExecutionId defaults to null and can be set.</summary>
|
||||
[Fact]
|
||||
public void OriginExecutionId_DefaultsToNull_AndIsSettable()
|
||||
{
|
||||
// Audit Log #23: OriginExecutionId carries the originating script
|
||||
// OriginExecutionId carries the originating script
|
||||
// execution's id from the site so the dispatcher can echo it onto
|
||||
// NotifyDeliver rows. Null for notifications submitted before the
|
||||
// column existed; settable from the NotificationSubmit message.
|
||||
@@ -36,6 +38,7 @@ public class NotificationEntityTests
|
||||
Assert.Equal(executionId, n.OriginExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that OriginParentExecutionId defaults to null and can be set.</summary>
|
||||
[Fact]
|
||||
public void OriginParentExecutionId_DefaultsToNull_AndIsSettable()
|
||||
{
|
||||
@@ -51,6 +54,7 @@ public class NotificationEntityTests
|
||||
Assert.Equal(parentExecutionId, n.OriginParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SourceNode defaults to null and can be set.</summary>
|
||||
[Fact]
|
||||
public void SourceNode_DefaultsToNull_AndIsSettable()
|
||||
{
|
||||
@@ -65,6 +69,7 @@ public class NotificationEntityTests
|
||||
Assert.Equal("node-a", n.SourceNode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the constructor throws for null arguments.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullArguments_Throw()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Entities;
|
||||
/// </summary>
|
||||
public class NotificationRecipientTests
|
||||
{
|
||||
/// <summary>Verifies that creating an email recipient sets the email address and leaves the phone number null.</summary>
|
||||
[Fact]
|
||||
public void ForEmail_SetsEmail_LeavesPhoneNull()
|
||||
{
|
||||
@@ -18,6 +19,7 @@ public class NotificationRecipientTests
|
||||
Assert.Null(r.PhoneNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an SMS recipient sets the phone number and leaves the email address null.</summary>
|
||||
[Fact]
|
||||
public void ForSms_SetsPhone_LeavesEmailNull()
|
||||
{
|
||||
@@ -28,6 +30,8 @@ public class NotificationRecipientTests
|
||||
Assert.Null(r.EmailAddress);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an email recipient with a blank name throws.</summary>
|
||||
/// <param name="name">The blank recipient name.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -35,6 +39,8 @@ public class NotificationRecipientTests
|
||||
public void ForEmail_BlankName_Throws(string? name)
|
||||
=> Assert.Throws<ArgumentException>(() => NotificationRecipient.ForEmail(name!, "jane@example.com"));
|
||||
|
||||
/// <summary>Verifies that creating an SMS recipient with a blank name throws.</summary>
|
||||
/// <param name="name">The blank recipient name.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -42,14 +48,18 @@ public class NotificationRecipientTests
|
||||
public void ForSms_BlankName_Throws(string? name)
|
||||
=> Assert.Throws<ArgumentException>(() => NotificationRecipient.ForSms(name!, "+15551234567"));
|
||||
|
||||
/// <summary>Verifies that creating an email recipient with a null email address throws.</summary>
|
||||
[Fact]
|
||||
public void ForEmail_NullEmail_Throws()
|
||||
=> Assert.Throws<ArgumentNullException>(() => NotificationRecipient.ForEmail("Jane", null!));
|
||||
|
||||
/// <summary>Verifies that creating an SMS recipient with a null phone number throws.</summary>
|
||||
[Fact]
|
||||
public void ForSms_NullPhone_Throws()
|
||||
=> Assert.Throws<ArgumentNullException>(() => NotificationRecipient.ForSms("Jane", null!));
|
||||
|
||||
/// <summary>Verifies that the public constructor throws for a blank name.</summary>
|
||||
/// <param name="name">The blank recipient name.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -57,6 +67,7 @@ public class NotificationRecipientTests
|
||||
public void PublicCtor_BlankName_Throws(string? name)
|
||||
=> Assert.Throws<ArgumentException>(() => new NotificationRecipient(name!, "jane@example.com"));
|
||||
|
||||
/// <summary>Verifies that the public constructor throws for a null email address.</summary>
|
||||
[Fact]
|
||||
public void PublicCtor_NullEmail_Throws()
|
||||
=> Assert.Throws<ArgumentNullException>(() => new NotificationRecipient("Jane", null!));
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Entities;
|
||||
|
||||
public class TemplateFolderTests
|
||||
{
|
||||
/// <summary>Verifies that the constructor sets the folder name.</summary>
|
||||
[Fact]
|
||||
public void Constructor_SetsName()
|
||||
{
|
||||
@@ -15,12 +16,14 @@ public class TemplateFolderTests
|
||||
Assert.Equal(0, folder.SortOrder);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the constructor throws when the name is null.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullName_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new TemplateFolder(null!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a template's folder id defaults to null.</summary>
|
||||
[Fact]
|
||||
public void Template_FolderId_DefaultsToNull()
|
||||
{
|
||||
|
||||
+5
-1
@@ -5,12 +5,14 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Interfaces.Protocol;
|
||||
/// <summary>
|
||||
/// Single-source-of-truth coverage for <see cref="AlarmCapableProtocols.IsAlarmCapable"/>.
|
||||
/// The OpcUa/MxGateway literal set previously lived inline in the Central UI
|
||||
/// (TemplateEdit / InstanceConfigure); #53 collapsed those copies into this helper,
|
||||
/// (TemplateEdit / InstanceConfigure); those copies were collapsed into this helper,
|
||||
/// so the literal set is pinned here rather than against page markup. Keep this in
|
||||
/// sync with the alarm-subscribable adapters registered in DataConnectionFactory.
|
||||
/// </summary>
|
||||
public class AlarmCapableProtocolsTests
|
||||
{
|
||||
/// <summary>Verifies that alarm-subscribable protocols are reported as alarm capable.</summary>
|
||||
/// <param name="protocol">The protocol name under test.</param>
|
||||
[Theory]
|
||||
[InlineData("OpcUa")]
|
||||
[InlineData("opcua")] // case-insensitive — matches DataConnectionFactory's OrdinalIgnoreCase key lookup
|
||||
@@ -22,6 +24,8 @@ public class AlarmCapableProtocolsTests
|
||||
Assert.True(AlarmCapableProtocols.IsAlarmCapable(protocol));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-alarm-capable, null or blank protocols are not reported as alarm capable.</summary>
|
||||
/// <param name="protocol">The protocol name under test.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
|
||||
+4
-1
@@ -6,11 +6,13 @@ using ICentralAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IC
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Reflection-level contract guards for the Audit Log (#23) writer interfaces.
|
||||
/// Reflection-level contract guards for the Audit Log writer interfaces.
|
||||
/// Locks in method signature so DI bindings + adapter implementations stay aligned.
|
||||
/// </summary>
|
||||
public class AuditWriterContractTests
|
||||
{
|
||||
/// <summary>Verifies that the writer interface exposes <c>WriteAsync</c> with the expected signature.</summary>
|
||||
/// <param name="writerType">The writer interface type under test.</param>
|
||||
[Theory]
|
||||
[InlineData(typeof(IAuditWriter))]
|
||||
[InlineData(typeof(ICentralAuditWriter))]
|
||||
@@ -27,6 +29,7 @@ public class AuditWriterContractTests
|
||||
Assert.True(parameters[1].HasDefaultValue);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the site and central audit writer interfaces are distinct types.</summary>
|
||||
[Fact]
|
||||
public void IAuditWriter_AndICentralAuditWriter_AreDistinctTypes()
|
||||
{
|
||||
|
||||
+4
-2
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ExternalCallResult"/>, in particular the Commons-021
|
||||
/// Tests for <see cref="ExternalCallResult"/>, in particular the
|
||||
/// thread-safe lazy parse of <c>Response</c>. The pre-fix implementation used
|
||||
/// two mutable fields (<c>_response</c>/<c>_responseParsed</c>) with no
|
||||
/// synchronization, so concurrent readers could each construct a fresh
|
||||
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Interfaces.Services;
|
||||
/// </summary>
|
||||
public class ExternalCallResultTests
|
||||
{
|
||||
/// <summary>Verifies that a null or empty response JSON yields a null response.</summary>
|
||||
[Fact]
|
||||
public void Response_NullOrEmptyJson_ReturnsNull()
|
||||
{
|
||||
@@ -24,6 +25,7 @@ public class ExternalCallResultTests
|
||||
Assert.Null(withEmpty.Response);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the response JSON is parsed into a dynamic element.</summary>
|
||||
[Fact]
|
||||
public void Response_ParsesJsonIntoDynamicElement()
|
||||
{
|
||||
@@ -37,7 +39,7 @@ public class ExternalCallResultTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commons-021: concurrent readers must observe the same parsed instance
|
||||
/// Concurrent readers must observe the same parsed instance
|
||||
/// (a `Lazy<T>` invariant). Under the pre-fix code two threads could
|
||||
/// both produce a fresh `DynamicJsonElement` and one would win the race —
|
||||
/// `ReferenceEquals` would then occasionally fail. With the fix every
|
||||
|
||||
@@ -86,15 +86,24 @@ public class KpiMetricAggregationTests
|
||||
(KpiSources.SiteHealth, "localDbReplicationConnected"),
|
||||
};
|
||||
|
||||
/// <summary>Supplies every emitted source and metric pair as theory data.</summary>
|
||||
/// <returns>The theory data rows of source and metric names.</returns>
|
||||
public static IEnumerable<object[]> AllEmittedCases() =>
|
||||
AllEmittedMetrics.Select(m => new object[] { m.Source, m.Metric });
|
||||
|
||||
/// <summary>Supplies the rate metric source and metric pairs as theory data.</summary>
|
||||
/// <returns>The theory data rows of source and metric names.</returns>
|
||||
public static IEnumerable<object[]> RateCases() =>
|
||||
RateMetrics.Select(m => new object[] { m.Source, m.Metric });
|
||||
|
||||
/// <summary>Supplies the gauge-sampled source and metric pairs as theory data.</summary>
|
||||
/// <returns>The theory data rows of source and metric names.</returns>
|
||||
public static IEnumerable<object[]> GaugeCases() =>
|
||||
GaugeSampling.Select(m => new object[] { m.Source, m.Metric });
|
||||
|
||||
/// <summary>Verifies that every emitted metric resolves to a defined aggregation.</summary>
|
||||
/// <param name="source">The KPI source name.</param>
|
||||
/// <param name="metric">The metric name.</param>
|
||||
[Theory]
|
||||
[MemberData(nameof(AllEmittedCases))]
|
||||
public void Resolve_EveryEmittedMetric_ReturnsDefinedAggregation(string source, string metric)
|
||||
@@ -106,6 +115,9 @@ public class KpiMetricAggregationTests
|
||||
$"({source}, {metric}) resolved to an undefined aggregation: {result}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that rate metrics are classified as rate.</summary>
|
||||
/// <param name="source">The KPI source name.</param>
|
||||
/// <param name="metric">The metric name.</param>
|
||||
[Theory]
|
||||
[MemberData(nameof(RateCases))]
|
||||
public void Resolve_RateMetrics_ClassifiedAsRate(string source, string metric)
|
||||
@@ -113,6 +125,9 @@ public class KpiMetricAggregationTests
|
||||
Assert.Equal(KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(source, metric));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that gauge-sampled metrics are classified as gauge.</summary>
|
||||
/// <param name="source">The KPI source name.</param>
|
||||
/// <param name="metric">The metric name.</param>
|
||||
[Theory]
|
||||
[MemberData(nameof(GaugeCases))]
|
||||
public void Resolve_GaugeSampling_ClassifiedAsGauge(string source, string metric)
|
||||
@@ -120,6 +135,7 @@ public class KpiMetricAggregationTests
|
||||
Assert.Equal(KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(source, metric));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that last-hour audit metrics resolve as gauges rather than rates.</summary>
|
||||
[Fact]
|
||||
public void Resolve_LastHourAuditMetrics_AreGaugeNotRate()
|
||||
{
|
||||
@@ -133,6 +149,9 @@ public class KpiMetricAggregationTests
|
||||
KpiMetricAggregationCatalog.Resolve(KpiSources.AuditLog, "errorEventsLastHour"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown metric defaults to a gauge.</summary>
|
||||
/// <param name="source">The KPI source name.</param>
|
||||
/// <param name="metric">The metric name.</param>
|
||||
[Theory]
|
||||
[InlineData(KpiSources.NotificationOutbox, "someBrandNewMetricAddedLater")]
|
||||
[InlineData("AnEntirelyNewSource", "anyMetric")]
|
||||
@@ -144,6 +163,7 @@ public class KpiMetricAggregationTests
|
||||
Assert.Equal(KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(source, metric));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the same metric name under a different source is keyed on the source and metric pair.</summary>
|
||||
[Fact]
|
||||
public void Resolve_SameMetricNameDifferentSource_KeysOnSourcePair()
|
||||
{
|
||||
@@ -157,8 +177,9 @@ public class KpiMetricAggregationTests
|
||||
KpiMetricAggregationCatalog.Resolve(KpiSources.SiteCallAudit, "deliveredLastInterval"));
|
||||
}
|
||||
|
||||
// ---- Task 8: catalog metric literals promoted to shared KpiMetrics constants (R4) ----
|
||||
// ---- Catalog metric literals promoted to shared KpiMetrics constants (R4) ----
|
||||
|
||||
/// <summary>Verifies that the promoted metric constants lock the historical persisted values.</summary>
|
||||
[Fact]
|
||||
public void PromotedMetricConstants_LockHistoricalPersistedValues()
|
||||
{
|
||||
@@ -168,6 +189,9 @@ public class KpiMetricAggregationTests
|
||||
Assert.Equal("eventLogWriteFailures", KpiMetrics.SiteHealth.EventLogWriteFailures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the promoted rate pairs still classify as rate.</summary>
|
||||
/// <param name="source">The KPI source name.</param>
|
||||
/// <param name="metric">The metric name.</param>
|
||||
[Theory]
|
||||
[InlineData(KpiSources.SiteCallAudit, KpiMetrics.SiteCallAudit.DeliveredLastInterval)]
|
||||
[InlineData(KpiSources.SiteHealth, KpiMetrics.SiteHealth.AlarmEvalErrors)]
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Kpi;
|
||||
|
||||
public class KpiSampleTests
|
||||
{
|
||||
/// <summary>Verifies that a KpiSample constructed with its required members round-trips the values.</summary>
|
||||
[Fact]
|
||||
public void KpiSample_ConstructedWithRequiredMembers_RoundTripsValues()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ public class KpiSampleTests
|
||||
Assert.Equal(DateTimeKind.Utc, sample.CapturedAtUtc.Kind);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a scoped sample carries its scope key.</summary>
|
||||
[Fact]
|
||||
public void KpiSample_ScopedSample_CarriesScopeKey()
|
||||
{
|
||||
@@ -49,6 +51,7 @@ public class KpiSampleTests
|
||||
Assert.Equal(12.5, sample.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each KPI scope and source constant equals its own name.</summary>
|
||||
[Fact]
|
||||
public void KpiScopesAndSources_ConstantValues_EqualTheirNames()
|
||||
{
|
||||
@@ -62,6 +65,7 @@ public class KpiSampleTests
|
||||
Assert.Equal("Node", KpiScopes.Node);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that KpiSeriesPoint is a value record struct with component-wise equality.</summary>
|
||||
[Fact]
|
||||
public void KpiSeriesPoint_IsValueRecordStruct_WithComponentEquality()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ public class KpiSeriesBucketerTests
|
||||
// Guard / invalid-argument contracts
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a maximum point count below two throws an ArgumentOutOfRangeException.</summary>
|
||||
[Fact]
|
||||
public void Bucket_MaxPointsLessThan2_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
@@ -25,6 +26,7 @@ public class KpiSeriesBucketerTests
|
||||
KpiSeriesBucketer.Bucket(raw, T(0), T(60), maxPoints: 1));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a maximum point count of zero throws an ArgumentOutOfRangeException.</summary>
|
||||
[Fact]
|
||||
public void Bucket_MaxPointsZero_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
@@ -34,6 +36,7 @@ public class KpiSeriesBucketerTests
|
||||
KpiSeriesBucketer.Bucket(raw, T(0), T(60), maxPoints: 0));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a window whose end equals its start throws an ArgumentOutOfRangeException.</summary>
|
||||
[Fact]
|
||||
public void Bucket_ToUtcEqualFromUtc_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
@@ -43,6 +46,7 @@ public class KpiSeriesBucketerTests
|
||||
KpiSeriesBucketer.Bucket(raw, T(30), T(30), maxPoints: 5));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a window that ends before it starts throws an ArgumentOutOfRangeException.</summary>
|
||||
[Fact]
|
||||
public void Bucket_ToUtcBeforeFromUtc_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
@@ -56,6 +60,7 @@ public class KpiSeriesBucketerTests
|
||||
// Empty / null raw series
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a null raw series returns an empty list.</summary>
|
||||
[Fact]
|
||||
public void Bucket_NullRaw_ReturnsEmptyList()
|
||||
{
|
||||
@@ -64,6 +69,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty raw series returns an empty list.</summary>
|
||||
[Fact]
|
||||
public void Bucket_EmptyRaw_ReturnsEmptyList()
|
||||
{
|
||||
@@ -77,6 +83,7 @@ public class KpiSeriesBucketerTests
|
||||
// raw.Count <= maxPoints → returned unchanged (same reference)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a raw series with exactly the maximum number of points is returned as the same reference.</summary>
|
||||
[Fact]
|
||||
public void Bucket_RawCountEqualsMaxPoints_ReturnsSameReference()
|
||||
{
|
||||
@@ -92,6 +99,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Same(raw, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a raw series with fewer points than the maximum is returned as the same reference.</summary>
|
||||
[Fact]
|
||||
public void Bucket_RawCountLessThanMaxPoints_ReturnsSameReference()
|
||||
{
|
||||
@@ -106,6 +114,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Same(raw, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a single-point series is returned as the same reference.</summary>
|
||||
[Fact]
|
||||
public void Bucket_SinglePoint_ReturnsSameReference()
|
||||
{
|
||||
@@ -120,6 +129,7 @@ public class KpiSeriesBucketerTests
|
||||
// Downsampling: correct bucket count and last-value selection
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a series with more points than the maximum is reduced to the maximum number of buckets.</summary>
|
||||
[Fact]
|
||||
public void Bucket_MorePointsThanMaxPoints_ReducesToMaxPointsBuckets()
|
||||
{
|
||||
@@ -140,6 +150,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Equal(30.0, result[2].Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that when several points fall in the same bucket the last value wins.</summary>
|
||||
[Fact]
|
||||
public void Bucket_MultiplePointsInSameBucket_LastValueWins()
|
||||
{
|
||||
@@ -160,6 +171,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Equal(5.0, result[1].Value); // only point in bucket 1
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each output point carries the bucket start rather than the raw point timestamp.</summary>
|
||||
[Fact]
|
||||
public void Bucket_BucketStartUtc_IsSetToBucketStartNotRawPointTimestamp()
|
||||
{
|
||||
@@ -187,10 +199,11 @@ public class KpiSeriesBucketerTests
|
||||
// Unsorted input: last-in-iteration wins within a bucket (NOT largest timestamp)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that for unsorted input the last point in iteration order is selected, not the point with the largest timestamp.</summary>
|
||||
[Fact]
|
||||
public void Bucket_UnsortedInput_SelectsLastInIterationNotLargestTimestamp()
|
||||
{
|
||||
// KpiHistory-006 regression: pins the documented contract that for unsorted input the
|
||||
// Regression: pins the documented contract that for unsorted input the
|
||||
// bucketer selects the LAST point in iteration order within a bucket — it does NOT pick
|
||||
// the largest-timestamp point. Both points below fall in bucket 0 ([0, 30)); the later
|
||||
// one in iteration order, T(5)=value 1.0, arrives second, so it overwrites T(20)=value
|
||||
@@ -216,10 +229,11 @@ public class KpiSeriesBucketerTests
|
||||
// Short series (raw.Count <= maxPoints): returned unchanged → raw capture timestamps
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a short series keeps its raw capture timestamps instead of bucket boundaries.</summary>
|
||||
[Fact]
|
||||
public void Bucket_ShortSeries_ReturnsRawCaptureTimestampsNotBucketBoundaries()
|
||||
{
|
||||
// KpiHistory-005/003 regression: the short-series early-return path returns the input
|
||||
// Regression: the short-series early-return path returns the input
|
||||
// unchanged, so each output point's BucketStartUtc is the RAW capture timestamp — NOT a
|
||||
// bucket-boundary timestamp (which is what the downsample path emits, asserted by
|
||||
// Bucket_BucketStartUtc_IsSetToBucketStartNotRawPointTimestamp). This pins the
|
||||
@@ -243,6 +257,7 @@ public class KpiSeriesBucketerTests
|
||||
// Right-edge: point exactly at toUtc lands in the last bucket
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a point exactly at the window end lands in the last bucket.</summary>
|
||||
[Fact]
|
||||
public void Bucket_PointAtToUtc_LandsInLastBucket()
|
||||
{
|
||||
@@ -264,6 +279,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Equal(7.0, result[1].Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that in a three-bucket window a point exactly at the window end lands in the last bucket.</summary>
|
||||
[Fact]
|
||||
public void Bucket_ThreeBucketWindow_PointAtToUtc_LandsInLastBucket()
|
||||
{
|
||||
@@ -291,6 +307,7 @@ public class KpiSeriesBucketerTests
|
||||
// Empty buckets omitted — no gap-filling
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that empty buckets caused by a gap in the raw data are omitted.</summary>
|
||||
[Fact]
|
||||
public void Bucket_GapInRaw_EmptyBucketsOmitted()
|
||||
{
|
||||
@@ -329,6 +346,7 @@ public class KpiSeriesBucketerTests
|
||||
// Points outside [fromUtc, toUtc] are ignored
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that points outside the window are ignored.</summary>
|
||||
[Fact]
|
||||
public void Bucket_PointsOutsideWindow_AreIgnored()
|
||||
{
|
||||
@@ -353,6 +371,7 @@ public class KpiSeriesBucketerTests
|
||||
// Two-point minimum — maxPoints == 2 works correctly
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a maximum of exactly two points produces at most two buckets.</summary>
|
||||
[Fact]
|
||||
public void Bucket_MaxPointsExactly2_ProducesAtMostTwoBuckets()
|
||||
{
|
||||
@@ -374,6 +393,7 @@ public class KpiSeriesBucketerTests
|
||||
// Per-metric reduction: Rate series sum per bucket (arch-review 04 round 2, R3)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a rate series sums the values in each bucket instead of taking the last value.</summary>
|
||||
[Fact]
|
||||
public void Bucket_RateSeries_SumsPerBucket_InsteadOfLastValue()
|
||||
{
|
||||
@@ -397,6 +417,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Equal(30.0, result[1].Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a short rate series still sums clustered points.</summary>
|
||||
[Fact]
|
||||
public void Bucket_RateSeries_ShortSeries_StillSumsClusteredPoints()
|
||||
{
|
||||
@@ -418,6 +439,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Equal(10.0, result[1].Value); // T(30) alone
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bucketing a rate series preserves the series total.</summary>
|
||||
[Fact]
|
||||
public void Bucket_RateSeries_PreservesSeriesTotal()
|
||||
{
|
||||
@@ -436,6 +458,7 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Equal(inWindowTotal, result.Sum(p => p.Value));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the default aggregation is gauge and its behavior is unchanged.</summary>
|
||||
[Fact]
|
||||
public void Bucket_DefaultAggregation_IsGauge_AndBehaviorUnchanged()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
|
||||
public class AlarmStateChangedEnrichmentTests
|
||||
{
|
||||
/// <summary>Verifies that a default alarm state change is of the computed kind with auto-acknowledge.</summary>
|
||||
[Fact]
|
||||
public void Defaults_AreComputedKind_WithAutoAck()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public class AlarmStateChangedEnrichmentTests
|
||||
Assert.Equal("", m.SourceReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the computed factory maps priority and state onto the message.</summary>
|
||||
[Fact]
|
||||
public void Factory_ForComputed_MapsPriorityAndState()
|
||||
{
|
||||
@@ -28,6 +30,7 @@ public class AlarmStateChangedEnrichmentTests
|
||||
|
||||
// ── MES alarm-status API §6.4: additive AckTime ──
|
||||
|
||||
/// <summary>Verifies that the acknowledge time defaults to null when using the positional constructor.</summary>
|
||||
[Fact]
|
||||
public void AckTime_DefaultsToNull_OnThePositionalConstructor()
|
||||
{
|
||||
@@ -40,6 +43,7 @@ public class AlarmStateChangedEnrichmentTests
|
||||
Assert.Null(m.AckTime); // …yet still no ack instant
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the acknowledge time round-trips through its init property.</summary>
|
||||
[Fact]
|
||||
public void AckTime_RoundTripsThroughTheInitProperty()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
/// </summary>
|
||||
public class BrowseCommandsRegistryTests
|
||||
{
|
||||
/// <summary>Verifies that the command registry discovers the browse-node command.</summary>
|
||||
[Fact]
|
||||
public void Registry_discovers_BrowseNodeCommand()
|
||||
{
|
||||
|
||||
+13
-1
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Task 21 (PLAN-07): the additive optional <c>SiteIdentifier</c> trailing param on the
|
||||
/// The additive optional <c>SiteIdentifier</c> trailing param on the
|
||||
/// Browse/Verify/Cert-trust management commands. Verifies backward compatibility (legacy
|
||||
/// JSON without the field deserializes to null) and round-trip survival when set.
|
||||
/// </summary>
|
||||
@@ -17,6 +17,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
|
||||
// ── Backward compatibility: legacy JSON (no SiteIdentifier) → null ──
|
||||
|
||||
/// <summary>Verifies that browse node command legacy json site identifier null.</summary>
|
||||
[Fact]
|
||||
public void BrowseNodeCommand_LegacyJson_SiteIdentifierNull()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal("conn", cmd.ConnectionName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that search address space command legacy json site identifier null.</summary>
|
||||
[Fact]
|
||||
public void SearchAddressSpaceCommand_LegacyJson_SiteIdentifierNull()
|
||||
{
|
||||
@@ -35,6 +37,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal("pump", cmd.Query);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that verify endpoint command legacy json site identifier null.</summary>
|
||||
[Fact]
|
||||
public void VerifyEndpointCommand_LegacyJson_SiteIdentifierNull()
|
||||
{
|
||||
@@ -44,6 +47,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal("OpcUa", cmd.Protocol);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that trust server cert command legacy json site identifier null.</summary>
|
||||
[Fact]
|
||||
public void TrustServerCertCommand_LegacyJson_SiteIdentifierNull()
|
||||
{
|
||||
@@ -53,6 +57,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal("ABCD", cmd.Thumbprint);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that remove server cert command legacy json site identifier null.</summary>
|
||||
[Fact]
|
||||
public void RemoveServerCertCommand_LegacyJson_SiteIdentifierNull()
|
||||
{
|
||||
@@ -62,6 +67,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal("ABCD", cmd.Thumbprint);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that list server certs command legacy json site identifier null.</summary>
|
||||
[Fact]
|
||||
public void ListServerCertsCommand_LegacyJson_SiteIdentifierNull()
|
||||
{
|
||||
@@ -72,6 +78,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
|
||||
// ── Round-trip: SiteIdentifier set survives serialize → deserialize ──
|
||||
|
||||
/// <summary>Verifies that browse node command round trip site identifier survives.</summary>
|
||||
[Fact]
|
||||
public void BrowseNodeCommand_RoundTrip_SiteIdentifierSurvives()
|
||||
{
|
||||
@@ -82,6 +89,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal(original, round);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that search address space command round trip site identifier survives.</summary>
|
||||
[Fact]
|
||||
public void SearchAddressSpaceCommand_RoundTrip_SiteIdentifierSurvives()
|
||||
{
|
||||
@@ -92,6 +100,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal(original, round);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that verify endpoint command round trip site identifier survives.</summary>
|
||||
[Fact]
|
||||
public void VerifyEndpointCommand_RoundTrip_SiteIdentifierSurvives()
|
||||
{
|
||||
@@ -102,6 +111,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal(original, round);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that trust server cert command round trip site identifier survives.</summary>
|
||||
[Fact]
|
||||
public void TrustServerCertCommand_RoundTrip_SiteIdentifierSurvives()
|
||||
{
|
||||
@@ -112,6 +122,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal(original, round);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that remove server cert command round trip site identifier survives.</summary>
|
||||
[Fact]
|
||||
public void RemoveServerCertCommand_RoundTrip_SiteIdentifierSurvives()
|
||||
{
|
||||
@@ -122,6 +133,7 @@ public class BrowseVerifyCertSiteIdentifierTests
|
||||
Assert.Equal(original, round);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that list server certs command round trip site identifier survives.</summary>
|
||||
[Fact]
|
||||
public void ListServerCertsCommand_RoundTrip_SiteIdentifierSurvives()
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ public class ClusterClientContractLockTests
|
||||
// (a) Old-shape payloads: newer trailing optional fields absent → defaults.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an old-shape <c>CreateDataConnectionCommand</c> without the newer trailing fields still deserializes.</summary>
|
||||
[Fact]
|
||||
public void CreateDataConnectionCommand_OldShape_WithoutNewerTrailingFields_Deserializes()
|
||||
{
|
||||
@@ -62,6 +63,7 @@ public class ClusterClientContractLockTests
|
||||
Assert.Equal(0, cmd.FailoverRetryCount); // value-type missing field → default(int), not 3
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an old-shape <c>DeployInstanceCommand</c> without the fetch fields still deserializes.</summary>
|
||||
[Fact]
|
||||
public void DeployInstanceCommand_OldShape_WithoutFetchFields_Deserializes()
|
||||
{
|
||||
@@ -78,6 +80,7 @@ public class ClusterClientContractLockTests
|
||||
Assert.Null(cmd.FetchToken); // default applied
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an old-shape <c>NotificationSubmit</c> without the execution correlation fields still deserializes.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_OldShape_WithoutExecutionCorrelationFields_Deserializes()
|
||||
{
|
||||
@@ -99,6 +102,7 @@ public class ClusterClientContractLockTests
|
||||
// (b) Unknown fields from a newer peer are ignored (never throw).
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an unknown field from a newer peer is ignored when deserializing <c>AttributeValueChanged</c>.</summary>
|
||||
[Fact]
|
||||
public void AttributeValueChanged_UnknownFieldFromNewerPeer_IsIgnored()
|
||||
{
|
||||
@@ -112,6 +116,7 @@ public class ClusterClientContractLockTests
|
||||
Assert.Equal("Good", evt.Quality);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown field from a newer peer is ignored when deserializing <c>GetAttributeRequest</c>.</summary>
|
||||
[Fact]
|
||||
public void GetAttributeRequest_UnknownFieldFromNewerPeer_IsIgnored()
|
||||
{
|
||||
@@ -125,6 +130,7 @@ public class ClusterClientContractLockTests
|
||||
Assert.Equal("a", req.AttributeName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown field from a newer peer is ignored when deserializing <c>SetStaticAttributeCommand</c>.</summary>
|
||||
[Fact]
|
||||
public void SetStaticAttributeCommand_UnknownFieldFromNewerPeer_IsIgnored()
|
||||
{
|
||||
@@ -142,6 +148,7 @@ public class ClusterClientContractLockTests
|
||||
// (c) Round-trip preserves values.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a <c>ManagementEnvelope</c> round trip preserves the correlation id and user.</summary>
|
||||
[Fact]
|
||||
public void ManagementEnvelope_RoundTrip_PreservesCorrelationIdAndUser()
|
||||
{
|
||||
@@ -159,6 +166,7 @@ public class ClusterClientContractLockTests
|
||||
Assert.Equal(new[] { "site-1", "site-2" }, round.User.PermittedSiteIds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>AttributeValueChanged</c> round trip preserves its values.</summary>
|
||||
[Fact]
|
||||
public void AttributeValueChanged_RoundTrip_PreservesValues()
|
||||
{
|
||||
@@ -175,6 +183,7 @@ public class ClusterClientContractLockTests
|
||||
Assert.Equal(evt.Timestamp, round.Timestamp);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a <c>CreateDataConnectionCommand</c> round trip preserves its values.</summary>
|
||||
[Fact]
|
||||
public void CreateDataConnectionCommand_RoundTrip_PreservesValues()
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// WP-9 (Phase 8): Message contract compatibility tests.
|
||||
/// Message contract compatibility tests.
|
||||
/// Verifies forward compatibility (unknown fields), backward compatibility (missing optional fields),
|
||||
/// and version skew scenarios for all critical message types.
|
||||
/// </summary>
|
||||
@@ -24,6 +24,7 @@ public class CompatibilityTests
|
||||
|
||||
// ── Forward Compatibility: unknown fields are ignored ──
|
||||
|
||||
/// <summary>Verifies that a deploy-instance command ignores an unknown field when deserialized.</summary>
|
||||
[Fact]
|
||||
public void ForwardCompat_DeployInstanceCommand_UnknownFieldIgnored()
|
||||
{
|
||||
@@ -46,6 +47,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("abc123", msg.RevisionHash);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site health report ignores an unknown field when deserialized.</summary>
|
||||
[Fact]
|
||||
public void ForwardCompat_SiteHealthReport_UnknownFieldIgnored()
|
||||
{
|
||||
@@ -70,6 +72,7 @@ public class CompatibilityTests
|
||||
Assert.Equal(5, msg.SequenceNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script call request ignores an unknown field when deserialized.</summary>
|
||||
[Fact]
|
||||
public void ForwardCompat_ScriptCallRequest_UnknownFieldIgnored()
|
||||
{
|
||||
@@ -90,6 +93,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("OnTrigger", msg.ScriptName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an attribute-value-changed message ignores an unknown field when deserialized.</summary>
|
||||
[Fact]
|
||||
public void ForwardCompat_AttributeValueChanged_UnknownFieldIgnored()
|
||||
{
|
||||
@@ -112,6 +116,7 @@ public class CompatibilityTests
|
||||
|
||||
// ── Backward Compatibility: missing optional fields ──
|
||||
|
||||
/// <summary>Verifies that a deployment status response deserializes when the error message is missing.</summary>
|
||||
[Fact]
|
||||
public void BackwardCompat_DeploymentStatusResponse_MissingErrorMessage()
|
||||
{
|
||||
@@ -130,6 +135,7 @@ public class CompatibilityTests
|
||||
Assert.Null(msg.ErrorMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script call result deserializes when the return value is missing.</summary>
|
||||
[Fact]
|
||||
public void BackwardCompat_ScriptCallResult_MissingReturnValue()
|
||||
{
|
||||
@@ -147,6 +153,7 @@ public class CompatibilityTests
|
||||
Assert.Null(msg.ReturnValue);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deploy-artifacts command deserializes when the optional lists are missing.</summary>
|
||||
[Fact]
|
||||
public void BackwardCompat_DeployArtifactsCommand_MissingOptionalLists()
|
||||
{
|
||||
@@ -164,6 +171,7 @@ public class CompatibilityTests
|
||||
Assert.Null(msg.ExternalSystems);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance lifecycle response deserializes when the error message is missing.</summary>
|
||||
[Fact]
|
||||
public void BackwardCompat_InstanceLifecycleResponse_MissingErrorMessage()
|
||||
{
|
||||
@@ -184,6 +192,7 @@ public class CompatibilityTests
|
||||
|
||||
// ── Version Skew: old message format still deserializable ──
|
||||
|
||||
/// <summary>Verifies that an old-shape deploy command deserializes with default values for newer fields.</summary>
|
||||
[Fact]
|
||||
public void VersionSkew_OldDeployCommand_DeserializesWithDefaults()
|
||||
{
|
||||
@@ -205,6 +214,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("old-hash", msg.RevisionHash);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an old-shape health report deserializes correctly.</summary>
|
||||
[Fact]
|
||||
public void VersionSkew_OldHealthReport_DeserializesCorrectly()
|
||||
{
|
||||
@@ -230,11 +240,12 @@ public class CompatibilityTests
|
||||
|
||||
// ── Round-trip serialization for all key message types ──
|
||||
|
||||
// Communication-016: RoundTrip_ConnectionStateChanged_Succeeds removed
|
||||
// RoundTrip_ConnectionStateChanged_Succeeds removed
|
||||
// alongside the dead ConnectionStateChanged message record. No production
|
||||
// code emits or receives this message — disconnect detection is owned by
|
||||
// the gRPC keepalive and the Ask-timeout path.
|
||||
|
||||
/// <summary>Verifies that an alarm-state-changed message survives a serialization round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_AlarmStateChanged_Succeeds()
|
||||
{
|
||||
@@ -247,6 +258,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("HighTemp", deserialized.AlarmName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an alarm-state-changed message preserves its acknowledgement time across a round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_AlarmStateChanged_PreservesAckTime()
|
||||
{
|
||||
@@ -264,6 +276,7 @@ public class CompatibilityTests
|
||||
Assert.Equal(ackedAt, deserialized!.AckTime);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing acknowledgement time deserializes as null.</summary>
|
||||
[Fact]
|
||||
public void BackwardCompat_AlarmStateChanged_MissingAckTime_DefaultsToNull()
|
||||
{
|
||||
@@ -285,6 +298,7 @@ public class CompatibilityTests
|
||||
Assert.Null(deserialized!.AckTime);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing MES-reportable flag deserializes as false.</summary>
|
||||
[Fact]
|
||||
public void BackwardCompat_AlarmStateChanged_MissingIsMesReportable_DefaultsToFalse()
|
||||
{
|
||||
@@ -308,6 +322,7 @@ public class CompatibilityTests
|
||||
Assert.False(deserialized!.IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a heartbeat message survives a serialization round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_HeartbeatMessage_Succeeds()
|
||||
{
|
||||
@@ -320,6 +335,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("node-a", deserialized.NodeHostname);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a disable-instance command survives a serialization round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DisableInstanceCommand_Succeeds()
|
||||
{
|
||||
@@ -331,6 +347,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("cmd-1", deserialized!.CommandId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an enable-instance command survives a serialization round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_EnableInstanceCommand_Succeeds()
|
||||
{
|
||||
@@ -342,6 +359,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("cmd-2", deserialized!.CommandId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a delete-instance command survives a serialization round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DeleteInstanceCommand_Succeeds()
|
||||
{
|
||||
@@ -355,6 +373,7 @@ public class CompatibilityTests
|
||||
|
||||
// ── Additive-only evolution: new fields added as nullable ──
|
||||
|
||||
/// <summary>Verifies that newly added nullable fields do not break deserialization of older payloads.</summary>
|
||||
[Fact]
|
||||
public void AdditiveEvolution_NewNullableFields_DoNotBreakDeserialization()
|
||||
{
|
||||
@@ -369,6 +388,7 @@ public class CompatibilityTests
|
||||
Assert.Null(msg!.ErrorMessage); // Optional field defaults to null
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown enum value is handled gracefully during deserialization.</summary>
|
||||
[Fact]
|
||||
public void EnumDeserialization_UnknownValue_HandledGracefully()
|
||||
{
|
||||
@@ -381,8 +401,9 @@ public class CompatibilityTests
|
||||
Assert.Equal((DeploymentStatus)99, msg!.Status);
|
||||
}
|
||||
|
||||
// ── DeploymentManager-006: query-the-site-before-redeploy contracts ──
|
||||
// ── query-the-site-before-redeploy contracts ──
|
||||
|
||||
/// <summary>Verifies that a deployment-state query request survives a serialization round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DeploymentStateQueryRequest_Succeeds()
|
||||
{
|
||||
@@ -395,6 +416,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("inst-1", deserialized.InstanceUniqueName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deployed deployment-state query response survives a serialization round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DeploymentStateQueryResponse_Deployed_Succeeds()
|
||||
{
|
||||
@@ -409,6 +431,7 @@ public class CompatibilityTests
|
||||
Assert.Equal("sha256:abc", deserialized.AppliedRevisionHash);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a not-deployed deployment-state query response keeps its null applied state across a round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DeploymentStateQueryResponse_NotDeployed_NullApplied()
|
||||
{
|
||||
|
||||
+5
-1
@@ -4,13 +4,14 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for Commons-008 — <see cref="SetConnectionBindingsCommand"/>
|
||||
/// Regression tests: <see cref="SetConnectionBindingsCommand"/>
|
||||
/// previously declared its bindings as <c>IReadOnlyList<(string, int)></c>.
|
||||
/// A <c>ValueTuple</c> serializes as <c>Item1</c>/<c>Item2</c> and cannot evolve
|
||||
/// additively (REQ-COM-5a). It is now a named <see cref="ConnectionBinding"/> record.
|
||||
/// </summary>
|
||||
public class ConnectionBindingSerializationTests
|
||||
{
|
||||
/// <summary>Verifies that connection binding serializes with named properties.</summary>
|
||||
[Fact]
|
||||
public void ConnectionBinding_SerializesWithNamedProperties()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public class ConnectionBindingSerializationTests
|
||||
Assert.False(doc.RootElement.TryGetProperty("Item2", out _));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that set connection bindings command round trips through json.</summary>
|
||||
[Fact]
|
||||
public void SetConnectionBindingsCommand_RoundTripsThroughJson()
|
||||
{
|
||||
@@ -52,6 +54,7 @@ public class ConnectionBindingSerializationTests
|
||||
Assert.Equal(original.Bindings, deserialized.Bindings);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that roundtrip preserves override when set.</summary>
|
||||
[Fact]
|
||||
public void Roundtrip_preserves_override_when_set()
|
||||
{
|
||||
@@ -65,6 +68,7 @@ public class ConnectionBindingSerializationTests
|
||||
Assert.Equal("ns=2;s=Pump1.Speed", roundtripped!.DataSourceReferenceOverride);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that roundtrip defaults override to null when absent.</summary>
|
||||
[Fact]
|
||||
public void Roundtrip_defaults_override_to_null_when_absent()
|
||||
{
|
||||
|
||||
+7
-2
@@ -6,12 +6,12 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log (#23) telemetry handoff: envelope + pull request/response DTOs.
|
||||
/// Audit Log telemetry handoff: envelope + pull request/response DTOs.
|
||||
/// At-least-once from sites; idempotent at central on <see cref="AuditEvent.EventId"/>.
|
||||
/// </summary>
|
||||
public class AuditTelemetryMessagesTests
|
||||
{
|
||||
// C3 (Task 2.5): canonical ZB.MOM.WW.Audit.AuditEvent built via the shared
|
||||
// Canonical ZB.MOM.WW.Audit.AuditEvent built via the shared
|
||||
// factory (domain fields ride in DetailsJson). These tests assert only the
|
||||
// envelope/response DTO behaviour, so the audit row's contents are incidental.
|
||||
private static AuditEvent MakeEvent(Guid? id = null) =>
|
||||
@@ -21,6 +21,7 @@ public class AuditTelemetryMessagesTests
|
||||
status: AuditStatus.Delivered,
|
||||
eventId: id ?? Guid.NewGuid());
|
||||
|
||||
/// <summary>Verifies that a telemetry envelope constructed with three events can be enumerated.</summary>
|
||||
[Fact]
|
||||
public void AuditTelemetryEnvelope_ConstructsWithThreeEvents_AndIsEnumerable()
|
||||
{
|
||||
@@ -42,6 +43,7 @@ public class AuditTelemetryMessagesTests
|
||||
Assert.Equal(3, collected.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the telemetry envelope is immutable and compares its event list by reference identity.</summary>
|
||||
[Fact]
|
||||
public void AuditTelemetryEnvelope_IsImmutable_RecordEqualityOnReferenceIdentityOfList()
|
||||
{
|
||||
@@ -62,6 +64,7 @@ public class AuditTelemetryMessagesTests
|
||||
Assert.Equal("site-01", a.SourceSiteId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a pull request can be constructed and is immutable.</summary>
|
||||
[Fact]
|
||||
public void PullAuditEventsRequest_ConstructsAndIsImmutable()
|
||||
{
|
||||
@@ -77,6 +80,7 @@ public class AuditTelemetryMessagesTests
|
||||
Assert.Equal(500, bigger.BatchSize);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a pull response with more-available set can be constructed and enumerated.</summary>
|
||||
[Fact]
|
||||
public void PullAuditEventsResponse_ConstructsWithMoreAvailableTrue_AndIsEnumerable()
|
||||
{
|
||||
@@ -94,6 +98,7 @@ public class AuditTelemetryMessagesTests
|
||||
Assert.Equal(2, collected.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a with-expression on a pull response changes only the targeted field.</summary>
|
||||
[Fact]
|
||||
public void PullAuditEventsResponse_WithExpression_ChangesSingleField()
|
||||
{
|
||||
|
||||
+10
-3
@@ -7,7 +7,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3 Bundle A — Task A4) — tests for the combined
|
||||
/// Tests for the combined
|
||||
/// audit + operational telemetry packet emitted per cached-call lifecycle event
|
||||
/// (<c>Submit</c> → per-attempt <c>ApiCallCached</c> / <c>DbWriteCached</c> →
|
||||
/// terminal <c>Resolve</c>). The site emits one packet per event; central writes
|
||||
@@ -29,7 +29,7 @@ public class CachedCallTelemetryTests
|
||||
string? errorMessage = null,
|
||||
int? httpStatus = null)
|
||||
{
|
||||
// C3 (Task 2.5): build the canonical ZB.MOM.WW.Audit.AuditEvent via the
|
||||
// Build the canonical ZB.MOM.WW.Audit.AuditEvent via the
|
||||
// shared factory; the ScadaBridge domain fields ride in DetailsJson and
|
||||
// are read back as typed properties via AsRow() in the assertions.
|
||||
// ForwardState is no longer a packet field — it is a site-storage-only
|
||||
@@ -61,7 +61,7 @@ public class CachedCallTelemetryTests
|
||||
Channel: nameof(AuditChannel.ApiOutbound),
|
||||
Target: "ERP.GetOrder",
|
||||
SourceSite: SiteId,
|
||||
// SourceNode: actual stamping arrives with Task 14; for now the
|
||||
// SourceNode: actual stamping is not done here; the
|
||||
// packet builder leaves the column null so existing assertions on
|
||||
// the packet's other fields stay intact.
|
||||
SourceNode: null,
|
||||
@@ -74,6 +74,7 @@ public class CachedCallTelemetryTests
|
||||
TerminalAtUtc: terminalAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a submit packet carries a CachedSubmit audit row and an operational retry count of zero.</summary>
|
||||
[Fact]
|
||||
public void SubmitPacket_AuditCarriesCachedSubmit_AndOperationalRetryCountZero()
|
||||
{
|
||||
@@ -91,6 +92,7 @@ public class CachedCallTelemetryTests
|
||||
Assert.Equal(trackedId, packet.Operational.TrackedOperationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an attempted packet carries an ApiCallCached audit row whose retry count matches the operational record.</summary>
|
||||
[Fact]
|
||||
public void AttemptedPacket_AuditCarriesApiCallCached_RetryCountAlignsBetweenAuditAndOperational()
|
||||
{
|
||||
@@ -121,6 +123,7 @@ public class CachedCallTelemetryTests
|
||||
Assert.Null(packet.Operational.TerminalAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an attempted database-write packet carries the DbWriteCached kind.</summary>
|
||||
[Fact]
|
||||
public void AttemptedPacket_DbWriteCached_CarriesDbWriteCachedKind()
|
||||
{
|
||||
@@ -144,6 +147,8 @@ public class CachedCallTelemetryTests
|
||||
Assert.Equal(1, packet.Operational.RetryCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a resolve packet carries a CachedResolve audit row and sets the operational terminal timestamp.</summary>
|
||||
/// <param name="terminalStatus">The terminal audit status being resolved.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Delivered)]
|
||||
[InlineData(AuditStatus.Failed)]
|
||||
@@ -169,6 +174,7 @@ public class CachedCallTelemetryTests
|
||||
Assert.Equal(terminalAt, packet.Operational.TerminalAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cached-call telemetry packet is equal to a copy built from the same values.</summary>
|
||||
[Fact]
|
||||
public void CachedCallTelemetry_RoundTripEquality()
|
||||
{
|
||||
@@ -190,6 +196,7 @@ public class CachedCallTelemetryTests
|
||||
Assert.Equal(1, c.Operational.RetryCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site-call operational record supports value equality and non-destructive copying with a with-expression.</summary>
|
||||
[Fact]
|
||||
public void SiteCallOperational_RoundTripEquality_AndWithExpression()
|
||||
{
|
||||
|
||||
+11
-4
@@ -4,8 +4,8 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ManagementCommandRegistry"/>, including the Commons-004
|
||||
/// regression: <c>GetCommandName</c> and <c>Resolve</c> must be symmetric — every
|
||||
/// Tests for <see cref="ManagementCommandRegistry"/>, including the
|
||||
/// regression where <c>GetCommandName</c> and <c>Resolve</c> must be symmetric — every
|
||||
/// type for which <c>GetCommandName</c> yields a name must round-trip back to the
|
||||
/// same type via <c>Resolve</c>.
|
||||
/// </summary>
|
||||
@@ -19,6 +19,7 @@ public class ManagementCommandRegistryTests
|
||||
&& t != typeof(ReadTagValuesCommand)
|
||||
&& t != typeof(ResolveRolesCommand));
|
||||
|
||||
/// <summary>Verifies that every registered command type round-trips through <c>GetCommandName</c> and <c>Resolve</c>.</summary>
|
||||
[Fact]
|
||||
public void GetCommandName_Resolve_RoundTrips_ForEveryRegisteredCommand()
|
||||
{
|
||||
@@ -30,6 +31,7 @@ public class ManagementCommandRegistryTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resolving a known command name returns its type.</summary>
|
||||
[Fact]
|
||||
public void Resolve_KnownCommand_ReturnsType()
|
||||
{
|
||||
@@ -37,12 +39,14 @@ public class ManagementCommandRegistryTests
|
||||
Assert.Equal(typeof(CreateSiteCommand), type);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resolving an unknown command name returns null.</summary>
|
||||
[Fact]
|
||||
public void Resolve_UnknownCommand_ReturnsNull()
|
||||
{
|
||||
Assert.Null(ManagementCommandRegistry.Resolve("NoSuchCommand"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resolving a command name is case-insensitive.</summary>
|
||||
[Fact]
|
||||
public void Resolve_IsCaseInsensitive()
|
||||
{
|
||||
@@ -50,7 +54,7 @@ public class ManagementCommandRegistryTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commons-004: <c>GetCommandName</c> previously stripped a <c>Command</c> suffix
|
||||
/// <c>GetCommandName</c> previously stripped a <c>Command</c> suffix
|
||||
/// from <em>any</em> type, producing names the registry cannot resolve. It must
|
||||
/// only return a name for a command type the registry actually contains.
|
||||
/// </summary>
|
||||
@@ -62,6 +66,7 @@ public class ManagementCommandRegistryTests
|
||||
() => ManagementCommandRegistry.GetCommandName(typeof(UnregisteredFakeCommand)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <c>GetCommandName</c> throws for a type that is not a registered command.</summary>
|
||||
[Fact]
|
||||
public void GetCommandName_NonCommandType_Throws()
|
||||
{
|
||||
@@ -69,12 +74,14 @@ public class ManagementCommandRegistryTests
|
||||
() => ManagementCommandRegistry.GetCommandName(typeof(string)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <c>GetCommandName</c> returns the command name with the <c>Command</c> suffix stripped.</summary>
|
||||
[Fact]
|
||||
public void GetCommandName_RegisteredCommand_ReturnsStrippedName()
|
||||
{
|
||||
Assert.Equal("CreateSite", ManagementCommandRegistry.GetCommandName(typeof(CreateSiteCommand)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all native alarm source commands are registered.</summary>
|
||||
[Fact]
|
||||
public void Resolve_NativeAlarmSourceCommands_AllRegistered()
|
||||
{
|
||||
@@ -95,7 +102,7 @@ public class ManagementCommandRegistryTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M9-T32c: the schema-library CRUD commands must auto-register by reflection
|
||||
/// The schema-library CRUD commands must auto-register by reflection
|
||||
/// (no manual registry entry) so the HTTP management boundary can route them.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
|
||||
@@ -17,6 +17,7 @@ public class MessageConventionTests
|
||||
&& !t.Name.StartsWith("<") // exclude compiler-generated types
|
||||
&& (t.IsClass || (t.IsValueType && !t.IsPrimitive)));
|
||||
|
||||
/// <summary>Verifies that every message type is a record.</summary>
|
||||
[Fact]
|
||||
public void AllMessageTypes_ShouldBeRecords()
|
||||
{
|
||||
@@ -29,6 +30,7 @@ public class MessageConventionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that every message timestamp property is a <c>DateTimeOffset</c>.</summary>
|
||||
[Fact]
|
||||
public void AllMessageTimestampProperties_ShouldBeDateTimeOffset()
|
||||
{
|
||||
@@ -46,6 +48,7 @@ public class MessageConventionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deploy-instance command survives a JSON round trip.</summary>
|
||||
[Fact]
|
||||
public void JsonRoundTrip_DeployInstanceCommand_ShouldSucceed()
|
||||
{
|
||||
@@ -60,6 +63,7 @@ public class MessageConventionTests
|
||||
Assert.Equal(msg.InstanceUniqueName, deserialized.InstanceUniqueName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that JSON containing an unknown field still deserializes.</summary>
|
||||
[Fact]
|
||||
public void JsonForwardCompatibility_UnknownField_ShouldDeserialize()
|
||||
{
|
||||
@@ -72,6 +76,7 @@ public class MessageConventionTests
|
||||
Assert.Equal("dep-1", deserialized!.DeploymentId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that JSON missing an optional field still deserializes.</summary>
|
||||
[Fact]
|
||||
public void JsonBackwardCompatibility_MissingOptionalField_ShouldDeserialize()
|
||||
{
|
||||
@@ -85,6 +90,7 @@ public class MessageConventionTests
|
||||
Assert.Null(deserialized.ErrorMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site health report survives a JSON round trip.</summary>
|
||||
[Fact]
|
||||
public void JsonRoundTrip_SiteHealthReport_ShouldSucceed()
|
||||
{
|
||||
@@ -110,6 +116,7 @@ public class MessageConventionTests
|
||||
Assert.Equal(1, deserialized.SequenceNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deploy-artifacts command survives a JSON round trip.</summary>
|
||||
[Fact]
|
||||
public void JsonRoundTrip_DeployArtifactsCommand_ShouldSucceed()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
|
||||
public class NativeAlarmMessagesTests
|
||||
{
|
||||
/// <summary>Verifies that subscribe alarms request: carries source and filter.</summary>
|
||||
[Fact]
|
||||
public void SubscribeAlarmsRequest_CarriesSourceAndFilter()
|
||||
{
|
||||
@@ -14,6 +15,7 @@ public class NativeAlarmMessagesTests
|
||||
Assert.Null(r.ConditionFilter);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that native alarm transition update: wraps transition.</summary>
|
||||
[Fact]
|
||||
public void NativeAlarmTransitionUpdate_WrapsTransition()
|
||||
{
|
||||
@@ -25,6 +27,7 @@ public class NativeAlarmMessagesTests
|
||||
Assert.Equal("Tank01", u.Transition.SourceObjectReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that native alarm transition, when ack time, is an additive trailing parameter.</summary>
|
||||
[Fact]
|
||||
public void NativeAlarmTransition_AckTime_IsAnAdditiveTrailingParameter()
|
||||
{
|
||||
|
||||
@@ -9,8 +9,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
/// </summary>
|
||||
public class NotificationMessagesTests
|
||||
{
|
||||
// ── Task 7: site/central notification message contracts ──
|
||||
// ── Site/central notification message contracts ──
|
||||
|
||||
/// <summary>Verifies that positional construction of a notification submit sets all fields.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_PositionalConstruction_SetsAllFields()
|
||||
{
|
||||
@@ -29,6 +30,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(enqueuedAt, msg.SiteEnqueuedAt);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a notification submit allows null optional source fields.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_AllowsNullOptionalSourceFields()
|
||||
{
|
||||
@@ -40,10 +42,11 @@ public class NotificationMessagesTests
|
||||
Assert.Null(msg.SourceScript);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the origin execution id of a notification submit defaults to null.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_OriginExecutionId_DefaultsToNull()
|
||||
{
|
||||
// Audit Log #23: OriginExecutionId is an additive trailing member — a
|
||||
// OriginExecutionId is an additive trailing member — a
|
||||
// submit built without it (old call sites / old serialized payloads)
|
||||
// leaves the id null.
|
||||
var msg = new NotificationSubmit(
|
||||
@@ -53,6 +56,7 @@ public class NotificationMessagesTests
|
||||
Assert.Null(msg.OriginExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the origin execution id round-trips when supplied.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_OriginExecutionId_RoundTripsWhenSupplied()
|
||||
{
|
||||
@@ -64,6 +68,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(executionId, msg.OriginExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the origin execution id survives a JSON round trip.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_OriginExecutionId_SurvivesJsonRoundTrip()
|
||||
{
|
||||
@@ -81,6 +86,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(executionId, roundTripped!.OriginExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the origin parent execution id defaults to null.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_OriginParentExecutionId_DefaultsToNull()
|
||||
{
|
||||
@@ -94,6 +100,7 @@ public class NotificationMessagesTests
|
||||
Assert.Null(msg.OriginParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the origin parent execution id round-trips when supplied.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_OriginParentExecutionId_RoundTripsWhenSupplied()
|
||||
{
|
||||
@@ -107,6 +114,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(parentExecutionId, msg.OriginParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the origin parent execution id survives a JSON round trip.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_OriginParentExecutionId_SurvivesJsonRoundTrip()
|
||||
{
|
||||
@@ -126,6 +134,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(parentExecutionId, roundTripped!.OriginParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a notification submit carries the source node.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_carries_SourceNode()
|
||||
{
|
||||
@@ -152,6 +161,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal("node-a", roundTripped!.SourceNode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two notification submits are equal when all fields match.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmit_ValueEquality_EqualWhenAllFieldsMatch()
|
||||
{
|
||||
@@ -163,6 +173,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a with-expression on a notification submit ack changes a single field.</summary>
|
||||
[Fact]
|
||||
public void NotificationSubmitAck_WithExpression_ChangesSingleField()
|
||||
{
|
||||
@@ -176,6 +187,7 @@ public class NotificationMessagesTests
|
||||
Assert.NotEqual(ack, rejected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that positional construction of a notification status query sets all fields.</summary>
|
||||
[Fact]
|
||||
public void NotificationStatusQuery_PositionalConstruction_SetsAllFields()
|
||||
{
|
||||
@@ -185,6 +197,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal("notif-9", msg.NotificationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that positional construction of a notification status response sets all fields.</summary>
|
||||
[Fact]
|
||||
public void NotificationStatusResponse_PositionalConstruction_SetsAllFields()
|
||||
{
|
||||
@@ -200,8 +213,9 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(deliveredAt, msg.DeliveredAt);
|
||||
}
|
||||
|
||||
// ── Task 8: outbox UI query/action contracts ──
|
||||
// ── Outbox UI query/action contracts ──
|
||||
|
||||
/// <summary>Verifies that positional construction of a notification outbox query request sets all fields.</summary>
|
||||
[Fact]
|
||||
public void NotificationOutboxQueryRequest_PositionalConstruction_SetsAllFields()
|
||||
{
|
||||
@@ -224,6 +238,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(50, msg.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two notification summaries are equal when all fields match.</summary>
|
||||
[Fact]
|
||||
public void NotificationSummary_ValueEquality_EqualWhenAllFieldsMatch()
|
||||
{
|
||||
@@ -239,6 +254,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that positional construction of a notification outbox query response sets all fields.</summary>
|
||||
[Fact]
|
||||
public void NotificationOutboxQueryResponse_PositionalConstruction_SetsAllFields()
|
||||
{
|
||||
@@ -255,6 +271,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal(1, msg.TotalCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the retry notification request and response round-trip their fields.</summary>
|
||||
[Fact]
|
||||
public void RetryNotificationRequestAndResponse_RoundTripFields()
|
||||
{
|
||||
@@ -267,6 +284,7 @@ public class NotificationMessagesTests
|
||||
Assert.Null(response.ErrorMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the discard notification request and response round-trip their fields.</summary>
|
||||
[Fact]
|
||||
public void DiscardNotificationRequestAndResponse_RoundTripFields()
|
||||
{
|
||||
@@ -278,6 +296,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal("not found", response.ErrorMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a with-expression on a notification KPI response changes a single field.</summary>
|
||||
[Fact]
|
||||
public void NotificationKpiResponse_WithExpression_ChangesSingleField()
|
||||
{
|
||||
@@ -294,6 +313,7 @@ public class NotificationMessagesTests
|
||||
Assert.NotEqual(kpi, updated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that positional construction of a notification KPI request sets the correlation id.</summary>
|
||||
[Fact]
|
||||
public void NotificationKpiRequest_PositionalConstruction_SetsCorrelationId()
|
||||
{
|
||||
@@ -301,6 +321,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal("corr-1", msg.CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the per-site notification KPI request carries the correlation id.</summary>
|
||||
[Fact]
|
||||
public void PerSiteNotificationKpiRequest_CarriesCorrelationId()
|
||||
{
|
||||
@@ -308,6 +329,7 @@ public class NotificationMessagesTests
|
||||
Assert.Equal("corr-1", request.CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the per-site notification KPI response carries the per-site snapshots.</summary>
|
||||
[Fact]
|
||||
public void PerSiteNotificationKpiResponse_CarriesPerSiteSnapshots()
|
||||
{
|
||||
|
||||
+3
@@ -4,6 +4,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
|
||||
public class ReadTagValuesCommandRegistryTests
|
||||
{
|
||||
/// <summary>Verifies that site-only and retired commands are not registered as HTTP commands.</summary>
|
||||
/// <param name="type">The command message type.</param>
|
||||
/// <param name="wireName">The command wire name.</param>
|
||||
[Theory]
|
||||
[InlineData(typeof(ReadTagValuesCommand), "ReadTagValues")]
|
||||
[InlineData(typeof(ResolveRolesCommand), "ResolveRoles")]
|
||||
|
||||
@@ -4,13 +4,14 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Site Call Audit (#22): construction, value-equality and optionality tests
|
||||
/// Site Call Audit: construction, value-equality and optionality tests
|
||||
/// for the Site Calls UI query / KPI / detail message contracts. Mirrors the
|
||||
/// Notification Outbox <c>NotificationMessagesTests</c> coverage of the read
|
||||
/// side, scoped to the contracts the Site Calls page consumes.
|
||||
/// </summary>
|
||||
public class SiteCallQueriesTests
|
||||
{
|
||||
/// <summary>Verifies that positional construction of a site call query request sets all fields.</summary>
|
||||
[Fact]
|
||||
public void SiteCallQueryRequest_PositionalConstruction_SetsAllFields()
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public class SiteCallQueriesTests
|
||||
Assert.Equal(50, request.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site call query request allows its optional filters to be null.</summary>
|
||||
[Fact]
|
||||
public void SiteCallQueryRequest_AllowsNullOptionalFilters()
|
||||
{
|
||||
@@ -48,6 +50,7 @@ public class SiteCallQueriesTests
|
||||
Assert.Null(request.AfterId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two site call query responses are equal when all their fields match.</summary>
|
||||
[Fact]
|
||||
public void SiteCallQueryResponse_ValueEquality_EqualWhenAllFieldsMatch()
|
||||
{
|
||||
@@ -58,6 +61,7 @@ public class SiteCallQueriesTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site call summary carries the entity columns and the stuck flag.</summary>
|
||||
[Fact]
|
||||
public void SiteCallSummary_CarriesEntityColumnsAndStuckFlag()
|
||||
{
|
||||
@@ -77,6 +81,7 @@ public class SiteCallQueriesTests
|
||||
Assert.True(summary.IsStuck);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a detail response for a missing row has a null detail.</summary>
|
||||
[Fact]
|
||||
public void SiteCallDetailResponse_MissingRow_HasNullDetail()
|
||||
{
|
||||
@@ -87,6 +92,7 @@ public class SiteCallQueriesTests
|
||||
Assert.Equal("site call not found", response.ErrorMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a failed KPI response zeroes its KPI fields.</summary>
|
||||
[Fact]
|
||||
public void SiteCallKpiResponse_FailureShape_ZeroesKpiFields()
|
||||
{
|
||||
@@ -101,6 +107,7 @@ public class SiteCallQueriesTests
|
||||
Assert.Null(response.OldestPendingAge);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the per-site KPI response carries a snapshot for each site.</summary>
|
||||
[Fact]
|
||||
public void PerSiteSiteCallKpiResponse_CarriesPerSiteSnapshots()
|
||||
{
|
||||
@@ -119,6 +126,7 @@ public class SiteCallQueriesTests
|
||||
Assert.Equal(TimeSpan.FromMinutes(15), site.OldestPendingAge);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the oldest pending age of a KPI snapshot can be null for an empty table.</summary>
|
||||
[Fact]
|
||||
public void SiteCallKpiSnapshot_OldestPendingAge_IsNullableForEmptyTable()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||
/// </summary>
|
||||
public class TraceFlagDefaultsTests
|
||||
{
|
||||
/// <summary>Verifies that existing positional constructions default to untraced.</summary>
|
||||
[Fact]
|
||||
public void ExistingPositionalConstructions_DefaultToUntraced()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ public class TraceFlagDefaultsTests
|
||||
Assert.Null(result.TraceEvents);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that trace events are audit events.</summary>
|
||||
[Fact]
|
||||
public void TraceEvents_AreAuditEvents()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests;
|
||||
|
||||
public class NativeAlarmSourceOverrideCsvParserTests
|
||||
{
|
||||
/// <summary>Verifies that a simple file parses into rows with no errors.</summary>
|
||||
[Fact]
|
||||
public void Parse_SimpleFile_ReturnsRowsNoErrors()
|
||||
{
|
||||
@@ -30,6 +31,7 @@ public class NativeAlarmSourceOverrideCsvParserTests
|
||||
Assert.Equal(3, result.Rows[1].LineNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that blank override fields become null so the inherited value applies.</summary>
|
||||
[Fact]
|
||||
public void Parse_BlankOverrideFields_BecomeNullInherited()
|
||||
{
|
||||
@@ -47,6 +49,7 @@ public class NativeAlarmSourceOverrideCsvParserTests
|
||||
Assert.Null(row.Filter);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that header names are matched case-insensitively.</summary>
|
||||
[Fact]
|
||||
public void Parse_HeaderCaseInsensitive_Accepted()
|
||||
{
|
||||
@@ -60,6 +63,7 @@ public class NativeAlarmSourceOverrideCsvParserTests
|
||||
Assert.Single(result.Rows);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a quoted field preserves an embedded comma.</summary>
|
||||
[Fact]
|
||||
public void Parse_QuotedFieldWithComma_PreservesEmbeddedComma()
|
||||
{
|
||||
@@ -74,6 +78,7 @@ public class NativeAlarmSourceOverrideCsvParserTests
|
||||
Assert.Equal("ns=2;s=a,b,c", row.SourceReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing header returns a header error.</summary>
|
||||
[Fact]
|
||||
public void Parse_MissingHeader_ReturnsHeaderError()
|
||||
{
|
||||
@@ -86,6 +91,7 @@ public class NativeAlarmSourceOverrideCsvParserTests
|
||||
Assert.Contains("SourceName,Connection,SourceReference,Filter", error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a row with the wrong column count reports a per-line error and is skipped.</summary>
|
||||
[Fact]
|
||||
public void Parse_WrongColumnCount_ReportsPerLineErrorAndSkipsRow()
|
||||
{
|
||||
@@ -104,6 +110,7 @@ public class NativeAlarmSourceOverrideCsvParserTests
|
||||
Assert.Equal("Temp", row.SourceName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a blank source name reports an error.</summary>
|
||||
[Fact]
|
||||
public void Parse_BlankSourceName_ReportsError()
|
||||
{
|
||||
@@ -118,6 +125,7 @@ public class NativeAlarmSourceOverrideCsvParserTests
|
||||
Assert.Contains("SourceName must not be blank", error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unterminated quote reports an error.</summary>
|
||||
[Fact]
|
||||
public void Parse_UnterminatedQuote_ReportsError()
|
||||
{
|
||||
@@ -131,6 +139,7 @@ public class NativeAlarmSourceOverrideCsvParserTests
|
||||
Assert.Contains(result.Errors, e => e.Contains("Unterminated quoted field"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that blank lines are skipped without an error.</summary>
|
||||
[Fact]
|
||||
public void Parse_BlankLinesSkippedWithoutError()
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class DataConnectionGaugeTests
|
||||
private const string ConnectedGauge = "scadabridge.site.data_connections.connected";
|
||||
private const string TotalGauge = "scadabridge.site.data_connections.total";
|
||||
|
||||
/// <summary>Verifies that the connected state raises both the connected and total gauges.</summary>
|
||||
[Fact]
|
||||
public void ConnectedState_RaisesConnectedAndTotal()
|
||||
{
|
||||
@@ -43,6 +44,8 @@ public sealed class DataConnectionGaugeTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a non-connected state raises only the total gauge.</summary>
|
||||
/// <param name="state">The state.</param>
|
||||
[Theory]
|
||||
[InlineData(ConnectionHealth.Disconnected)]
|
||||
[InlineData(ConnectionHealth.Connecting)]
|
||||
@@ -70,6 +73,7 @@ public sealed class DataConnectionGaugeTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a transition from connected to disconnected drops the connected gauge but keeps the total.</summary>
|
||||
[Fact]
|
||||
public void TransitionFromConnectedToDisconnected_DropsConnectedButKeepsTotal()
|
||||
{
|
||||
@@ -94,6 +98,7 @@ public sealed class DataConnectionGaugeTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that removing a data connection drops it from both gauges.</summary>
|
||||
[Fact]
|
||||
public void RemoveDataConnection_DropsItFromBothGauges()
|
||||
{
|
||||
@@ -111,6 +116,7 @@ public sealed class DataConnectionGaugeTests
|
||||
Assert.Equal(totalBefore, ReadGauge(TotalGauge));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that removing a data connection is idempotent.</summary>
|
||||
[Fact]
|
||||
public void RemoveDataConnection_IsIdempotent()
|
||||
{
|
||||
@@ -123,6 +129,7 @@ public sealed class DataConnectionGaugeTests
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null or empty connection name is ignored.</summary>
|
||||
[Fact]
|
||||
public void NullOrEmptyConnectionName_IsIgnored()
|
||||
{
|
||||
@@ -136,6 +143,7 @@ public sealed class DataConnectionGaugeTests
|
||||
Assert.Equal(totalBefore, ReadGauge(TotalGauge));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the stream subscriber gauge is published under its renamed name.</summary>
|
||||
[Fact]
|
||||
public void StreamSubscriberGauge_IsPublishedUnderTheRenamedName()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests;
|
||||
|
||||
public class OverrideCsvParserTests
|
||||
{
|
||||
/// <summary>Verifies that a simple three-column file parses into two rows with no errors.</summary>
|
||||
[Fact]
|
||||
public void Parse_SimpleThreeColumnFile_ReturnsTwoRowsNoErrors()
|
||||
{
|
||||
@@ -25,6 +26,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal(3, result.Rows[1].LineNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that rows from a two-column file without an element type have a null element type.</summary>
|
||||
[Fact]
|
||||
public void Parse_TwoColumnFileWithoutElementType_RowsHaveNullElementType()
|
||||
{
|
||||
@@ -39,6 +41,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal("Pump A", result.Rows[1].Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a quoted value containing a comma keeps the embedded comma.</summary>
|
||||
[Fact]
|
||||
public void Parse_QuotedValueWithComma_PreservesEmbeddedComma()
|
||||
{
|
||||
@@ -53,6 +56,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Null(row.ElementType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a doubled quote escape is unescaped to a single quote.</summary>
|
||||
[Fact]
|
||||
public void Parse_DoubledQuoteEscape_UnescapesToSingleQuote()
|
||||
{
|
||||
@@ -65,6 +69,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal("he said \"hi\"", row.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty value field yields a null value.</summary>
|
||||
[Fact]
|
||||
public void Parse_EmptyValueField_YieldsNullValue()
|
||||
{
|
||||
@@ -79,6 +84,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Null(row.ElementType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a row with too few columns produces a line-numbered error and is excluded from the result.</summary>
|
||||
[Fact]
|
||||
public void Parse_RowWithTooFewColumns_ProducesLineNumberedErrorAndExcludesRow()
|
||||
{
|
||||
@@ -95,6 +101,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Contains("2", error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a blank attribute name produces a line-numbered error.</summary>
|
||||
[Fact]
|
||||
public void Parse_BlankAttributeName_ProducesLineNumberedError()
|
||||
{
|
||||
@@ -107,6 +114,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Contains("2", error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that blank lines are skipped without raising an error.</summary>
|
||||
[Fact]
|
||||
public void Parse_BlankLines_AreSkippedWithoutError()
|
||||
{
|
||||
@@ -121,6 +129,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal(5, result.Rows[1].LineNumber);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing header returns zero rows and a header error.</summary>
|
||||
[Fact]
|
||||
public void Parse_MissingHeader_ReturnsZeroRowsAndHeaderError()
|
||||
{
|
||||
@@ -133,6 +142,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Contains("header", error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the header match is case-insensitive and trims surrounding whitespace.</summary>
|
||||
[Fact]
|
||||
public void Parse_HeaderIsCaseInsensitiveAndTrimsWhitespace()
|
||||
{
|
||||
@@ -147,6 +157,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal("Int32", row.ElementType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that empty input returns a header error.</summary>
|
||||
[Fact]
|
||||
public void Parse_EmptyInput_ReturnsHeaderError()
|
||||
{
|
||||
@@ -156,6 +167,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Single(result.Errors);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unquoted whitespace is trimmed while quoted whitespace is preserved.</summary>
|
||||
[Fact]
|
||||
public void Parse_UnquotedWhitespace_IsTrimmedButQuotedWhitespacePreserved()
|
||||
{
|
||||
@@ -170,6 +182,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal(" spaced ", result.Rows[1].Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unquoted quote character in the middle of a field is preserved literally.</summary>
|
||||
[Fact]
|
||||
public void Parse_MidFieldUnquotedQuote_IsPreservedAsLiteral()
|
||||
{
|
||||
@@ -186,6 +199,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal("Type", row.ElementType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unterminated quoted field produces a line-numbered error and is excluded from the result.</summary>
|
||||
[Fact]
|
||||
public void Parse_UnterminatedQuotedField_ProducesLineNumberedErrorAndExcludesRow()
|
||||
{
|
||||
@@ -200,6 +214,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Contains("Unterminated", error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a well-formed quoted field containing a comma still parses correctly.</summary>
|
||||
[Fact]
|
||||
public void Parse_WellFormedQuotedFieldWithComma_StillParses()
|
||||
{
|
||||
@@ -216,6 +231,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal("Type", row.ElementType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty quoted field yields a null value.</summary>
|
||||
[Fact]
|
||||
public void Parse_EmptyQuotedField_YieldsNullValue()
|
||||
{
|
||||
@@ -231,6 +247,7 @@ public class OverrideCsvParserTests
|
||||
Assert.Equal("Type", row.ElementType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a quoted field followed by trailing whitespace after the closing quote is accepted.</summary>
|
||||
[Fact]
|
||||
public void Parse_QuotedFieldWithTrailingWhitespaceAfterClose_IsAccepted()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,10 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Scripts;
|
||||
/// <summary>Flow Debugger script entry (design 2026-09-08 §2): the monitored attribute of a ValueChange/Conditional trigger.</summary>
|
||||
public class ScriptTriggerAttributeTests
|
||||
{
|
||||
/// <summary>Verifies the case: write Kinds, return The Monitored Attribute.</summary>
|
||||
/// <param name="kind">The kind test input.</param>
|
||||
/// <param name="config">The config test input.</param>
|
||||
/// <param name="expected">The expected test input.</param>
|
||||
[Theory]
|
||||
[InlineData("ValueChange", """{"attributeName":"Level"}""", "Level")]
|
||||
[InlineData("valuechange", """{"attribute":"Level"}""", "Level")]
|
||||
@@ -17,6 +21,9 @@ public class ScriptTriggerAttributeTests
|
||||
Assert.True(ScriptTriggerAttribute.IsWriteKind(kind));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: other Kinds, return False, even When The Config Names An Attribute.</summary>
|
||||
/// <param name="kind">The kind test input.</param>
|
||||
/// <param name="config">The config test input.</param>
|
||||
[Theory]
|
||||
[InlineData("Call", """{"attributeName":"Level"}""")]
|
||||
[InlineData("Interval", """{"intervalMs":1000}""")]
|
||||
@@ -30,6 +37,9 @@ public class ScriptTriggerAttributeTests
|
||||
Assert.False(ScriptTriggerAttribute.IsWriteKind(kind));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: write Kind, without AReadable Attribute, returns False, but Is Still AWrite Kind.</summary>
|
||||
/// <param name="kind">The kind test input.</param>
|
||||
/// <param name="config">The config test input.</param>
|
||||
[Theory]
|
||||
[InlineData("ValueChange", null)]
|
||||
[InlineData("ValueChange", "")]
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Alarms;
|
||||
|
||||
public class AlarmConditionStateTests
|
||||
{
|
||||
/// <summary>Verifies that a default alarm condition state is normal and unshelved.</summary>
|
||||
[Fact]
|
||||
public void AlarmConditionState_DefaultsAreNormalUnshelved()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public class AlarmConditionStateTests
|
||||
Assert.Equal(0, s.Severity);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native alarm transition carries its source and condition.</summary>
|
||||
[Fact]
|
||||
public void NativeAlarmTransition_CarriesSourceAndCondition()
|
||||
{
|
||||
|
||||
@@ -12,15 +12,18 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
/// </summary>
|
||||
public class AttributeValueCodecTests
|
||||
{
|
||||
/// <summary>Verifies that a string list encodes as a JSON array.</summary>
|
||||
[Fact]
|
||||
public void Encode_StringList_ProducesJsonArray() =>
|
||||
Assert.Equal("[\"WO-1\",\"WO-2\"]",
|
||||
AttributeValueCodec.Encode(new List<string> { "WO-1", "WO-2" }));
|
||||
|
||||
/// <summary>Verifies that a scalar string is encoded unchanged.</summary>
|
||||
[Fact]
|
||||
public void Encode_Scalar_String_ReturnedAsIs() =>
|
||||
Assert.Equal("hello", AttributeValueCodec.Encode("hello"));
|
||||
|
||||
/// <summary>Verifies that a scalar double is encoded using the invariant culture.</summary>
|
||||
[Fact]
|
||||
public void Encode_Scalar_Double_IsInvariant()
|
||||
{
|
||||
@@ -36,19 +39,23 @@ public class AttributeValueCodecTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that encoding null returns null.</summary>
|
||||
[Fact]
|
||||
public void Encode_Null_ReturnsNull() =>
|
||||
Assert.Null(AttributeValueCodec.Encode(null));
|
||||
|
||||
/// <summary>Verifies that an empty list encodes as an empty bracket pair.</summary>
|
||||
[Fact]
|
||||
public void Encode_EmptyList_IsBracketPair() =>
|
||||
Assert.Equal("[]", AttributeValueCodec.Encode(new List<string>()));
|
||||
|
||||
/// <summary>Verifies that a string containing a comma is escaped inside the encoded array.</summary>
|
||||
[Fact]
|
||||
public void Encode_StringWithComma_IsEscaped() =>
|
||||
Assert.Equal("[\"ACME, Inc.\"]",
|
||||
AttributeValueCodec.Encode(new List<string> { "ACME, Inc." }));
|
||||
|
||||
/// <summary>Verifies that a float list survives an encode and decode round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_FloatList()
|
||||
{
|
||||
@@ -57,6 +64,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 1.5f, 2.25f, -3.75f }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a double list is encoded using the invariant culture.</summary>
|
||||
[Fact]
|
||||
public void Encode_DoubleList_IsInvariant()
|
||||
{
|
||||
@@ -73,21 +81,25 @@ public class AttributeValueCodecTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an integer list encodes as native JSON numbers.</summary>
|
||||
[Fact]
|
||||
public void Encode_Int32List_ProducesNativeNumbers() =>
|
||||
Assert.Equal("[10,20,30]",
|
||||
AttributeValueCodec.Encode(new List<int> { 10, 20, 30 }));
|
||||
|
||||
/// <summary>Verifies that a boolean list encodes as native JSON booleans.</summary>
|
||||
[Fact]
|
||||
public void Encode_BoolList_ProducesNativeBooleans() =>
|
||||
Assert.Equal("[true,false]",
|
||||
AttributeValueCodec.Encode(new List<bool> { true, false }));
|
||||
|
||||
/// <summary>Verifies that string elements remain quoted in the encoded array.</summary>
|
||||
[Fact]
|
||||
public void Encode_StringList_StaysQuoted() =>
|
||||
Assert.Equal("[\"a\",\"b\"]",
|
||||
AttributeValueCodec.Encode(new List<string> { "a", "b" }));
|
||||
|
||||
/// <summary>Verifies that a date-time list encodes its elements in ISO 8601 form.</summary>
|
||||
[Fact]
|
||||
public void Encode_DateTimeList_IsIso8601()
|
||||
{
|
||||
@@ -98,6 +110,7 @@ public class AttributeValueCodecTests
|
||||
Assert.DoesNotContain("06/16/2026", json);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the old string form of a float list still decodes.</summary>
|
||||
[Fact]
|
||||
public void Decode_OldStringFloatForm_BackwardCompatible()
|
||||
{
|
||||
@@ -105,6 +118,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 1.5f, 2.25f }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the old string form of a date-time list still decodes.</summary>
|
||||
[Fact]
|
||||
public void Decode_OldStringDateTimeForm_BackwardCompatible()
|
||||
{
|
||||
@@ -115,6 +129,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new DateTime(2026, 6, 16, 12, 30, 45, DateTimeKind.Unspecified), back[0]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the native integer array form decodes correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_NewNativeIntForm_Parses()
|
||||
{
|
||||
@@ -122,6 +137,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 10, 20 }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the old string form of an integer list still decodes.</summary>
|
||||
[Fact]
|
||||
public void Decode_OldStringIntForm_BackwardCompatible()
|
||||
{
|
||||
@@ -129,6 +145,8 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 10, 20 }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that both the native and the string boolean array forms decode.</summary>
|
||||
/// <param name="json">The encoded boolean array to decode.</param>
|
||||
[Theory]
|
||||
[InlineData("[true,false]")]
|
||||
[InlineData("[\"True\",\"False\"]")]
|
||||
@@ -138,6 +156,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { true, false }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an integer list survives an encode and decode round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_Int32List()
|
||||
{
|
||||
@@ -146,6 +165,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 1, 2, 3 }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a double list round-trips regardless of the current culture.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DoubleList_IsCultureInvariant()
|
||||
{
|
||||
@@ -163,6 +183,7 @@ public class AttributeValueCodecTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a boolean list survives an encode and decode round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_BoolList()
|
||||
{
|
||||
@@ -171,6 +192,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { true, false, true }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a date-time list round-trips through its ISO 8601 form.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DateTimeList_Iso8601()
|
||||
{
|
||||
@@ -181,6 +203,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { a, b }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a JSON string array decodes to a list of strings.</summary>
|
||||
[Fact]
|
||||
public void Decode_StringArray_ProducesListOfString()
|
||||
{
|
||||
@@ -188,38 +211,48 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { "a", "b" }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that decoding a scalar value returns it as a string.</summary>
|
||||
[Fact]
|
||||
public void Decode_Scalar_ReturnsString() =>
|
||||
Assert.Equal("42", AttributeValueCodec.Decode("42", DataType.Int32, null));
|
||||
|
||||
/// <summary>Verifies that decoding a null list value returns null.</summary>
|
||||
[Fact]
|
||||
public void Decode_List_NullValue_ReturnsNull() =>
|
||||
Assert.Null(AttributeValueCodec.Decode(null, DataType.List, DataType.String));
|
||||
|
||||
/// <summary>Verifies that decoding an empty list value returns null.</summary>
|
||||
[Fact]
|
||||
public void Decode_List_EmptyValue_ReturnsNull() =>
|
||||
Assert.Null(AttributeValueCodec.Decode("", DataType.List, DataType.String));
|
||||
|
||||
/// <summary>Verifies that malformed JSON throws when decoding a list.</summary>
|
||||
[Fact]
|
||||
public void Decode_MalformedJson_Throws() =>
|
||||
Assert.Throws<FormatException>(() =>
|
||||
AttributeValueCodec.Decode("not json", DataType.List, DataType.String));
|
||||
|
||||
/// <summary>Verifies that an element that cannot be parsed throws when decoding a list.</summary>
|
||||
[Fact]
|
||||
public void Decode_UnparseableElement_Throws() =>
|
||||
Assert.Throws<FormatException>(() =>
|
||||
AttributeValueCodec.Decode("[\"abc\"]", DataType.List, DataType.Int32));
|
||||
|
||||
/// <summary>Verifies that a null element throws when decoding a list.</summary>
|
||||
[Fact]
|
||||
public void Decode_NullElement_Throws() =>
|
||||
Assert.Throws<FormatException>(() =>
|
||||
AttributeValueCodec.Decode("[null]", DataType.List, DataType.String));
|
||||
|
||||
/// <summary>Verifies that decoding a list without an element type throws.</summary>
|
||||
[Fact]
|
||||
public void Decode_List_WithoutElementType_Throws() =>
|
||||
Assert.Throws<FormatException>(() =>
|
||||
AttributeValueCodec.Decode("[\"a\"]", DataType.List, null));
|
||||
|
||||
/// <summary>Verifies that only scalar data types are valid list element types.</summary>
|
||||
/// <param name="t">The data type to check.</param>
|
||||
/// <param name="expected">Whether the type is expected to be a valid element type.</param>
|
||||
[Theory]
|
||||
[InlineData(DataType.String, true)]
|
||||
[InlineData(DataType.Int32, true)]
|
||||
@@ -237,6 +270,9 @@ public class AttributeValueCodecTests
|
||||
// string-input path in Decode (JSON elements): here elements arrive as boxed
|
||||
// CLR values and are converted via Convert.ToXxx (or parsed when string).
|
||||
|
||||
/// <summary>Verifies that each scalar data type maps to its expected CLR type.</summary>
|
||||
/// <param name="t">The scalar data type to map.</param>
|
||||
/// <param name="expected">The expected CLR type.</param>
|
||||
[Theory]
|
||||
[InlineData(DataType.String, typeof(string))]
|
||||
[InlineData(DataType.Int32, typeof(int))]
|
||||
@@ -247,10 +283,12 @@ public class AttributeValueCodecTests
|
||||
public void ElementClrType_MapsEachScalar(DataType t, Type expected) =>
|
||||
Assert.Equal(expected, AttributeValueCodec.ElementClrType(t));
|
||||
|
||||
/// <summary>Verifies that a data type that is not a valid element type is rejected.</summary>
|
||||
[Fact]
|
||||
public void ElementClrType_RejectsNonElementType() =>
|
||||
Assert.Throws<FormatException>(() => AttributeValueCodec.ElementClrType(DataType.List));
|
||||
|
||||
/// <summary>Verifies that coercing an enumerable builds a list of the requested element type.</summary>
|
||||
[Fact]
|
||||
public void CoerceEnumerable_BuildsTypedList()
|
||||
{
|
||||
@@ -259,16 +297,18 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 10, 20, 30 }, typed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that elements of mixed CLR types are converted to the requested element type.</summary>
|
||||
[Fact]
|
||||
public void CoerceEnumerable_ConvertsMixedClrElementTypes()
|
||||
{
|
||||
// Ints and numeric strings both coerce to List<double> via Convert/parse —
|
||||
// the object-input behavior InstanceActor's MV-8 coercion depends on.
|
||||
// the object-input behavior the Instance Actor's coercion depends on.
|
||||
var list = AttributeValueCodec.CoerceEnumerable(new object[] { 1, "2.5", 3.75 }, DataType.Double);
|
||||
var typed = Assert.IsType<List<double>>(list);
|
||||
Assert.Equal(new[] { 1.0, 2.5, 3.75 }, typed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that date-time elements round-trip through the invariant culture during coercion.</summary>
|
||||
[Fact]
|
||||
public void CoerceEnumerable_DateTimeRoundtripsViaInvariantCulture()
|
||||
{
|
||||
@@ -279,6 +319,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(when, typed[0]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that coercion throws for an unsupported element type.</summary>
|
||||
[Fact]
|
||||
public void CoerceEnumerable_ThrowsOnUnsupportedElementType() =>
|
||||
Assert.Throws<FormatException>(
|
||||
|
||||
@@ -40,6 +40,7 @@ public class AuditDetailsCodecTests
|
||||
// Round-trip: all non-null fields survive
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that details with every field populated survive a serialize and deserialize round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_AllFields_Populated()
|
||||
{
|
||||
@@ -67,6 +68,7 @@ public class AuditDetailsCodecTests
|
||||
// Round-trip: all-null record
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that details whose optional fields are all null round trip to empty details.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_AllNullOptionals_ReturnsEmptyDetails()
|
||||
{
|
||||
@@ -97,6 +99,7 @@ public class AuditDetailsCodecTests
|
||||
// Null / empty JSON deserialize → empty AuditDetails (no throw)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that deserializing null returns empty details.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_Null_ReturnsEmpty()
|
||||
{
|
||||
@@ -105,6 +108,7 @@ public class AuditDetailsCodecTests
|
||||
Assert.Null(d.Channel);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deserializing an empty string returns empty details.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_EmptyString_ReturnsEmpty()
|
||||
{
|
||||
@@ -117,6 +121,7 @@ public class AuditDetailsCodecTests
|
||||
// Determinism: same input → byte-identical JSON across two calls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that serializing the same input repeatedly produces byte-identical output.</summary>
|
||||
[Fact]
|
||||
public void Serialize_SameInput_ProducesByteIdenticalOutput_AcrossCalls()
|
||||
{
|
||||
@@ -135,6 +140,7 @@ public class AuditDetailsCodecTests
|
||||
// null fields omitted, PayloadTruncated=false present because it is non-nullable bool)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that serializing a known input produces the expected JSON.</summary>
|
||||
[Fact]
|
||||
public void Serialize_KnownInput_MatchesExpectedJson()
|
||||
{
|
||||
@@ -178,6 +184,7 @@ public class AuditDetailsCodecTests
|
||||
// Redaction marker preserved verbatim (UnsafeRelaxedJsonEscaping must not mangle <>)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a redaction marker is preserved verbatim through serialization.</summary>
|
||||
[Fact]
|
||||
public void Serialize_RedactionMarker_IsPreservedVerbatim()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,10 @@ public class AuditFieldBuildersTests
|
||||
// BuildAction: "{channel}.{kind}"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that building an action yields the channel and kind joined by a dot.</summary>
|
||||
/// <param name="channel">The audit channel.</param>
|
||||
/// <param name="kind">The audit kind.</param>
|
||||
/// <param name="expected">The expected action string.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditChannel.ApiOutbound, AuditKind.ApiCall, "ApiOutbound.ApiCall")]
|
||||
[InlineData(AuditChannel.ApiOutbound, AuditKind.ApiCallCached, "ApiOutbound.ApiCallCached")]
|
||||
@@ -32,6 +36,9 @@ public class AuditFieldBuildersTests
|
||||
// BuildCategory: channel.ToString()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that building a category yields the channel name.</summary>
|
||||
/// <param name="channel">The audit channel.</param>
|
||||
/// <param name="expected">The expected category string.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditChannel.ApiOutbound, "ApiOutbound")]
|
||||
[InlineData(AuditChannel.DbOutbound, "DbOutbound")]
|
||||
|
||||
@@ -14,6 +14,8 @@ public class AuditOutcomeProjectorTests
|
||||
// InboundAuthFailure → Denied, regardless of status (checked FIRST)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an inbound auth failure always projects to a denied outcome, whatever the status.</summary>
|
||||
/// <param name="status">The audit status under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Submitted)]
|
||||
[InlineData(AuditStatus.Forwarded)]
|
||||
@@ -33,6 +35,8 @@ public class AuditOutcomeProjectorTests
|
||||
// Delivered → Success (for all non-auth-failure kinds)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a delivered status projects to success for every kind other than an auth failure.</summary>
|
||||
/// <param name="kind">The audit kind under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditKind.ApiCall)]
|
||||
[InlineData(AuditKind.ApiCallCached)]
|
||||
@@ -53,6 +57,8 @@ public class AuditOutcomeProjectorTests
|
||||
// Failure statuses → Failure outcome
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that failed, parked and discarded statuses project to a failure outcome.</summary>
|
||||
/// <param name="status">The failure status under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Failed)]
|
||||
[InlineData(AuditStatus.Parked)]
|
||||
@@ -65,6 +71,8 @@ public class AuditOutcomeProjectorTests
|
||||
}
|
||||
|
||||
// InboundAuthFailure still wins over failure statuses
|
||||
/// <summary>Verifies that an inbound auth failure projects to denied even when the status is a failure status.</summary>
|
||||
/// <param name="status">The failure status under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Failed)]
|
||||
[InlineData(AuditStatus.Parked)]
|
||||
@@ -79,6 +87,8 @@ public class AuditOutcomeProjectorTests
|
||||
// In-progress / short-circuit statuses → Success
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that in-progress and skipped statuses project to a success outcome.</summary>
|
||||
/// <param name="status">The in-progress status under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Submitted)]
|
||||
[InlineData(AuditStatus.Forwarded)]
|
||||
@@ -94,6 +104,10 @@ public class AuditOutcomeProjectorTests
|
||||
// Full status × non-auth-failure kind cross-product (spot-check each status once more)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that each status and kind pair projects to the outcome in the specification table.</summary>
|
||||
/// <param name="status">The audit status under test.</param>
|
||||
/// <param name="kind">The audit kind under test.</param>
|
||||
/// <param name="expected">The expected projected outcome.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Submitted, AuditKind.DbWrite, AuditOutcome.Success)]
|
||||
[InlineData(AuditStatus.Forwarded, AuditKind.CachedSubmit, AuditOutcome.Success)]
|
||||
|
||||
+4
@@ -15,6 +15,8 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Audit;
|
||||
/// </summary>
|
||||
public class ScadaBridgeAuditEventFactoryActorTests
|
||||
{
|
||||
/// <summary>Verifies that system originated emit, preserves caller actor, verbatim.</summary>
|
||||
/// <param name="systemActor">Test case input.</param>
|
||||
[Theory]
|
||||
// Mirrors the literal system actors the outbound emitters pass:
|
||||
// NotificationOutboxActor → "system"; AuditingDbCommand → the source script.
|
||||
@@ -33,6 +35,7 @@ public class ScadaBridgeAuditEventFactoryActorTests
|
||||
Assert.Equal(systemActor, evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that authenticated emit, preserves caller actor, verbatim.</summary>
|
||||
[Fact]
|
||||
public void AuthenticatedEmit_PreservesCallerActor_Verbatim()
|
||||
{
|
||||
@@ -47,6 +50,7 @@ public class ScadaBridgeAuditEventFactoryActorTests
|
||||
Assert.Equal("alice", evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that null actor, maps to empty string, on canonical record.</summary>
|
||||
[Fact]
|
||||
public void NullActor_MapsToEmptyString_OnCanonicalRecord()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M8): tests for the shared multi-value query-param parsers used by
|
||||
/// Tests for the shared multi-value query-param parsers used by
|
||||
/// the ManagementService + CentralUI audit endpoints and the <c>AuditLogPage</c>
|
||||
/// drill-in parser. Two contracts are under test:
|
||||
/// <list type="bullet">
|
||||
@@ -18,18 +18,21 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
/// </summary>
|
||||
public class AuditQueryParamParsersTests
|
||||
{
|
||||
/// <summary>Verifies that parse enum list null input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_NullInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumList<AuditChannel>(null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list empty input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_EmptyInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumList<AuditChannel>(Array.Empty<string?>()));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list all values valid parses everything.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_AllValuesValid_ParsesEverything()
|
||||
{
|
||||
@@ -38,6 +41,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.DbOutbound }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list is case insensitive.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_IsCaseInsensitive()
|
||||
{
|
||||
@@ -45,6 +49,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list drops unparseable element keeps the rest.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_DropsUnparseableElement_KeepsTheRest()
|
||||
{
|
||||
@@ -53,6 +58,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.Notification }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list all values unparseable returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_AllValuesUnparseable_ReturnsNull()
|
||||
{
|
||||
@@ -65,18 +71,22 @@ public class AuditQueryParamParsersTests
|
||||
// case-SENSITIVE AcceptOnlyFromAmong guard and still fail loudly on garbage.
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that parse enum list strict null input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_NullInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumListStrict<AuditStatus>(null, "status"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict empty input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_EmptyInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumListStrict<AuditStatus>(Array.Empty<string?>(), "status"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict is case insensitive and trims.</summary>
|
||||
/// <param name="raw">The raw value.</param>
|
||||
[Theory]
|
||||
[InlineData("delivered")]
|
||||
[InlineData("DELIVERED")]
|
||||
@@ -88,6 +98,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditStatus.Delivered }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict all values valid parses everything.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_AllValuesValid_ParsesEverything()
|
||||
{
|
||||
@@ -96,6 +107,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.DbOutbound }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict unrecognised value throws naming the valid set.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_UnrecognisedValue_ThrowsNamingTheValidSet()
|
||||
{
|
||||
@@ -113,6 +125,8 @@ public class AuditQueryParamParsersTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict numeric string is rejected.</summary>
|
||||
/// <param name="raw">The raw value.</param>
|
||||
[Theory]
|
||||
// "7" is out of range; "3" lands on a REAL member. Enum.TryParse accepts BOTH —
|
||||
// a filter is a name-based contract, never an ordinal one, so both are rejected.
|
||||
@@ -128,6 +142,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Contains(raw, ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict blank elements are still dropped silently.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_BlankElements_AreStillDroppedSilently()
|
||||
{
|
||||
@@ -138,6 +153,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditStatus.Delivered }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict all blank returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_AllBlank_ReturnsNull()
|
||||
{
|
||||
@@ -146,8 +162,7 @@ public class AuditQueryParamParsersTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Comma-joined single token (arch-review 2026-08-19 P6-3, register row 99
|
||||
// closure defect): a bare Enum.TryParse splits and bitwise-ORs a comma list
|
||||
// Comma-joined single token (arch-review 2026-08-19 closure defect): a bare Enum.TryParse splits and bitwise-ORs a comma list
|
||||
// inside ONE token for ANY enum, not just [Flags] ones. AuditStatus is dense
|
||||
// and 0-based, so the OR'd composite almost always collides with a REAL, but
|
||||
// WRONG, member. Values arriving as separate tokens (repeated query param /
|
||||
@@ -157,6 +172,7 @@ public class AuditQueryParamParsersTests
|
||||
// instead RETURNS the wrong composite value below.
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that parse enum list strict comma joined two names is rejected not silently orded into wrong member.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_CommaJoinedTwoNames_IsRejected_NotSilentlyOrdedIntoWrongMember()
|
||||
{
|
||||
@@ -169,6 +185,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Contains("status", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict comma joined name and ordinal is rejected not smuggled past numeric guard.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_CommaJoinedNameAndOrdinal_IsRejected_NotSmuggledPastNumericGuard()
|
||||
{
|
||||
@@ -181,6 +198,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Contains("Failed,7", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict comma joined names drop a member is rejected.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_CommaJoinedNamesDropAMember_IsRejected()
|
||||
{
|
||||
@@ -192,6 +210,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Contains("Submitted,Parked", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict repeated param tokens of valid names still parse independently.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_RepeatedParamTokensOfValidNames_StillParseIndependently()
|
||||
{
|
||||
@@ -205,12 +224,14 @@ public class AuditQueryParamParsersTests
|
||||
result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse string list null input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseStringList_NullInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseStringList(null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse string list trims values and drops blanks.</summary>
|
||||
[Fact]
|
||||
public void ParseStringList_TrimsValuesAndDropsBlanks()
|
||||
{
|
||||
@@ -219,6 +240,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { "site-1", "site-2" }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse string list all blank returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseStringList_AllBlank_ReturnsNull()
|
||||
{
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
|
||||
|
||||
public class MxGatewayEndpointConfigSerializerTests
|
||||
{
|
||||
/// <summary>Verifies that serializing and then deserializing round-trips every field.</summary>
|
||||
[Fact]
|
||||
public void Serialize_then_Deserialize_round_trips_all_fields()
|
||||
{
|
||||
@@ -33,6 +34,8 @@ public class MxGatewayEndpointConfigSerializerTests
|
||||
Assert.Equal(original.ReadTimeoutMs, round.ReadTimeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null, blank or malformed JSON document deserializes to the default configuration.</summary>
|
||||
/// <param name="json">The JSON document to deserialize.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -46,6 +49,7 @@ public class MxGatewayEndpointConfigSerializerTests
|
||||
Assert.Equal(def.ReadTimeoutMs, result.ReadTimeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that converting to a flat dictionary and back round-trips the configuration.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_FromFlatDict_round_trips()
|
||||
{
|
||||
@@ -74,6 +78,7 @@ public class MxGatewayEndpointConfigSerializerTests
|
||||
Assert.Equal(original.ReadTimeoutMs, round.ReadTimeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an invalid numeric value in a flat dictionary falls back to the default.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_invalid_numeric_falls_back_to_default()
|
||||
{
|
||||
|
||||
+37
-2
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
|
||||
|
||||
public class OpcUaEndpointConfigSerializerTests
|
||||
{
|
||||
/// <summary>Verifies that a typed config round-trips through serialization with all fields preserved.</summary>
|
||||
[Fact]
|
||||
public void Serialize_TypedRoundtrip_PreservesAllFields()
|
||||
{
|
||||
@@ -44,6 +45,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(60, round.Heartbeat.MaxSilenceSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that serialization uses camelCase property names.</summary>
|
||||
[Fact]
|
||||
public void Serialize_UsesCamelCase()
|
||||
{
|
||||
@@ -53,6 +55,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.DoesNotContain("\"EndpointUrl\"", json);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the security mode is serialized as a camelCase string.</summary>
|
||||
[Fact]
|
||||
public void Serialize_SecurityModeAsCamelCaseString()
|
||||
{
|
||||
@@ -63,6 +66,8 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("signAndEncrypt", doc.RootElement.GetProperty("securityMode").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null or empty input deserializes to the default config.</summary>
|
||||
/// <param name="input">The null, empty or whitespace JSON input.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -77,6 +82,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Null(config.Heartbeat);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a legacy flat dictionary is flagged as legacy and its fields are populated.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_LegacyFlatDict_IsLegacyTrue_PopulatesFields()
|
||||
{
|
||||
@@ -105,6 +111,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(20, config.Heartbeat.MaxSilenceSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a legacy dictionary with a PascalCase <c>EndpointUrl</c> key is deserialized correctly.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_LegacyWithEndpointUrlPascalKey_Works()
|
||||
{
|
||||
@@ -114,6 +121,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("opc.tcp://x:4840", config.EndpointUrl);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a legacy dictionary with mixed value types preserves all fields.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_LegacyWithMixedTypeValues_PreservesAllFields()
|
||||
{
|
||||
@@ -135,6 +143,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(1000, config.PublishingIntervalMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a parsed legacy config reports a legacy status.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_LegacyParsed_StatusIsLegacy()
|
||||
{
|
||||
@@ -145,6 +154,8 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(OpcUaConfigParseStatus.Legacy, result.Status);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that malformed input is reported as malformed rather than legacy.</summary>
|
||||
/// <param name="input">The malformed JSON input.</param>
|
||||
[Theory]
|
||||
[InlineData("not json at all")]
|
||||
[InlineData("[1,2,3]")]
|
||||
@@ -159,6 +170,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("", result.Config.EndpointUrl);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an object without an endpoint URL is parsed as legacy.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_ObjectWithoutEndpointUrl_ParsesAsLegacy()
|
||||
{
|
||||
@@ -168,8 +180,9 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.True(result.IsLegacy);
|
||||
}
|
||||
|
||||
// ── Commons-014 regression: a corrupt typed row must not be mislabelled Legacy ──
|
||||
// ── Regression: a corrupt typed row must not be mislabelled Legacy ──
|
||||
|
||||
/// <summary>Verifies that a typed-shaped row with an invalid enum value is reported as malformed rather than legacy.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_TypedShapeWithInvalidEnum_ReportsMalformedNotLegacy()
|
||||
{
|
||||
@@ -184,6 +197,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.False(result.IsLegacy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a typed-shaped row with a wrongly typed field is reported as malformed rather than legacy.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_TypedShapeWithWrongTypeField_ReportsMalformedNotLegacy()
|
||||
{
|
||||
@@ -196,10 +210,11 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.False(result.IsLegacy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a valid typed row is still reported as typed.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_ValidTypedRow_StillReportsTyped()
|
||||
{
|
||||
// Guard: a clean typed row is still classified Typed after the Commons-014 fix.
|
||||
// Guard: a clean typed row is still classified Typed after the fix.
|
||||
var json = """{"endpointUrl":"opc.tcp://x:4840","securityMode":"sign"}""";
|
||||
|
||||
var result = OpcUaEndpointConfigSerializer.Deserialize(json);
|
||||
@@ -209,6 +224,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(OpcUaSecurityMode.Sign, result.Config.SecurityMode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deserializing the result into a two-element deconstruction still works.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_TwoElementDeconstruction_StillWorks()
|
||||
{
|
||||
@@ -219,6 +235,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("opc.tcp://x:4840", config.EndpointUrl);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null heartbeat is omitted from the flat dictionary.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_OmitsNullHeartbeat()
|
||||
{
|
||||
@@ -233,6 +250,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.False(dict.ContainsKey("HeartbeatMaxSilence"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the heartbeat is included in the flat dictionary when set.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_IncludesHeartbeat_WhenSet()
|
||||
{
|
||||
@@ -247,6 +265,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("45", dict["HeartbeatMaxSilence"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building a config from a flat dictionary round-trips all keys.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_RoundTripsAllKeys()
|
||||
{
|
||||
@@ -269,6 +288,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
|
||||
// ── Layer A/B extensions: subscription tuning, auth, deadband ──
|
||||
|
||||
/// <summary>Verifies that the new subscription scalar settings round-trip through serialization.</summary>
|
||||
[Fact]
|
||||
public void Serialize_RoundtripsNewSubscriptionScalars()
|
||||
{
|
||||
@@ -291,6 +311,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(OpcUaTimestampsToReturn.Both, round.TimestampsToReturn);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the deadband setting round-trips through serialization.</summary>
|
||||
[Fact]
|
||||
public void Serialize_RoundtripsDeadband()
|
||||
{
|
||||
@@ -312,6 +333,12 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(2.5, round.Deadband.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the user identity settings round-trip through serialization for each token type.</summary>
|
||||
/// <param name="tokenType">The user token type.</param>
|
||||
/// <param name="user">The user name.</param>
|
||||
/// <param name="pass">The password.</param>
|
||||
/// <param name="certPath">The user certificate path.</param>
|
||||
/// <param name="certPass">The user certificate password.</param>
|
||||
[Theory]
|
||||
[InlineData(OpcUaUserTokenType.UsernamePassword, "user1", "pass1", "", "")]
|
||||
[InlineData(OpcUaUserTokenType.X509Certificate, "", "", "/etc/pki/client.pfx", "pfxpass")]
|
||||
@@ -343,6 +370,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(certPass, round.UserIdentity.CertificatePassword);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null user identity and deadband are omitted from the typed JSON.</summary>
|
||||
[Fact]
|
||||
public void Serialize_NullUserIdentityAndDeadband_OmittedFromTypedJson()
|
||||
{
|
||||
@@ -356,6 +384,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Null(round.Deadband);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the new scalar settings are included in the flat dictionary.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_IncludesNewScalars()
|
||||
{
|
||||
@@ -375,6 +404,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("Server", dict["TimestampsToReturn"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null user identity and deadband are omitted from the flat dictionary.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_OmitsNullUserIdentityAndDeadband()
|
||||
{
|
||||
@@ -387,6 +417,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.False(dict.ContainsKey("Deadband.Value"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the user identity is included in the flat dictionary when set.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_IncludesUserIdentity_WhenSet()
|
||||
{
|
||||
@@ -407,6 +438,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("secret", dict["UserIdentity.Password"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the deadband is included in the flat dictionary when set.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_IncludesDeadband_WhenSet()
|
||||
{
|
||||
@@ -421,6 +453,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("1.5", dict["Deadband.Value"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building a config from a flat dictionary materializes the user identity.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_MaterializesUserIdentity()
|
||||
{
|
||||
@@ -439,6 +472,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("hunter2", config.UserIdentity.Password);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building a config from a flat dictionary materializes the deadband.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_MaterializesDeadband()
|
||||
{
|
||||
@@ -455,6 +489,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(0.25, config.Deadband.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an anonymous token type still materializes the user identity from a flat dictionary.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_AnonymousTokenTypeStillMaterializesUserIdentity()
|
||||
{
|
||||
|
||||
+6
@@ -4,6 +4,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
|
||||
|
||||
public class OpcUaReferenceFormTests
|
||||
{
|
||||
/// <summary>Verifies that OPC UA reference strings are classified as durable or non-durable by form.</summary>
|
||||
/// <param name="reference">The OPC UA reference string under test.</param>
|
||||
/// <param name="expected">Whether the reference is expected to be durable.</param>
|
||||
[Theory]
|
||||
// durable → true
|
||||
[InlineData("nsu=https://zb.com/otopcua/raw;s=Line1.Pump.Speed", true)]
|
||||
@@ -16,6 +19,9 @@ public class OpcUaReferenceFormTests
|
||||
[InlineData(" ", true)]
|
||||
[InlineData(null, true)]
|
||||
// bare server-namespace index → false (warn)
|
||||
/// <summary>Verifies that reference forms are classified as durable or not as expected.</summary>
|
||||
/// <param name="reference">The OPC UA reference text, or null.</param>
|
||||
/// <param name="expected">The expected durability classification.</param>
|
||||
[InlineData("ns=2;s=MyDevice.Temperature", false)]
|
||||
[InlineData("ns=1;i=1001", false)]
|
||||
[InlineData("ns=10;s=x", false)]
|
||||
|
||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DynamicJsonElement"/>, including the Commons-002 regression:
|
||||
/// Tests for <see cref="DynamicJsonElement"/>, including the regression where
|
||||
/// a wrapped element must remain valid for deferred (script-time) access even after
|
||||
/// the <see cref="JsonDocument"/> it was parsed from has been disposed.
|
||||
/// </summary>
|
||||
@@ -19,8 +19,9 @@ public class DynamicJsonElementTests
|
||||
// now throw ObjectDisposedException on the first member access.
|
||||
}
|
||||
|
||||
// ── Commons-002 regression: lifetime independence from the source document ──
|
||||
// ── Regression: lifetime independence from the source document ──
|
||||
|
||||
/// <summary>Verifies that member access still works after the source document is disposed.</summary>
|
||||
[Fact]
|
||||
public void MemberAccess_WorksAfterSourceDocumentDisposed()
|
||||
{
|
||||
@@ -30,6 +31,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal(7, (int)obj.id);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that index access still works after the source document is disposed.</summary>
|
||||
[Fact]
|
||||
public void IndexAccess_WorksAfterSourceDocumentDisposed()
|
||||
{
|
||||
@@ -38,6 +40,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal("b", obj.items[1]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nested member access still works after the source document is disposed.</summary>
|
||||
[Fact]
|
||||
public void NestedAccess_WorksAfterSourceDocumentDisposed()
|
||||
{
|
||||
@@ -46,6 +49,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal(42, (int)obj.outer.inner.value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <c>ToString</c> still works after the source document is disposed.</summary>
|
||||
[Fact]
|
||||
public void ToString_WorksAfterSourceDocumentDisposed()
|
||||
{
|
||||
@@ -54,6 +58,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal("site-1", obj.label.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that access survives garbage collection of the source document.</summary>
|
||||
[Fact]
|
||||
public void Access_SurvivesGarbageCollection_OfSourceDocument()
|
||||
{
|
||||
@@ -78,6 +83,7 @@ public class DynamicJsonElementTests
|
||||
|
||||
// ── Basic conversion / access behavior ──────────────────────────
|
||||
|
||||
/// <summary>Verifies that a JSON number converts to an int.</summary>
|
||||
[Fact]
|
||||
public void Convert_NumberToInt()
|
||||
{
|
||||
@@ -85,6 +91,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal(123, (int)obj.n);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a JSON boolean converts to a bool.</summary>
|
||||
[Fact]
|
||||
public void Convert_BoolFromJson()
|
||||
{
|
||||
@@ -92,6 +99,7 @@ public class DynamicJsonElementTests
|
||||
Assert.True((bool)obj.flag);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that accessing a missing member throws a RuntimeBinderException.</summary>
|
||||
[Fact]
|
||||
public void MissingMember_Throws()
|
||||
{
|
||||
@@ -102,8 +110,9 @@ public class DynamicJsonElementTests
|
||||
() => { var _ = obj.doesNotExist; });
|
||||
}
|
||||
|
||||
// ── Commons-006 regression: TryConvert(object) must never null out a present value ──
|
||||
// ── Regression: TryConvert(object) must never null out a present value ──
|
||||
|
||||
/// <summary>Verifies that converting a present value to <c>object</c> returns a non-null result.</summary>
|
||||
[Fact]
|
||||
public void TryConvert_ObjectTarget_OnPresentValue_ReturnsNonNull()
|
||||
{
|
||||
@@ -118,6 +127,7 @@ public class DynamicJsonElementTests
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that converting a JSON null to <c>object</c> succeeds with a null result.</summary>
|
||||
[Fact]
|
||||
public void TryConvert_ObjectTarget_OnJsonNull_ReturnsNull()
|
||||
{
|
||||
@@ -131,6 +141,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that converting an unconvertible value to a non-object target reports failure.</summary>
|
||||
[Fact]
|
||||
public void TryConvert_NonObjectTarget_OnUnconvertibleValue_ReportsFailure()
|
||||
{
|
||||
@@ -145,8 +156,9 @@ public class DynamicJsonElementTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ── Commons-013 regression: integral index values other than int must work ──
|
||||
// ── Regression: integral index values other than int must work ──
|
||||
|
||||
/// <summary>Verifies that index access works with a long index.</summary>
|
||||
[Fact]
|
||||
public void IndexAccess_WithLongIndex_Works()
|
||||
{
|
||||
@@ -158,16 +170,20 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal("b", obj.items[idx]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that index access works with an index derived from a wrapped JSON number.</summary>
|
||||
[Fact]
|
||||
public void IndexAccess_WithIndexDerivedFromWrappedJsonNumber_Works()
|
||||
{
|
||||
// The exact failing case from Commons-013: count is a wrapped JSON number
|
||||
// The exact failing case from that regression: count is a wrapped JSON number
|
||||
// (unwrapped as long), so count - 1 is a long.
|
||||
dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ], "count": 3 }""");
|
||||
|
||||
Assert.Equal("c", obj.items[obj.count - 1]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that index access works with integral indexes that widen to an int.</summary>
|
||||
/// <param name="index">The test input.</param>
|
||||
/// <param name="expected">The test input.</param>
|
||||
[Theory]
|
||||
[InlineData((byte)0, "a")]
|
||||
[InlineData((short)1, "b")]
|
||||
@@ -178,6 +194,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal(expected, obj.items[index]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a long index outside the array range throws.</summary>
|
||||
[Fact]
|
||||
public void IndexAccess_WithLongIndexOutOfRange_Throws()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
public class EnumTests
|
||||
{
|
||||
/// <summary>Verifies that an enum has exactly the expected member names, in order.</summary>
|
||||
/// <param name="enumType">The enum type under test.</param>
|
||||
/// <param name="expectedNames">The expected member names in declaration order.</param>
|
||||
[Theory]
|
||||
[InlineData(typeof(DataType), new[] { "Boolean", "Int32", "Float", "Double", "String", "DateTime", "Binary", "List" })]
|
||||
[InlineData(typeof(InstanceState), new[] { "NotDeployed", "Enabled", "Disabled" })]
|
||||
@@ -20,6 +23,8 @@ public class EnumTests
|
||||
Assert.Equal(expectedNames, actualNames);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an enum type has a singular name rather than a plural one.</summary>
|
||||
/// <param name="enumType">The enum type under test.</param>
|
||||
[Theory]
|
||||
[InlineData(typeof(DataType))]
|
||||
[InlineData(typeof(InstanceState))]
|
||||
|
||||
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Asserts the exact member sets of the Audit Log (#23) enums.
|
||||
/// Asserts the exact member sets of the Audit Log enums.
|
||||
/// Lock-in tests; any addition/removal/rename is a deliberate design change
|
||||
/// that must come with a corresponding update to alog.md §4.
|
||||
/// </summary>
|
||||
public class AuditEnumTests
|
||||
{
|
||||
/// <summary>Verifies that audit channel has exactly expected members.</summary>
|
||||
[Fact]
|
||||
public void AuditChannel_HasExactlyExpectedMembers()
|
||||
{
|
||||
@@ -22,6 +23,7 @@ public class AuditEnumTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that audit kind has exactly twenty two expected members.</summary>
|
||||
[Fact]
|
||||
public void AuditKind_HasExactlyTwentyTwoExpectedMembers()
|
||||
{
|
||||
@@ -45,6 +47,7 @@ public class AuditEnumTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that audit status has exactly eight expected members.</summary>
|
||||
[Fact]
|
||||
public void AuditStatus_HasExactlyEightExpectedMembers()
|
||||
{
|
||||
@@ -62,6 +65,7 @@ public class AuditEnumTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that audit forward state has exactly expected members.</summary>
|
||||
[Fact]
|
||||
public void AuditForwardState_HasExactlyExpectedMembers()
|
||||
{
|
||||
|
||||
@@ -4,11 +4,12 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Commons-010: coverage for the small computed-property logic on
|
||||
/// Coverage for the small computed-property logic on
|
||||
/// <see cref="ConfigurationDiff"/> and <see cref="ScriptScope"/>.
|
||||
/// </summary>
|
||||
public class FlatteningAndScriptScopeTests
|
||||
{
|
||||
/// <summary>Verifies that a configuration diff with no changes reports HasChanges as false.</summary>
|
||||
[Fact]
|
||||
public void ConfigurationDiff_NoChanges_HasChangesIsFalse()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public class FlatteningAndScriptScopeTests
|
||||
Assert.False(diff.HasChanges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a configuration diff containing an attribute change reports HasChanges as true.</summary>
|
||||
[Fact]
|
||||
public void ConfigurationDiff_WithAttributeChange_HasChangesIsTrue()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ public class FlatteningAndScriptScopeTests
|
||||
Assert.True(diff.HasChanges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a root script scope has no parent.</summary>
|
||||
[Fact]
|
||||
public void ScriptScope_Root_HasNoParent()
|
||||
{
|
||||
@@ -39,6 +42,7 @@ public class FlatteningAndScriptScopeTests
|
||||
Assert.Null(ScriptScope.Root.ParentPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script scope with a parent path reports HasParent as true.</summary>
|
||||
[Fact]
|
||||
public void ScriptScope_WithParentPath_HasParentIsTrue()
|
||||
{
|
||||
|
||||
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #36: <see cref="HandshakeResult"/> lets a script tell WHICH leg of a reset-aware
|
||||
/// <see cref="HandshakeResult"/> lets a script tell WHICH leg of a reset-aware
|
||||
/// handshake failed without naming the <see cref="HandshakePhase"/> enum (script bodies
|
||||
/// import only System/System.Collections.Generic/System.Linq/System.Threading.Tasks).
|
||||
/// </summary>
|
||||
public class HandshakeResultTests
|
||||
{
|
||||
/// <summary>Verifies that ResetFailed is true only when the reset phase did not match.</summary>
|
||||
[Fact]
|
||||
public void ResetFailed_IsTrue_OnlyForAnUnmatchedResetPhase()
|
||||
{
|
||||
|
||||
+16
-1
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.InboundApi;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the M9-T32b JSON-Schema <c>$ref</c> resolver: the caller-supplied
|
||||
/// Tests for the JSON-Schema <c>$ref</c> resolver: the caller-supplied
|
||||
/// resolution seam (<c>lib:Name</c> pointer convention), cycle/depth guarding,
|
||||
/// and dangling-ref reporting. The resolver is OPTIONAL — schemas without a
|
||||
/// <c>$ref</c> parse exactly as before (verified by the no-ref-unchanged tests).
|
||||
@@ -13,6 +13,7 @@ public class InboundApiSchemaRefTests
|
||||
{
|
||||
// ── Pointer convention: {"$ref":"lib:Name"} resolves through the seam ──────
|
||||
|
||||
/// <summary>Verifies that a library reference resolves to the referenced shape.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_LibRef_ResolvesToReferencedShape()
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Equal("integer", field.Schema.Type);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a reference on a nested field is resolved inline.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_NestedFieldRef_ResolvesInline()
|
||||
{
|
||||
@@ -52,6 +54,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Equal("zip", Assert.Single(shipTo.Schema.Fields).Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing a library reference with a resolver yields the referenced shape.</summary>
|
||||
[Fact]
|
||||
public void Parse_LibRef_WithResolver_ResolvesToReferencedShape()
|
||||
{
|
||||
@@ -68,6 +71,7 @@ public class InboundApiSchemaRefTests
|
||||
|
||||
// ── Dangling ref: resolver returns null → reported, not silently dropped ──
|
||||
|
||||
/// <summary>Verifies that a dangling reference is reported.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DanglingRef_IsReported()
|
||||
{
|
||||
@@ -78,6 +82,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Contains("Missing", result.UnresolvedRefs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a dangling nested reference is reported.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DanglingNestedRef_IsReported()
|
||||
{
|
||||
@@ -88,6 +93,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Contains("Gone", result.UnresolvedRefs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a reference parsed without a resolver is reported as dangling.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_RefWithNoResolver_IsDangling()
|
||||
{
|
||||
@@ -100,6 +106,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Contains("Whatever", result.UnresolvedRefs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing a dangling reference with a resolver throws.</summary>
|
||||
[Fact]
|
||||
public void Parse_DanglingRef_WithResolver_Throws()
|
||||
{
|
||||
@@ -111,6 +118,7 @@ public class InboundApiSchemaRefTests
|
||||
|
||||
// ── Cycle / depth guard: controlled error, no stack overflow ──────────────
|
||||
|
||||
/// <summary>Verifies that a direct reference cycle is reported instead of overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DirectCycle_IsReportedNotStackOverflow()
|
||||
{
|
||||
@@ -128,6 +136,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Contains(result.UnresolvedRefs, r => r.Contains("Foo", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an indirect reference cycle is reported instead of overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_IndirectCycle_IsReportedNotStackOverflow()
|
||||
{
|
||||
@@ -145,6 +154,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.NotEmpty(result.UnresolvedRefs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing a direct cycle with a resolver throws instead of overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void Parse_DirectCycle_WithResolver_ThrowsNotStackOverflow()
|
||||
{
|
||||
@@ -159,6 +169,7 @@ public class InboundApiSchemaRefTests
|
||||
name => schemas.GetValueOrDefault(name)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deep reference chain respects the depth cap without overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DeepRefChain_RespectsDepthCapWithoutOverflow()
|
||||
{
|
||||
@@ -179,6 +190,7 @@ public class InboundApiSchemaRefTests
|
||||
|
||||
// ── Behavior unchanged for schemas WITHOUT a $ref ─────────────────────────
|
||||
|
||||
/// <summary>Verifies that parsing a schema with no references gives the same result as the plain parse.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_NoRef_MatchesParse()
|
||||
{
|
||||
@@ -196,6 +208,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.False(withRefs.Schema.Fields[1].Required);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a schema with no references parses identically with or without a resolver.</summary>
|
||||
[Fact]
|
||||
public void Parse_NoRef_UnchangedWithOrWithoutResolver()
|
||||
{
|
||||
@@ -212,6 +225,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Equal("number", withResolver.Items!.Type);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty input returns a null schema and no references.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_Empty_ReturnsNullSchemaNoRefs()
|
||||
{
|
||||
@@ -222,6 +236,7 @@ public class InboundApiSchemaRefTests
|
||||
|
||||
// ── Diamond: two sibling refs to the SAME name resolve (not a cycle) ───────
|
||||
|
||||
/// <summary>Verifies that sibling references to the same target resolve without being reported as a false cycle.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DiamondSiblingRefs_ResolveBothNotFalseCycle()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
/// </summary>
|
||||
public class PurgeTimerScheduleTests
|
||||
{
|
||||
/// <summary>Verifies that a long interval is capped at the short first tick.</summary>
|
||||
[Fact]
|
||||
public void InitialDelay_LongInterval_IsCappedAtShortFirstTick()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public class PurgeTimerScheduleTests
|
||||
Assert.Equal(PurgeTimerSchedule.ShortFirstTick, PurgeTimerSchedule.InitialDelay(TimeSpan.FromHours(24)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an interval shorter than the short first tick is left unchanged.</summary>
|
||||
[Fact]
|
||||
public void InitialDelay_IntervalShorterThanShortFirstTick_IsUnchanged()
|
||||
{
|
||||
@@ -26,12 +28,14 @@ public class PurgeTimerScheduleTests
|
||||
Assert.Equal(fast, PurgeTimerSchedule.InitialDelay(fast));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an interval equal to the short first tick yields that value.</summary>
|
||||
[Fact]
|
||||
public void InitialDelay_IntervalEqualToShortFirstTick_IsThatValue()
|
||||
{
|
||||
Assert.Equal(PurgeTimerSchedule.ShortFirstTick, PurgeTimerSchedule.InitialDelay(PurgeTimerSchedule.ShortFirstTick));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the short first tick is five minutes.</summary>
|
||||
[Fact]
|
||||
public void ShortFirstTick_IsFiveMinutes()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
public class ResultTests
|
||||
{
|
||||
/// <summary>Verifies that creating a success produces a successful result.</summary>
|
||||
[Fact]
|
||||
public void Success_ShouldCreateSuccessfulResult()
|
||||
{
|
||||
@@ -14,6 +15,7 @@ public class ResultTests
|
||||
Assert.Equal(42, result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a failure produces a failed result.</summary>
|
||||
[Fact]
|
||||
public void Failure_ShouldCreateFailedResult()
|
||||
{
|
||||
@@ -24,6 +26,7 @@ public class ResultTests
|
||||
Assert.Equal("something went wrong", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading the value of a failed result throws.</summary>
|
||||
[Fact]
|
||||
public void Value_OnFailure_ShouldThrow()
|
||||
{
|
||||
@@ -32,6 +35,7 @@ public class ResultTests
|
||||
Assert.Throws<InvalidOperationException>(() => result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading the error of a successful result throws.</summary>
|
||||
[Fact]
|
||||
public void Error_OnSuccess_ShouldThrow()
|
||||
{
|
||||
@@ -40,6 +44,7 @@ public class ResultTests
|
||||
Assert.Throws<InvalidOperationException>(() => result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that matching a successful result invokes the success handler.</summary>
|
||||
[Fact]
|
||||
public void Match_OnSuccess_ShouldCallOnSuccess()
|
||||
{
|
||||
@@ -52,6 +57,7 @@ public class ResultTests
|
||||
Assert.Equal("value=42", output);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that matching a failed result invokes the failure handler.</summary>
|
||||
[Fact]
|
||||
public void Match_OnFailure_ShouldCallOnFailure()
|
||||
{
|
||||
@@ -64,6 +70,7 @@ public class ResultTests
|
||||
Assert.Equal("error=bad", output);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a success result works with a nullable reference type.</summary>
|
||||
[Fact]
|
||||
public void Success_WithNullableReferenceType_ShouldWork()
|
||||
{
|
||||
@@ -73,14 +80,17 @@ public class ResultTests
|
||||
Assert.Equal("hello", result.Value);
|
||||
}
|
||||
|
||||
// ── Commons-011 regression: a failed Result must always carry a message ──
|
||||
// ── Regression: a failed Result must always carry a message ──
|
||||
|
||||
/// <summary>Verifies that creating a failure with a null error throws.</summary>
|
||||
[Fact]
|
||||
public void Failure_WithNullError_ShouldThrow()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => Result<int>.Failure(null!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a failure with a blank error throws.</summary>
|
||||
/// <param name="error">A blank or whitespace-only error message.</param>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
public class RetryPolicyTests
|
||||
{
|
||||
/// <summary>Verifies that retry policy should be immutable record.</summary>
|
||||
[Fact]
|
||||
public void RetryPolicy_ShouldBeImmutableRecord()
|
||||
{
|
||||
@@ -13,6 +14,7 @@ public class RetryPolicyTests
|
||||
Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that retry policy with expression should create new instance.</summary>
|
||||
[Fact]
|
||||
public void RetryPolicy_WithExpression_ShouldCreateNewInstance()
|
||||
{
|
||||
@@ -24,6 +26,7 @@ public class RetryPolicyTests
|
||||
Assert.Equal(original.Delay, modified.Delay);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that retry policy equal values should be equal.</summary>
|
||||
[Fact]
|
||||
public void RetryPolicy_EqualValues_ShouldBeEqual()
|
||||
{
|
||||
|
||||
@@ -4,17 +4,19 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Commons-010: coverage for <see cref="ScriptArgs.Normalize"/> — the script-call
|
||||
/// Coverage for <see cref="ScriptArgs.Normalize"/> — the script-call
|
||||
/// parameter normalizer (dictionary / anonymous-object / primitive-rejection paths).
|
||||
/// </summary>
|
||||
public class ScriptArgsTests
|
||||
{
|
||||
/// <summary>Verifies that normalizing null returns null.</summary>
|
||||
[Fact]
|
||||
public void Normalize_Null_ReturnsNull()
|
||||
{
|
||||
Assert.Null(ScriptArgs.Normalize(null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a read-only dictionary is returned as is.</summary>
|
||||
[Fact]
|
||||
public void Normalize_ReadOnlyDictionary_ReturnedAsIs()
|
||||
{
|
||||
@@ -26,6 +28,7 @@ public class ScriptArgsTests
|
||||
Assert.Same(input, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a plain dictionary is returned as is.</summary>
|
||||
[Fact]
|
||||
public void Normalize_PlainDictionary_ReturnedAsIs()
|
||||
{
|
||||
@@ -39,6 +42,7 @@ public class ScriptArgsTests
|
||||
Assert.Equal(1, result!["a"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the keys of a non-generic dictionary are converted to strings.</summary>
|
||||
[Fact]
|
||||
public void Normalize_NonGenericDictionary_KeysStringified()
|
||||
{
|
||||
@@ -49,6 +53,7 @@ public class ScriptArgsTests
|
||||
Assert.Equal("answer", result!["42"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the properties of an anonymous object become dictionary entries.</summary>
|
||||
[Fact]
|
||||
public void Normalize_AnonymousObject_PropertiesBecomeEntries()
|
||||
{
|
||||
@@ -58,6 +63,8 @@ public class ScriptArgsTests
|
||||
Assert.Equal(3, result["count"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that normalizing a primitive value throws.</summary>
|
||||
/// <param name="primitive">The primitive value to normalize.</param>
|
||||
[Theory]
|
||||
[InlineData(42)]
|
||||
[InlineData(true)]
|
||||
@@ -67,12 +74,14 @@ public class ScriptArgsTests
|
||||
Assert.Throws<ArgumentException>(() => ScriptArgs.Normalize(primitive));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that normalizing a string throws.</summary>
|
||||
[Fact]
|
||||
public void Normalize_String_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => ScriptArgs.Normalize("hello"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that normalizing a decimal throws.</summary>
|
||||
[Fact]
|
||||
public void Normalize_Decimal_Throws()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ public class ScriptParametersTests
|
||||
{
|
||||
// ── Non-nullable scalar Get<T> ──────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting an int returns the value when the stored type is already an int.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_ExactTypeMatch()
|
||||
{
|
||||
@@ -14,6 +15,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int converts a stored long.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_FromLong()
|
||||
{
|
||||
@@ -21,6 +23,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int parses a stored string.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_FromString()
|
||||
{
|
||||
@@ -28,6 +31,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int throws when the key is missing.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_MissingKey_Throws()
|
||||
{
|
||||
@@ -36,6 +40,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("Parameter 'x' not found", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int throws when the stored value is null.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_NullValue_Throws()
|
||||
{
|
||||
@@ -44,6 +49,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("Parameter 'x' value is null", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int throws when the stored value cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_Unparsable_Throws()
|
||||
{
|
||||
@@ -52,6 +58,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("could not be parsed as Int32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string returns a stored string directly.</summary>
|
||||
[Fact]
|
||||
public void Get_String_DirectReturn()
|
||||
{
|
||||
@@ -59,6 +66,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal("hello", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string throws when the stored value is null.</summary>
|
||||
[Fact]
|
||||
public void Get_String_NullValue_Throws()
|
||||
{
|
||||
@@ -67,6 +75,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("value is null", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string converts a stored int.</summary>
|
||||
[Fact]
|
||||
public void Get_String_FromInt()
|
||||
{
|
||||
@@ -74,6 +83,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal("42", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a bool returns true for a stored true value.</summary>
|
||||
[Fact]
|
||||
public void Get_Bool_True()
|
||||
{
|
||||
@@ -81,6 +91,7 @@ public class ScriptParametersTests
|
||||
Assert.True(p.Get<bool>("b"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double converts a stored long.</summary>
|
||||
[Fact]
|
||||
public void Get_Double_FromLong()
|
||||
{
|
||||
@@ -88,6 +99,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42.0, p.Get<double>("d"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double converts a stored float.</summary>
|
||||
[Fact]
|
||||
public void Get_Double_FromFloat()
|
||||
{
|
||||
@@ -95,6 +107,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(3.14, p.Get<double>("d"), 2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a long converts a stored int.</summary>
|
||||
[Fact]
|
||||
public void Get_Long_FromInt()
|
||||
{
|
||||
@@ -102,6 +115,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42L, p.Get<long>("l"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a float converts a stored double.</summary>
|
||||
[Fact]
|
||||
public void Get_Float_FromDouble()
|
||||
{
|
||||
@@ -109,6 +123,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(3.14f, p.Get<float>("f"), 2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a date and time parses a stored string.</summary>
|
||||
[Fact]
|
||||
public void Get_DateTime_FromString()
|
||||
{
|
||||
@@ -117,6 +132,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new DateTime(2026, 3, 22, 10, 30, 0, DateTimeKind.Utc), result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a date and time returns the value when the stored type is already a date and time.</summary>
|
||||
[Fact]
|
||||
public void Get_DateTime_ExactType()
|
||||
{
|
||||
@@ -127,6 +143,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── Nullable scalar Get<T?> ─────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting a nullable int returns null when the key is missing.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_MissingKey_ReturnsNull()
|
||||
{
|
||||
@@ -134,6 +151,7 @@ public class ScriptParametersTests
|
||||
Assert.Null(p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable int returns null when the stored value is null.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_NullValue_ReturnsNull()
|
||||
{
|
||||
@@ -141,6 +159,7 @@ public class ScriptParametersTests
|
||||
Assert.Null(p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable int returns the value when it is valid.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_ValidValue_ReturnsValue()
|
||||
{
|
||||
@@ -148,9 +167,10 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
// Commons-003: a parameter that is *present but unconvertible* is a caller/script
|
||||
// A parameter that is *present but unconvertible* is a caller/script
|
||||
// bug and must throw — not be silently mapped to null (which a script would
|
||||
// misread as "not supplied"). Genuinely absent/null still returns null.
|
||||
/// <summary>Verifies that getting a nullable int throws when the value is present but cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_PresentButUnparsable_Throws()
|
||||
{
|
||||
@@ -159,6 +179,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("could not be parsed as Int32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable int throws when the value is present but overflows.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_PresentButOverflowing_Throws()
|
||||
{
|
||||
@@ -167,6 +188,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("could not be parsed as Int32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable date and time throws when the value is present but cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableDateTime_PresentButUnparsable_Throws()
|
||||
{
|
||||
@@ -174,6 +196,7 @@ public class ScriptParametersTests
|
||||
Assert.Throws<ScriptParameterException>(() => p.Get<DateTime?>("dt"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable double returns the value when it is valid.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableDouble_ValidValue()
|
||||
{
|
||||
@@ -181,6 +204,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(3.14, p.Get<double?>("d"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable bool returns the value when it is valid.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableBool_ValidValue()
|
||||
{
|
||||
@@ -190,6 +214,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── Array Get<T[]> ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting an int array converts a stored list of longs.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_FromListOfLongs()
|
||||
{
|
||||
@@ -198,6 +223,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { 1, 2, 3 }, p.Get<int[]>("arr"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array from an empty list returns an empty array.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_EmptyList()
|
||||
{
|
||||
@@ -205,6 +231,7 @@ public class ScriptParametersTests
|
||||
Assert.Empty(p.Get<int[]>("arr"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array throws when the stored value is not a list.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_NonList_Throws()
|
||||
{
|
||||
@@ -213,6 +240,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("is not a list or array", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array throws with the element index when an element cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_UnparsableElement_ThrowsWithIndex()
|
||||
{
|
||||
@@ -224,6 +252,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("Int32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array throws when an element is null.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_NullElement_Throws()
|
||||
{
|
||||
@@ -233,6 +262,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("element at index 1 is null", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string array converts a stored list of strings.</summary>
|
||||
[Fact]
|
||||
public void Get_StringArray_FromListOfStrings()
|
||||
{
|
||||
@@ -241,6 +271,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { "a", "b", "c" }, p.Get<string[]>("arr"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double array converts a stored list of numbers.</summary>
|
||||
[Fact]
|
||||
public void Get_DoubleArray_FromListOfNumbers()
|
||||
{
|
||||
@@ -249,6 +280,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { 1.1, 2.2, 3.3 }, p.Get<double[]>("arr"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array throws when the key is missing.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_MissingKey_Throws()
|
||||
{
|
||||
@@ -259,6 +291,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── List<T> Get<List<T>> ────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting an int list converts a stored list of longs.</summary>
|
||||
[Fact]
|
||||
public void Get_ListInt_FromListOfLongs()
|
||||
{
|
||||
@@ -268,6 +301,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new List<int> { 10, 20, 30 }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string list converts a stored list of strings.</summary>
|
||||
[Fact]
|
||||
public void Get_ListString_FromListOfStrings()
|
||||
{
|
||||
@@ -276,6 +310,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new List<string> { "x", "y" }, p.Get<List<string>>("items"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int list throws with the element index when an element cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_ListInt_UnparsableElement_ThrowsWithIndex()
|
||||
{
|
||||
@@ -285,6 +320,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("element at index 1", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double list converts a stored list of mixed numeric types.</summary>
|
||||
[Fact]
|
||||
public void Get_ListDouble_FromMixedNumbers()
|
||||
{
|
||||
@@ -299,6 +335,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── Backward compatibility ──────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that the indexer returns the stored value for a key.</summary>
|
||||
[Fact]
|
||||
public void Indexer_Works()
|
||||
{
|
||||
@@ -306,6 +343,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal("val", p["key"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <c>ContainsKey</c> reports whether a key is present.</summary>
|
||||
[Fact]
|
||||
public void ContainsKey_Works()
|
||||
{
|
||||
@@ -314,6 +352,7 @@ public class ScriptParametersTests
|
||||
Assert.False(p.ContainsKey("missing"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <c>TryGetValue</c> returns the stored value for a present key.</summary>
|
||||
[Fact]
|
||||
public void TryGetValue_Works()
|
||||
{
|
||||
@@ -322,6 +361,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, val);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the count reflects the number of stored entries.</summary>
|
||||
[Fact]
|
||||
public void Count_Works()
|
||||
{
|
||||
@@ -329,6 +369,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(2, p.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that enumeration yields every stored entry.</summary>
|
||||
[Fact]
|
||||
public void Enumeration_Works()
|
||||
{
|
||||
@@ -338,6 +379,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { "a", "b" }, keys);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the empty constructor produces an empty dictionary.</summary>
|
||||
[Fact]
|
||||
public void EmptyConstructor_ProducesEmptyDictionary()
|
||||
{
|
||||
@@ -348,6 +390,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── Edge cases ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting a double converts a stored int.</summary>
|
||||
[Fact]
|
||||
public void Get_Double_FromInt()
|
||||
{
|
||||
@@ -357,6 +400,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── JsonElement values (from JSON deserialization) ─────────────
|
||||
|
||||
/// <summary>Verifies that getting an int reads a stored JSON element.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_FromJsonElement()
|
||||
{
|
||||
@@ -366,6 +410,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array reads a stored list of JSON elements.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_FromJsonElementList()
|
||||
{
|
||||
@@ -376,6 +421,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { 10, 20, 30 }, p.Get<int[]>("items"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int list reads a stored list of JSON elements.</summary>
|
||||
[Fact]
|
||||
public void Get_ListInt_FromJsonElementList()
|
||||
{
|
||||
@@ -385,6 +431,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new List<int> { 1, 2, 3 }, p.Get<List<int>>("items"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string reads a stored JSON element.</summary>
|
||||
[Fact]
|
||||
public void Get_String_FromJsonElement()
|
||||
{
|
||||
@@ -394,6 +441,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal("hello", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double reads a stored JSON element.</summary>
|
||||
[Fact]
|
||||
public void Get_Double_FromJsonElement()
|
||||
{
|
||||
@@ -403,6 +451,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(3.14, p.Get<double>("d"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a bool reads a stored JSON element.</summary>
|
||||
[Fact]
|
||||
public void Get_Bool_FromJsonElement()
|
||||
{
|
||||
@@ -412,6 +461,7 @@ public class ScriptParametersTests
|
||||
Assert.True(p.Get<bool>("b"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int throws when the stored long overflows an int.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_OverflowFromLong_Throws()
|
||||
{
|
||||
|
||||
+11
-1
@@ -12,6 +12,7 @@ public class SecretReferenceScannerTests
|
||||
private static DataConnectionArtifact Dc(string name, string? primary, string? backup = null)
|
||||
=> new(name, "MxGateway", primary, backup, 3);
|
||||
|
||||
/// <summary>Verifies that nested secret references are found case-insensitively across the primary and backup configuration.</summary>
|
||||
[Fact]
|
||||
public void FindsNestedRefs_CaseInsensitive_AcrossPrimaryAndBackup()
|
||||
{
|
||||
@@ -23,6 +24,8 @@ public class SecretReferenceScannerTests
|
||||
Assert.Equal(["GW1", "GW2"], refs.Single(r => r.Name == "mxgw-key").ReferencedBy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that values that are not secret references are ignored.</summary>
|
||||
/// <param name="json">The connection JSON that contains no secret reference.</param>
|
||||
[Theory]
|
||||
[InlineData("""{"ApiKey":"${secret:node-local}"}""")]
|
||||
[InlineData("""{"Note":"see secret:x in docs"}""")]
|
||||
@@ -34,6 +37,7 @@ public class SecretReferenceScannerTests
|
||||
public void IgnoresNonReferences(string json)
|
||||
=> Assert.Empty(SecretReferenceScanner.Scan([Dc("C", json)]));
|
||||
|
||||
/// <summary>Verifies that a connection referencing the same secret several times is reported once.</summary>
|
||||
[Fact]
|
||||
public void ReferencedBy_DeduplicatesWhenOneConnectionRefsTheSameSecretMultipleTimes()
|
||||
{
|
||||
@@ -47,8 +51,11 @@ public class SecretReferenceScannerTests
|
||||
Assert.Equal(["GW1"], refs.Single(r => r.Name == "mxgw-key").ReferencedBy);
|
||||
}
|
||||
|
||||
// ── Shared parser (final review M3) ──
|
||||
// ── Shared parser ──
|
||||
|
||||
/// <summary>Verifies that the parser accepts trimmed, case-insensitive secret references.</summary>
|
||||
/// <param name="value">The raw value to parse.</param>
|
||||
/// <param name="expected">The secret name the parser is expected to return.</param>
|
||||
[Theory]
|
||||
[InlineData("secret:mxgw-key", "mxgw-key")]
|
||||
[InlineData("SECRET:mxgw-key", "mxgw-key")]
|
||||
@@ -60,6 +67,8 @@ public class SecretReferenceScannerTests
|
||||
Assert.Equal(expected, name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the parser rejects values that are not secret references.</summary>
|
||||
/// <param name="value">The value that is not a secret reference.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -74,6 +83,7 @@ public class SecretReferenceScannerTests
|
||||
Assert.Equal(string.Empty, name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the scanner and the parser agree on the name of a padded, differently-cased reference.</summary>
|
||||
[Fact]
|
||||
public void Scan_AndParser_AgreeOnThePaddedCaseVariantName()
|
||||
{
|
||||
|
||||
@@ -11,12 +11,13 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
/// </summary>
|
||||
public class SiteCallOperationalTests
|
||||
{
|
||||
/// <summary>Verifies that a site call operational record carries its source node.</summary>
|
||||
[Fact]
|
||||
public void SiteCallOperational_carries_SourceNode()
|
||||
{
|
||||
// SourceNode identifies the cluster node that emitted the cached call
|
||||
// (site node-a/node-b or central-a/central-b). Nullable — callsites
|
||||
// pass null until INodeIdentityProvider stamping arrives in Task 14.
|
||||
// pass null until INodeIdentityProvider stamping arrives.
|
||||
var trackedId = TrackedOperationId.New();
|
||||
var nowUtc = new DateTime(2026, 5, 23, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
public class SiteNotificationKpiSnapshotTests
|
||||
{
|
||||
/// <summary>Verifies that the constructor assigns all members.</summary>
|
||||
[Fact]
|
||||
public void Constructor_AssignsAllMembers()
|
||||
{
|
||||
@@ -23,6 +24,7 @@ public class SiteNotificationKpiSnapshotTests
|
||||
Assert.Equal(TimeSpan.FromMinutes(12), snapshot.OldestPendingAge);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the oldest pending age is nullable for sites with no backlog.</summary>
|
||||
[Fact]
|
||||
public void OldestPendingAge_IsNullableForSitesWithNoBacklog()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for Commons-001: the check-then-act race between the timer
|
||||
/// Regression tests for the check-then-act race between the timer
|
||||
/// callback (<c>OnTimerElapsed</c>) and <c>OnValueReceived</c> / <c>Stop</c> / <c>Start</c>.
|
||||
/// Placed in the same non-parallel collection as <see cref="StaleTagMonitorTests"/> to
|
||||
/// avoid CPU contention from other timer-based tests running concurrently.
|
||||
|
||||
@@ -29,18 +29,22 @@ public class StaleTagMonitorTests
|
||||
return condition(); // final check
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing the monitor with a zero time span throws.</summary>
|
||||
[Fact]
|
||||
public void Constructor_ZeroTimeSpan_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StaleTagMonitor(TimeSpan.Zero));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing the monitor with a negative time span throws.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NegativeTimeSpan_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StaleTagMonitor(TimeSpan.FromSeconds(-1)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the stale event fires after the maximum silence elapses.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Stale_FiresAfterMaxSilence()
|
||||
{
|
||||
@@ -54,6 +58,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the stale event fires only once during continued silence.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Stale_FiresOnlyOnce()
|
||||
{
|
||||
@@ -72,6 +78,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that receiving a value resets the silence timer.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnValueReceived_ResetsTimer()
|
||||
{
|
||||
@@ -93,6 +101,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the stale event can still fire after silence following a received value.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnValueReceived_AllowsStaleAfterSilence()
|
||||
{
|
||||
@@ -110,6 +120,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that receiving a value resets the stale flag so the event can fire a second time.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnValueReceived_ResetsStaleFlag_AllowsSecondFire()
|
||||
{
|
||||
@@ -130,6 +142,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(2, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stopping the monitor prevents the stale event from firing.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Stop_PreventsStale()
|
||||
{
|
||||
@@ -143,6 +157,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the monitor prevents the stale event from firing.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_PreventsStale()
|
||||
{
|
||||
@@ -156,6 +172,7 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the maximum silence property returns the configured value.</summary>
|
||||
[Fact]
|
||||
public void MaxSilence_ReturnsConfiguredValue()
|
||||
{
|
||||
|
||||
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3): tests for the strongly-typed cached-operation identifier
|
||||
/// Tests for the strongly-typed cached-operation identifier
|
||||
/// produced by <c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c> and
|
||||
/// surfaced to scripts via <c>Tracking.Status(id)</c>.
|
||||
/// </summary>
|
||||
public class TrackedOperationIdTests
|
||||
{
|
||||
/// <summary>Verifies that each new identifier is unique and non-empty.</summary>
|
||||
[Fact]
|
||||
public void New_ProducesUniqueIds()
|
||||
{
|
||||
@@ -20,6 +21,7 @@ public class TrackedOperationIdTests
|
||||
Assert.NotEqual(Guid.Empty, b.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing the string form of an identifier restores the original value.</summary>
|
||||
[Fact]
|
||||
public void Parse_RoundTrip_PreservesValue()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ public class TrackedOperationIdTests
|
||||
Assert.Equal(original.Value, parsed.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invalid, null and empty input fail to parse and yield a default identifier.</summary>
|
||||
[Fact]
|
||||
public void TryParse_InvalidInput_ReturnsFalse()
|
||||
{
|
||||
@@ -45,6 +48,7 @@ public class TrackedOperationIdTests
|
||||
Assert.Equal(default, emptyResult);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that valid input parses successfully and yields the original identifier.</summary>
|
||||
[Fact]
|
||||
public void TryParse_ValidInput_ReturnsTrueAndId()
|
||||
{
|
||||
@@ -55,6 +59,7 @@ public class TrackedOperationIdTests
|
||||
Assert.Equal(original, parsed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two identifiers wrapping the same value are equal and share a hash code.</summary>
|
||||
[Fact]
|
||||
public void Equality_BasedOnValue()
|
||||
{
|
||||
@@ -68,6 +73,7 @@ public class TrackedOperationIdTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the string form uses the standard hyphenated GUID format.</summary>
|
||||
[Fact]
|
||||
public void ToString_StandardGuidFormat()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// M8 A1: focused shape / value-equality / round-trip tests for the Transport (#24)
|
||||
/// Focused shape / value-equality / round-trip tests for the Transport
|
||||
/// name-mapping DTOs — <see cref="SiteMapping"/>, <see cref="ConnectionMapping"/>,
|
||||
/// and <see cref="BundleNameMap"/>. These records cross the Central UI ⇆ ManagementActor
|
||||
/// boundary (and may be persisted in an import session) via System.Text.Json, so a
|
||||
@@ -16,6 +16,7 @@ public sealed class BundleNameMapTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>Verifies that the empty map has no sites or connections.</summary>
|
||||
[Fact]
|
||||
public void Empty_HasNoSitesOrConnections()
|
||||
{
|
||||
@@ -25,6 +26,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Empty(map.Connections);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the empty map is a stable shared instance.</summary>
|
||||
[Fact]
|
||||
public void Empty_IsStableInstance()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Same(BundleNameMap.Empty, BundleNameMap.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that site mappings with identical fields are equal.</summary>
|
||||
[Fact]
|
||||
public void SiteMapping_ValueEquality_HoldsForIdenticalFields()
|
||||
{
|
||||
@@ -42,6 +45,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that site mappings with different actions are not equal.</summary>
|
||||
[Fact]
|
||||
public void SiteMapping_ValueEquality_DiffersWhenActionDiffers()
|
||||
{
|
||||
@@ -51,6 +55,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.NotEqual(mapTo, createNew);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connection mappings with identical fields are equal.</summary>
|
||||
[Fact]
|
||||
public void ConnectionMapping_ValueEquality_HoldsForIdenticalFields()
|
||||
{
|
||||
@@ -61,6 +66,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a map-to-existing mapping carries a non-null target.</summary>
|
||||
[Fact]
|
||||
public void MapToExisting_CarriesNonNullTarget()
|
||||
{
|
||||
@@ -76,6 +82,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Equal("OPC-Primary", conn.TargetConnectionName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a create-new mapping leaves the target null.</summary>
|
||||
[Fact]
|
||||
public void CreateNew_LeavesTargetNull()
|
||||
{
|
||||
@@ -86,6 +93,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Null(conn.TargetConnectionName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a JSON round trip preserves the sites and connections of a name map.</summary>
|
||||
[Fact]
|
||||
public void BundleNameMap_JsonRoundTrip_PreservesSitesAndConnections()
|
||||
{
|
||||
@@ -114,6 +122,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.True(map.Connections.SequenceEqual(rt.Connections));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a JSON round trip of an empty name map stays empty.</summary>
|
||||
[Fact]
|
||||
public void BundleNameMap_Empty_JsonRoundTrip_PreservesEmptiness()
|
||||
{
|
||||
|
||||
+13
-1
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Commons-015: <see cref="EncryptionMetadata"/> must reject malformed envelopes at
|
||||
/// <see cref="EncryptionMetadata"/> must reject malformed envelopes at
|
||||
/// the type boundary (unknown algorithm, unsupported KDF, sub-minimum or over-cap
|
||||
/// iteration counts, null salt/IV). Valid construction must round-trip the fields.
|
||||
/// </summary>
|
||||
public sealed class EncryptionMetadataTests
|
||||
{
|
||||
/// <summary>Verifies that the constructor accepts the documented values.</summary>
|
||||
[Fact]
|
||||
public void Constructor_WithDocumentedValues_Succeeds()
|
||||
{
|
||||
@@ -28,6 +29,8 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Equal("def", meta.IvB64);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown algorithm is rejected.</summary>
|
||||
/// <param name="algorithm">The unsupported algorithm name.</param>
|
||||
[Theory]
|
||||
[InlineData("AES-128-CBC")] // weaker algorithm
|
||||
[InlineData("AES-256-CBC")] // unauthenticated mode
|
||||
@@ -47,6 +50,8 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Contains("AES-256-GCM", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown key-derivation function is rejected.</summary>
|
||||
/// <param name="kdf">The unsupported key-derivation function name.</param>
|
||||
[Theory]
|
||||
[InlineData("PBKDF2-SHA1")] // weaker hash
|
||||
[InlineData("argon2id")] // unsupported KDF
|
||||
@@ -65,6 +70,8 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Contains("PBKDF2-SHA256", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an out-of-range iteration count is rejected.</summary>
|
||||
/// <param name="iterations">The out-of-range iteration count.</param>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
@@ -84,6 +91,8 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Equal("Iterations", ex.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an iteration count at the allowed boundary is accepted.</summary>
|
||||
/// <param name="iterations">The boundary iteration count.</param>
|
||||
[Theory]
|
||||
[InlineData(100_000)] // OWASP minimum (exact)
|
||||
[InlineData(600_000)] // design-doc production value
|
||||
@@ -101,6 +110,7 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Equal(iterations, meta.Iterations);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null salt is rejected.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullSalt_Throws()
|
||||
{
|
||||
@@ -113,6 +123,7 @@ public sealed class EncryptionMetadataTests
|
||||
IvB64: "def"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null initialization vector is rejected.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullIv_Throws()
|
||||
{
|
||||
@@ -124,6 +135,7 @@ public sealed class EncryptionMetadataTests
|
||||
IvB64: null!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty salt and initialization vector are accepted for the seed pattern.</summary>
|
||||
[Fact]
|
||||
public void Constructor_EmptySaltAndIv_Succeeds_ForSeedPattern()
|
||||
{
|
||||
|
||||
@@ -4,18 +4,16 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Commons-020: focused shape / round-trip tests for the Transport (#24) record DTOs
|
||||
/// Focused shape / round-trip tests for the Transport record DTOs
|
||||
/// — <see cref="BundleManifest"/>, <see cref="ExportSelection"/>,
|
||||
/// <see cref="ImportPreview"/>, <see cref="ImportResolution"/>, and
|
||||
/// <see cref="ImportResult"/>. These records cross the Central UI ⇆ bundle file boundary
|
||||
/// via System.Text.Json, so a positional/tuple slip would break bundles in the field.
|
||||
/// EncryptionMetadata has its own focused tests under EncryptionMetadataTests.cs
|
||||
/// (Commons-015) and is reused here only to populate manifest fixtures.
|
||||
/// and is reused here only to populate manifest fixtures.
|
||||
/// </summary>
|
||||
public sealed class TransportRecordsTests
|
||||
{
|
||||
// STM: TransportRecordsTests-Commons-020 marker — used by grep verification.
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
@@ -25,6 +23,7 @@ public sealed class TransportRecordsTests
|
||||
// BundleManifest
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the <c>BundleManifest</c> constructor preserves all fields.</summary>
|
||||
[Fact]
|
||||
public void BundleManifest_Constructor_RoundTripsAllFields()
|
||||
{
|
||||
@@ -62,6 +61,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.Equal("Pump", manifest.Contents[0].Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a <c>BundleManifest</c> survives a JSON round trip with all fields intact.</summary>
|
||||
[Fact]
|
||||
public void BundleManifest_JsonRoundTrip_PreservesAllFields()
|
||||
{
|
||||
@@ -106,6 +106,7 @@ public sealed class TransportRecordsTests
|
||||
// ExportSelection
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the <c>ExportSelection</c> constructor preserves every id list.</summary>
|
||||
[Fact]
|
||||
public void ExportSelection_Constructor_PreservesAllIdLists()
|
||||
{
|
||||
@@ -126,6 +127,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.True(sel.IncludeDependencies);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>ExportSelection</c> JSON round trip preserves the include-dependencies flag and ids.</summary>
|
||||
[Fact]
|
||||
public void ExportSelection_JsonRoundTrip_PreservesIncludeDependenciesAndIds()
|
||||
{
|
||||
@@ -152,6 +154,7 @@ public sealed class TransportRecordsTests
|
||||
// ImportPreview
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an <c>ImportPreview</c> can be constructed with every conflict kind.</summary>
|
||||
[Fact]
|
||||
public void ImportPreview_Constructor_AllowsAllConflictKinds()
|
||||
{
|
||||
@@ -175,6 +178,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.Equal("Parameters property mismatch", preview.Items[3].BlockerReason);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>ImportPreview</c> JSON round trip preserves the conflict kind and optional fields.</summary>
|
||||
[Fact]
|
||||
public void ImportPreview_JsonRoundTrip_PreservesConflictKindAndOptionalFields()
|
||||
{
|
||||
@@ -201,6 +205,9 @@ public sealed class TransportRecordsTests
|
||||
// ImportResolution
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the <c>ImportResolution</c> constructor preserves each resolution action and rename target.</summary>
|
||||
/// <param name="action">The resolution action under test.</param>
|
||||
/// <param name="renameTo">The rename target, or null when the action is not a rename.</param>
|
||||
[Theory]
|
||||
[InlineData(ResolutionAction.Add, null)]
|
||||
[InlineData(ResolutionAction.Overwrite, null)]
|
||||
@@ -215,6 +222,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.Equal(renameTo, res.RenameTo);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>ImportResolution</c> JSON round trip preserves the rename target.</summary>
|
||||
[Fact]
|
||||
public void ImportResolution_JsonRoundTrip_PreservesRenameTo()
|
||||
{
|
||||
@@ -232,6 +240,7 @@ public sealed class TransportRecordsTests
|
||||
// ImportResult
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the <c>ImportResult</c> constructor preserves all counters and stale instance ids.</summary>
|
||||
[Fact]
|
||||
public void ImportResult_Constructor_PreservesAllCountersAndStaleIds()
|
||||
{
|
||||
@@ -254,6 +263,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.Equal("audit-corr-001", result.AuditEventCorrelation);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>ImportResult</c> JSON round trip preserves the counts and correlation id.</summary>
|
||||
[Fact]
|
||||
public void ImportResult_JsonRoundTrip_PreservesCountsAndCorrelation()
|
||||
{
|
||||
@@ -280,6 +290,7 @@ public sealed class TransportRecordsTests
|
||||
// Record equality sanity (catches positional/tuple slip)
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that transport records compare equal by value.</summary>
|
||||
[Fact]
|
||||
public void TransportRecords_RecordValueEquality()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ValidationResult.SummarizeErrors"/> (followup #8) — the grouped,
|
||||
/// Tests for <see cref="ValidationResult.SummarizeErrors"/> — the grouped,
|
||||
/// capped summary that replaces the flat semicolon-joined deploy error dump.
|
||||
/// </summary>
|
||||
public class ValidationResultSummaryTests
|
||||
@@ -14,12 +14,14 @@ public class ValidationResultSummaryTests
|
||||
$"Attribute '{canonicalName}' has a data source reference but no connection binding.",
|
||||
canonicalName);
|
||||
|
||||
/// <summary>Verifies that a result with no errors summarizes to an empty string.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_NoErrors_ReturnsEmpty()
|
||||
{
|
||||
Assert.Equal(string.Empty, ValidationResult.Success().SummarizeErrors());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the summary leads with the total error count.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_LeadsWithTotalCount()
|
||||
{
|
||||
@@ -33,6 +35,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.StartsWith("3 errors:", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a single error uses the singular noun.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_SingleError_UsesSingularNoun()
|
||||
{
|
||||
@@ -40,6 +43,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.StartsWith("1 error:", result.SummarizeErrors());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unbound attributes are rolled up by module with counts.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_RollsUpUnboundAttributesByModuleWithCounts()
|
||||
{
|
||||
@@ -58,6 +62,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.DoesNotContain("Attr11' has a data source reference", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the number of listed modules is capped with an "and N more" suffix.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_CapsModuleBreadthWithAndNMore()
|
||||
{
|
||||
@@ -70,6 +75,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.Contains("Module0 (1)", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a top-level member is grouped under the root.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_TopLevelMember_GroupsUnderRoot()
|
||||
{
|
||||
@@ -77,6 +83,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.Contains("(root) (1)", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that entries without an entity name fall back to a capped message list.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_EntriesWithoutEntityName_FallBackToCappedMessageList()
|
||||
{
|
||||
@@ -91,6 +98,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.Contains("… and 3 more", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that mixed error categories are listed as separate category groups.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_MixedCategories_ListsEachCategoryGroup()
|
||||
{
|
||||
|
||||
@@ -4,32 +4,36 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ValueFormatter"/>. Includes the Commons-012 regression:
|
||||
/// Tests for <see cref="ValueFormatter"/>. Includes the culture-invariance regression:
|
||||
/// formatting must be culture-invariant because the formatter feeds non-UI contexts
|
||||
/// (gRPC stream events, logs) where locale-dependent output would be inconsistent.
|
||||
/// </summary>
|
||||
public class ValueFormatterTests
|
||||
{
|
||||
/// <summary>Verifies that formatting a null value returns an empty string.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_Null_ReturnsEmptyString()
|
||||
{
|
||||
Assert.Equal("", ValueFormatter.FormatDisplayValue(null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that formatting a string returns the value unchanged.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_String_ReturnsValueUnchanged()
|
||||
{
|
||||
Assert.Equal("hello", ValueFormatter.FormatDisplayValue("hello"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that formatting a collection joins the elements with a comma.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_Collection_JoinsWithComma()
|
||||
{
|
||||
Assert.Equal("1,2,3", ValueFormatter.FormatDisplayValue(new[] { 1, 2, 3 }));
|
||||
}
|
||||
|
||||
// ── Commons-012 regression: culture-invariant numeric/date formatting ──
|
||||
// ── Culture-invariance regression: numeric/date formatting ──
|
||||
|
||||
/// <summary>Verifies that formatting a double uses the invariant culture regardless of the thread culture.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_Double_UsesInvariantCulture_RegardlessOfThreadCulture()
|
||||
{
|
||||
@@ -46,6 +50,7 @@ public class ValueFormatterTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that formatting a DateTime uses the invariant culture regardless of the thread culture.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_DateTime_UsesInvariantCulture_RegardlessOfThreadCulture()
|
||||
{
|
||||
@@ -63,6 +68,7 @@ public class ValueFormatterTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that formatting a collection of doubles uses the invariant culture.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_CollectionOfDoubles_UsesInvariantCulture()
|
||||
{
|
||||
|
||||
+7
@@ -12,6 +12,7 @@ public class MxGatewayEndpointConfigValidatorTests
|
||||
ApiKey = "key",
|
||||
};
|
||||
|
||||
/// <summary>Verifies that a valid config passes validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_ValidConfig_IsValid()
|
||||
{
|
||||
@@ -20,6 +21,7 @@ public class MxGatewayEndpointConfigValidatorTests
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing endpoint fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_MissingEndpoint_Fails()
|
||||
{
|
||||
@@ -33,6 +35,8 @@ public class MxGatewayEndpointConfigValidatorTests
|
||||
&& e.Message.Contains("required", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an endpoint with an unsupported scheme fails validation.</summary>
|
||||
/// <param name="url">The endpoint URL with the bad scheme.</param>
|
||||
[Theory]
|
||||
[InlineData("opc.tcp://x:4840")]
|
||||
[InlineData("ftp://x")]
|
||||
@@ -46,6 +50,7 @@ public class MxGatewayEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "Endpoint");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing API key fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_MissingApiKey_Fails()
|
||||
{
|
||||
@@ -56,6 +61,7 @@ public class MxGatewayEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "ApiKey");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a non-positive read timeout fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_NonPositiveReadTimeout_Fails()
|
||||
{
|
||||
@@ -66,6 +72,7 @@ public class MxGatewayEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "ReadTimeoutMs");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that prefixed field names appear in the validation errors.</summary>
|
||||
[Fact]
|
||||
public void Validate_PrefixedFieldNames_AppearInErrors()
|
||||
{
|
||||
|
||||
+19
@@ -12,6 +12,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
// Defaults satisfy the spec: Lifetime(30) >= 3 * KeepAlive(10).
|
||||
};
|
||||
|
||||
/// <summary>Verifies that the default settings with a valid endpoint URL validate successfully.</summary>
|
||||
[Fact]
|
||||
public void Validate_DefaultsWithValidUrl_IsValid()
|
||||
{
|
||||
@@ -20,6 +21,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing endpoint URL fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_MissingEndpointUrl_Fails()
|
||||
{
|
||||
@@ -33,6 +35,8 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
&& e.Message.Contains("required", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a malformed or non-OPC UA endpoint URL fails validation.</summary>
|
||||
/// <param name="url">The invalid endpoint URL to validate.</param>
|
||||
[Theory]
|
||||
[InlineData("http://x")]
|
||||
[InlineData("opc.tcp://")]
|
||||
@@ -46,6 +50,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "EndpointUrl");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a lifetime count below three times the keep-alive count fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_LifetimeLessThanThreeTimesKeepAlive_Fails()
|
||||
{
|
||||
@@ -57,6 +62,8 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "LifetimeCount");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a non-positive timing value fails validation.</summary>
|
||||
/// <param name="field">The name of the timing field set to a non-positive value.</param>
|
||||
[Theory]
|
||||
[InlineData(nameof(OpcUaEndpointConfig.SessionTimeoutMs))]
|
||||
[InlineData(nameof(OpcUaEndpointConfig.OperationTimeoutMs))]
|
||||
@@ -71,6 +78,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == field);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a queue size of zero fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_QueueSizeZero_Fails()
|
||||
{
|
||||
@@ -80,6 +88,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "QueueSize");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that enabling the heartbeat without a tag path fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_HeartbeatEnabledNoTagPath_Fails()
|
||||
{
|
||||
@@ -89,6 +98,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "Heartbeat.TagPath");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a non-positive heartbeat silence threshold fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_HeartbeatNonPositiveSilence_Fails()
|
||||
{
|
||||
@@ -98,6 +108,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "Heartbeat.MaxSilenceSeconds");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the field prefix is applied to every validation error.</summary>
|
||||
[Fact]
|
||||
public void Validate_FieldPrefix_AppliedToEveryError()
|
||||
{
|
||||
@@ -112,6 +123,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
|
||||
// ── Layer B extensions: auth, deadband, subscription display name ──
|
||||
|
||||
/// <summary>Verifies that an empty subscription display name fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_EmptySubscriptionDisplayName_Fails()
|
||||
{
|
||||
@@ -121,6 +133,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "SubscriptionDisplayName");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an anonymous user identity requires no additional fields.</summary>
|
||||
[Fact]
|
||||
public void Validate_UserIdentityAnonymous_NoExtraFieldsRequired()
|
||||
{
|
||||
@@ -130,6 +143,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.True(r.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a username/password identity without a username fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_UsernamePasswordWithoutUsername_Fails()
|
||||
{
|
||||
@@ -144,6 +158,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "UserIdentity.Username");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an X.509 identity without a certificate path fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_X509WithoutCertificatePath_Fails()
|
||||
{
|
||||
@@ -157,6 +172,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "UserIdentity.CertificatePath");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a username/password identity with a username passes validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_UsernamePasswordWithUsername_Passes()
|
||||
{
|
||||
@@ -171,6 +187,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.True(r.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deadband with a non-positive value fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_DeadbandWithNonPositiveValue_Fails()
|
||||
{
|
||||
@@ -180,6 +197,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.Contains(r.Errors, e => e.EntityName == "Deadband.Value");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deadband with a positive value passes validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_DeadbandWithPositiveValue_Passes()
|
||||
{
|
||||
@@ -189,6 +207,7 @@ public class OpcUaEndpointConfigValidatorTests
|
||||
Assert.True(r.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that user identity errors carry the field prefix.</summary>
|
||||
[Fact]
|
||||
public void Validate_UserIdentityErrorsPrefixed_WithFieldPrefix()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user