docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)

Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -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)]
@@ -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,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()
{
@@ -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()
{
@@ -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()
{
@@ -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()
{
@@ -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()
{
@@ -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()
{