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:
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Alarms;
|
||||
|
||||
public class AlarmConditionStateTests
|
||||
{
|
||||
/// <summary>Verifies that a default alarm condition state is normal and unshelved.</summary>
|
||||
[Fact]
|
||||
public void AlarmConditionState_DefaultsAreNormalUnshelved()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public class AlarmConditionStateTests
|
||||
Assert.Equal(0, s.Severity);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native alarm transition carries its source and condition.</summary>
|
||||
[Fact]
|
||||
public void NativeAlarmTransition_CarriesSourceAndCondition()
|
||||
{
|
||||
|
||||
@@ -12,15 +12,18 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
/// </summary>
|
||||
public class AttributeValueCodecTests
|
||||
{
|
||||
/// <summary>Verifies that a string list encodes as a JSON array.</summary>
|
||||
[Fact]
|
||||
public void Encode_StringList_ProducesJsonArray() =>
|
||||
Assert.Equal("[\"WO-1\",\"WO-2\"]",
|
||||
AttributeValueCodec.Encode(new List<string> { "WO-1", "WO-2" }));
|
||||
|
||||
/// <summary>Verifies that a scalar string is encoded unchanged.</summary>
|
||||
[Fact]
|
||||
public void Encode_Scalar_String_ReturnedAsIs() =>
|
||||
Assert.Equal("hello", AttributeValueCodec.Encode("hello"));
|
||||
|
||||
/// <summary>Verifies that a scalar double is encoded using the invariant culture.</summary>
|
||||
[Fact]
|
||||
public void Encode_Scalar_Double_IsInvariant()
|
||||
{
|
||||
@@ -36,19 +39,23 @@ public class AttributeValueCodecTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that encoding null returns null.</summary>
|
||||
[Fact]
|
||||
public void Encode_Null_ReturnsNull() =>
|
||||
Assert.Null(AttributeValueCodec.Encode(null));
|
||||
|
||||
/// <summary>Verifies that an empty list encodes as an empty bracket pair.</summary>
|
||||
[Fact]
|
||||
public void Encode_EmptyList_IsBracketPair() =>
|
||||
Assert.Equal("[]", AttributeValueCodec.Encode(new List<string>()));
|
||||
|
||||
/// <summary>Verifies that a string containing a comma is escaped inside the encoded array.</summary>
|
||||
[Fact]
|
||||
public void Encode_StringWithComma_IsEscaped() =>
|
||||
Assert.Equal("[\"ACME, Inc.\"]",
|
||||
AttributeValueCodec.Encode(new List<string> { "ACME, Inc." }));
|
||||
|
||||
/// <summary>Verifies that a float list survives an encode and decode round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_FloatList()
|
||||
{
|
||||
@@ -57,6 +64,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 1.5f, 2.25f, -3.75f }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a double list is encoded using the invariant culture.</summary>
|
||||
[Fact]
|
||||
public void Encode_DoubleList_IsInvariant()
|
||||
{
|
||||
@@ -73,21 +81,25 @@ public class AttributeValueCodecTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an integer list encodes as native JSON numbers.</summary>
|
||||
[Fact]
|
||||
public void Encode_Int32List_ProducesNativeNumbers() =>
|
||||
Assert.Equal("[10,20,30]",
|
||||
AttributeValueCodec.Encode(new List<int> { 10, 20, 30 }));
|
||||
|
||||
/// <summary>Verifies that a boolean list encodes as native JSON booleans.</summary>
|
||||
[Fact]
|
||||
public void Encode_BoolList_ProducesNativeBooleans() =>
|
||||
Assert.Equal("[true,false]",
|
||||
AttributeValueCodec.Encode(new List<bool> { true, false }));
|
||||
|
||||
/// <summary>Verifies that string elements remain quoted in the encoded array.</summary>
|
||||
[Fact]
|
||||
public void Encode_StringList_StaysQuoted() =>
|
||||
Assert.Equal("[\"a\",\"b\"]",
|
||||
AttributeValueCodec.Encode(new List<string> { "a", "b" }));
|
||||
|
||||
/// <summary>Verifies that a date-time list encodes its elements in ISO 8601 form.</summary>
|
||||
[Fact]
|
||||
public void Encode_DateTimeList_IsIso8601()
|
||||
{
|
||||
@@ -98,6 +110,7 @@ public class AttributeValueCodecTests
|
||||
Assert.DoesNotContain("06/16/2026", json);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the old string form of a float list still decodes.</summary>
|
||||
[Fact]
|
||||
public void Decode_OldStringFloatForm_BackwardCompatible()
|
||||
{
|
||||
@@ -105,6 +118,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 1.5f, 2.25f }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the old string form of a date-time list still decodes.</summary>
|
||||
[Fact]
|
||||
public void Decode_OldStringDateTimeForm_BackwardCompatible()
|
||||
{
|
||||
@@ -115,6 +129,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new DateTime(2026, 6, 16, 12, 30, 45, DateTimeKind.Unspecified), back[0]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the native integer array form decodes correctly.</summary>
|
||||
[Fact]
|
||||
public void Decode_NewNativeIntForm_Parses()
|
||||
{
|
||||
@@ -122,6 +137,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 10, 20 }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the old string form of an integer list still decodes.</summary>
|
||||
[Fact]
|
||||
public void Decode_OldStringIntForm_BackwardCompatible()
|
||||
{
|
||||
@@ -129,6 +145,8 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 10, 20 }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that both the native and the string boolean array forms decode.</summary>
|
||||
/// <param name="json">The encoded boolean array to decode.</param>
|
||||
[Theory]
|
||||
[InlineData("[true,false]")]
|
||||
[InlineData("[\"True\",\"False\"]")]
|
||||
@@ -138,6 +156,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { true, false }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an integer list survives an encode and decode round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_Int32List()
|
||||
{
|
||||
@@ -146,6 +165,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 1, 2, 3 }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a double list round-trips regardless of the current culture.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DoubleList_IsCultureInvariant()
|
||||
{
|
||||
@@ -163,6 +183,7 @@ public class AttributeValueCodecTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a boolean list survives an encode and decode round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_BoolList()
|
||||
{
|
||||
@@ -171,6 +192,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { true, false, true }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a date-time list round-trips through its ISO 8601 form.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_DateTimeList_Iso8601()
|
||||
{
|
||||
@@ -181,6 +203,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { a, b }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a JSON string array decodes to a list of strings.</summary>
|
||||
[Fact]
|
||||
public void Decode_StringArray_ProducesListOfString()
|
||||
{
|
||||
@@ -188,38 +211,48 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { "a", "b" }, back);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that decoding a scalar value returns it as a string.</summary>
|
||||
[Fact]
|
||||
public void Decode_Scalar_ReturnsString() =>
|
||||
Assert.Equal("42", AttributeValueCodec.Decode("42", DataType.Int32, null));
|
||||
|
||||
/// <summary>Verifies that decoding a null list value returns null.</summary>
|
||||
[Fact]
|
||||
public void Decode_List_NullValue_ReturnsNull() =>
|
||||
Assert.Null(AttributeValueCodec.Decode(null, DataType.List, DataType.String));
|
||||
|
||||
/// <summary>Verifies that decoding an empty list value returns null.</summary>
|
||||
[Fact]
|
||||
public void Decode_List_EmptyValue_ReturnsNull() =>
|
||||
Assert.Null(AttributeValueCodec.Decode("", DataType.List, DataType.String));
|
||||
|
||||
/// <summary>Verifies that malformed JSON throws when decoding a list.</summary>
|
||||
[Fact]
|
||||
public void Decode_MalformedJson_Throws() =>
|
||||
Assert.Throws<FormatException>(() =>
|
||||
AttributeValueCodec.Decode("not json", DataType.List, DataType.String));
|
||||
|
||||
/// <summary>Verifies that an element that cannot be parsed throws when decoding a list.</summary>
|
||||
[Fact]
|
||||
public void Decode_UnparseableElement_Throws() =>
|
||||
Assert.Throws<FormatException>(() =>
|
||||
AttributeValueCodec.Decode("[\"abc\"]", DataType.List, DataType.Int32));
|
||||
|
||||
/// <summary>Verifies that a null element throws when decoding a list.</summary>
|
||||
[Fact]
|
||||
public void Decode_NullElement_Throws() =>
|
||||
Assert.Throws<FormatException>(() =>
|
||||
AttributeValueCodec.Decode("[null]", DataType.List, DataType.String));
|
||||
|
||||
/// <summary>Verifies that decoding a list without an element type throws.</summary>
|
||||
[Fact]
|
||||
public void Decode_List_WithoutElementType_Throws() =>
|
||||
Assert.Throws<FormatException>(() =>
|
||||
AttributeValueCodec.Decode("[\"a\"]", DataType.List, null));
|
||||
|
||||
/// <summary>Verifies that only scalar data types are valid list element types.</summary>
|
||||
/// <param name="t">The data type to check.</param>
|
||||
/// <param name="expected">Whether the type is expected to be a valid element type.</param>
|
||||
[Theory]
|
||||
[InlineData(DataType.String, true)]
|
||||
[InlineData(DataType.Int32, true)]
|
||||
@@ -237,6 +270,9 @@ public class AttributeValueCodecTests
|
||||
// string-input path in Decode (JSON elements): here elements arrive as boxed
|
||||
// CLR values and are converted via Convert.ToXxx (or parsed when string).
|
||||
|
||||
/// <summary>Verifies that each scalar data type maps to its expected CLR type.</summary>
|
||||
/// <param name="t">The scalar data type to map.</param>
|
||||
/// <param name="expected">The expected CLR type.</param>
|
||||
[Theory]
|
||||
[InlineData(DataType.String, typeof(string))]
|
||||
[InlineData(DataType.Int32, typeof(int))]
|
||||
@@ -247,10 +283,12 @@ public class AttributeValueCodecTests
|
||||
public void ElementClrType_MapsEachScalar(DataType t, Type expected) =>
|
||||
Assert.Equal(expected, AttributeValueCodec.ElementClrType(t));
|
||||
|
||||
/// <summary>Verifies that a data type that is not a valid element type is rejected.</summary>
|
||||
[Fact]
|
||||
public void ElementClrType_RejectsNonElementType() =>
|
||||
Assert.Throws<FormatException>(() => AttributeValueCodec.ElementClrType(DataType.List));
|
||||
|
||||
/// <summary>Verifies that coercing an enumerable builds a list of the requested element type.</summary>
|
||||
[Fact]
|
||||
public void CoerceEnumerable_BuildsTypedList()
|
||||
{
|
||||
@@ -259,16 +297,18 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(new[] { 10, 20, 30 }, typed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that elements of mixed CLR types are converted to the requested element type.</summary>
|
||||
[Fact]
|
||||
public void CoerceEnumerable_ConvertsMixedClrElementTypes()
|
||||
{
|
||||
// Ints and numeric strings both coerce to List<double> via Convert/parse —
|
||||
// the object-input behavior InstanceActor's MV-8 coercion depends on.
|
||||
// the object-input behavior the Instance Actor's coercion depends on.
|
||||
var list = AttributeValueCodec.CoerceEnumerable(new object[] { 1, "2.5", 3.75 }, DataType.Double);
|
||||
var typed = Assert.IsType<List<double>>(list);
|
||||
Assert.Equal(new[] { 1.0, 2.5, 3.75 }, typed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that date-time elements round-trip through the invariant culture during coercion.</summary>
|
||||
[Fact]
|
||||
public void CoerceEnumerable_DateTimeRoundtripsViaInvariantCulture()
|
||||
{
|
||||
@@ -279,6 +319,7 @@ public class AttributeValueCodecTests
|
||||
Assert.Equal(when, typed[0]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that coercion throws for an unsupported element type.</summary>
|
||||
[Fact]
|
||||
public void CoerceEnumerable_ThrowsOnUnsupportedElementType() =>
|
||||
Assert.Throws<FormatException>(
|
||||
|
||||
@@ -40,6 +40,7 @@ public class AuditDetailsCodecTests
|
||||
// Round-trip: all non-null fields survive
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that details with every field populated survive a serialize and deserialize round trip.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_AllFields_Populated()
|
||||
{
|
||||
@@ -67,6 +68,7 @@ public class AuditDetailsCodecTests
|
||||
// Round-trip: all-null record
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that details whose optional fields are all null round trip to empty details.</summary>
|
||||
[Fact]
|
||||
public void RoundTrip_AllNullOptionals_ReturnsEmptyDetails()
|
||||
{
|
||||
@@ -97,6 +99,7 @@ public class AuditDetailsCodecTests
|
||||
// Null / empty JSON deserialize → empty AuditDetails (no throw)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that deserializing null returns empty details.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_Null_ReturnsEmpty()
|
||||
{
|
||||
@@ -105,6 +108,7 @@ public class AuditDetailsCodecTests
|
||||
Assert.Null(d.Channel);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deserializing an empty string returns empty details.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_EmptyString_ReturnsEmpty()
|
||||
{
|
||||
@@ -117,6 +121,7 @@ public class AuditDetailsCodecTests
|
||||
// Determinism: same input → byte-identical JSON across two calls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that serializing the same input repeatedly produces byte-identical output.</summary>
|
||||
[Fact]
|
||||
public void Serialize_SameInput_ProducesByteIdenticalOutput_AcrossCalls()
|
||||
{
|
||||
@@ -135,6 +140,7 @@ public class AuditDetailsCodecTests
|
||||
// null fields omitted, PayloadTruncated=false present because it is non-nullable bool)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that serializing a known input produces the expected JSON.</summary>
|
||||
[Fact]
|
||||
public void Serialize_KnownInput_MatchesExpectedJson()
|
||||
{
|
||||
@@ -178,6 +184,7 @@ public class AuditDetailsCodecTests
|
||||
// Redaction marker preserved verbatim (UnsafeRelaxedJsonEscaping must not mangle <>)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a redaction marker is preserved verbatim through serialization.</summary>
|
||||
[Fact]
|
||||
public void Serialize_RedactionMarker_IsPreservedVerbatim()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,10 @@ public class AuditFieldBuildersTests
|
||||
// BuildAction: "{channel}.{kind}"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that building an action yields the channel and kind joined by a dot.</summary>
|
||||
/// <param name="channel">The audit channel.</param>
|
||||
/// <param name="kind">The audit kind.</param>
|
||||
/// <param name="expected">The expected action string.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditChannel.ApiOutbound, AuditKind.ApiCall, "ApiOutbound.ApiCall")]
|
||||
[InlineData(AuditChannel.ApiOutbound, AuditKind.ApiCallCached, "ApiOutbound.ApiCallCached")]
|
||||
@@ -32,6 +36,9 @@ public class AuditFieldBuildersTests
|
||||
// BuildCategory: channel.ToString()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that building a category yields the channel name.</summary>
|
||||
/// <param name="channel">The audit channel.</param>
|
||||
/// <param name="expected">The expected category string.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditChannel.ApiOutbound, "ApiOutbound")]
|
||||
[InlineData(AuditChannel.DbOutbound, "DbOutbound")]
|
||||
|
||||
@@ -14,6 +14,8 @@ public class AuditOutcomeProjectorTests
|
||||
// InboundAuthFailure → Denied, regardless of status (checked FIRST)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an inbound auth failure always projects to a denied outcome, whatever the status.</summary>
|
||||
/// <param name="status">The audit status under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Submitted)]
|
||||
[InlineData(AuditStatus.Forwarded)]
|
||||
@@ -33,6 +35,8 @@ public class AuditOutcomeProjectorTests
|
||||
// Delivered → Success (for all non-auth-failure kinds)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a delivered status projects to success for every kind other than an auth failure.</summary>
|
||||
/// <param name="kind">The audit kind under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditKind.ApiCall)]
|
||||
[InlineData(AuditKind.ApiCallCached)]
|
||||
@@ -53,6 +57,8 @@ public class AuditOutcomeProjectorTests
|
||||
// Failure statuses → Failure outcome
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that failed, parked and discarded statuses project to a failure outcome.</summary>
|
||||
/// <param name="status">The failure status under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Failed)]
|
||||
[InlineData(AuditStatus.Parked)]
|
||||
@@ -65,6 +71,8 @@ public class AuditOutcomeProjectorTests
|
||||
}
|
||||
|
||||
// InboundAuthFailure still wins over failure statuses
|
||||
/// <summary>Verifies that an inbound auth failure projects to denied even when the status is a failure status.</summary>
|
||||
/// <param name="status">The failure status under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Failed)]
|
||||
[InlineData(AuditStatus.Parked)]
|
||||
@@ -79,6 +87,8 @@ public class AuditOutcomeProjectorTests
|
||||
// In-progress / short-circuit statuses → Success
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that in-progress and skipped statuses project to a success outcome.</summary>
|
||||
/// <param name="status">The in-progress status under test.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Submitted)]
|
||||
[InlineData(AuditStatus.Forwarded)]
|
||||
@@ -94,6 +104,10 @@ public class AuditOutcomeProjectorTests
|
||||
// Full status × non-auth-failure kind cross-product (spot-check each status once more)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that each status and kind pair projects to the outcome in the specification table.</summary>
|
||||
/// <param name="status">The audit status under test.</param>
|
||||
/// <param name="kind">The audit kind under test.</param>
|
||||
/// <param name="expected">The expected projected outcome.</param>
|
||||
[Theory]
|
||||
[InlineData(AuditStatus.Submitted, AuditKind.DbWrite, AuditOutcome.Success)]
|
||||
[InlineData(AuditStatus.Forwarded, AuditKind.CachedSubmit, AuditOutcome.Success)]
|
||||
|
||||
+4
@@ -15,6 +15,8 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Audit;
|
||||
/// </summary>
|
||||
public class ScadaBridgeAuditEventFactoryActorTests
|
||||
{
|
||||
/// <summary>Verifies that system originated emit, preserves caller actor, verbatim.</summary>
|
||||
/// <param name="systemActor">Test case input.</param>
|
||||
[Theory]
|
||||
// Mirrors the literal system actors the outbound emitters pass:
|
||||
// NotificationOutboxActor → "system"; AuditingDbCommand → the source script.
|
||||
@@ -33,6 +35,7 @@ public class ScadaBridgeAuditEventFactoryActorTests
|
||||
Assert.Equal(systemActor, evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that authenticated emit, preserves caller actor, verbatim.</summary>
|
||||
[Fact]
|
||||
public void AuthenticatedEmit_PreservesCallerActor_Verbatim()
|
||||
{
|
||||
@@ -47,6 +50,7 @@ public class ScadaBridgeAuditEventFactoryActorTests
|
||||
Assert.Equal("alice", evt.Actor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that null actor, maps to empty string, on canonical record.</summary>
|
||||
[Fact]
|
||||
public void NullActor_MapsToEmptyString_OnCanonicalRecord()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M8): tests for the shared multi-value query-param parsers used by
|
||||
/// Tests for the shared multi-value query-param parsers used by
|
||||
/// the ManagementService + CentralUI audit endpoints and the <c>AuditLogPage</c>
|
||||
/// drill-in parser. Two contracts are under test:
|
||||
/// <list type="bullet">
|
||||
@@ -18,18 +18,21 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
/// </summary>
|
||||
public class AuditQueryParamParsersTests
|
||||
{
|
||||
/// <summary>Verifies that parse enum list null input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_NullInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumList<AuditChannel>(null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list empty input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_EmptyInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumList<AuditChannel>(Array.Empty<string?>()));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list all values valid parses everything.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_AllValuesValid_ParsesEverything()
|
||||
{
|
||||
@@ -38,6 +41,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.DbOutbound }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list is case insensitive.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_IsCaseInsensitive()
|
||||
{
|
||||
@@ -45,6 +49,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list drops unparseable element keeps the rest.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_DropsUnparseableElement_KeepsTheRest()
|
||||
{
|
||||
@@ -53,6 +58,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.Notification }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list all values unparseable returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumList_AllValuesUnparseable_ReturnsNull()
|
||||
{
|
||||
@@ -65,18 +71,22 @@ public class AuditQueryParamParsersTests
|
||||
// case-SENSITIVE AcceptOnlyFromAmong guard and still fail loudly on garbage.
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that parse enum list strict null input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_NullInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumListStrict<AuditStatus>(null, "status"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict empty input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_EmptyInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumListStrict<AuditStatus>(Array.Empty<string?>(), "status"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict is case insensitive and trims.</summary>
|
||||
/// <param name="raw">The raw value.</param>
|
||||
[Theory]
|
||||
[InlineData("delivered")]
|
||||
[InlineData("DELIVERED")]
|
||||
@@ -88,6 +98,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditStatus.Delivered }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict all values valid parses everything.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_AllValuesValid_ParsesEverything()
|
||||
{
|
||||
@@ -96,6 +107,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.DbOutbound }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict unrecognised value throws naming the valid set.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_UnrecognisedValue_ThrowsNamingTheValidSet()
|
||||
{
|
||||
@@ -113,6 +125,8 @@ public class AuditQueryParamParsersTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict numeric string is rejected.</summary>
|
||||
/// <param name="raw">The raw value.</param>
|
||||
[Theory]
|
||||
// "7" is out of range; "3" lands on a REAL member. Enum.TryParse accepts BOTH —
|
||||
// a filter is a name-based contract, never an ordinal one, so both are rejected.
|
||||
@@ -128,6 +142,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Contains(raw, ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict blank elements are still dropped silently.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_BlankElements_AreStillDroppedSilently()
|
||||
{
|
||||
@@ -138,6 +153,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { AuditStatus.Delivered }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict all blank returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_AllBlank_ReturnsNull()
|
||||
{
|
||||
@@ -146,8 +162,7 @@ public class AuditQueryParamParsersTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Comma-joined single token (arch-review 2026-08-19 P6-3, register row 99
|
||||
// closure defect): a bare Enum.TryParse splits and bitwise-ORs a comma list
|
||||
// Comma-joined single token (arch-review 2026-08-19 closure defect): a bare Enum.TryParse splits and bitwise-ORs a comma list
|
||||
// inside ONE token for ANY enum, not just [Flags] ones. AuditStatus is dense
|
||||
// and 0-based, so the OR'd composite almost always collides with a REAL, but
|
||||
// WRONG, member. Values arriving as separate tokens (repeated query param /
|
||||
@@ -157,6 +172,7 @@ public class AuditQueryParamParsersTests
|
||||
// instead RETURNS the wrong composite value below.
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that parse enum list strict comma joined two names is rejected not silently orded into wrong member.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_CommaJoinedTwoNames_IsRejected_NotSilentlyOrdedIntoWrongMember()
|
||||
{
|
||||
@@ -169,6 +185,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Contains("status", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict comma joined name and ordinal is rejected not smuggled past numeric guard.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_CommaJoinedNameAndOrdinal_IsRejected_NotSmuggledPastNumericGuard()
|
||||
{
|
||||
@@ -181,6 +198,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Contains("Failed,7", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict comma joined names drop a member is rejected.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_CommaJoinedNamesDropAMember_IsRejected()
|
||||
{
|
||||
@@ -192,6 +210,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Contains("Submitted,Parked", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse enum list strict repeated param tokens of valid names still parse independently.</summary>
|
||||
[Fact]
|
||||
public void ParseEnumListStrict_RepeatedParamTokensOfValidNames_StillParseIndependently()
|
||||
{
|
||||
@@ -205,12 +224,14 @@ public class AuditQueryParamParsersTests
|
||||
result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse string list null input returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseStringList_NullInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseStringList(null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse string list trims values and drops blanks.</summary>
|
||||
[Fact]
|
||||
public void ParseStringList_TrimsValuesAndDropsBlanks()
|
||||
{
|
||||
@@ -219,6 +240,7 @@ public class AuditQueryParamParsersTests
|
||||
Assert.Equal(new[] { "site-1", "site-2" }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parse string list all blank returns null.</summary>
|
||||
[Fact]
|
||||
public void ParseStringList_AllBlank_ReturnsNull()
|
||||
{
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
|
||||
|
||||
public class MxGatewayEndpointConfigSerializerTests
|
||||
{
|
||||
/// <summary>Verifies that serializing and then deserializing round-trips every field.</summary>
|
||||
[Fact]
|
||||
public void Serialize_then_Deserialize_round_trips_all_fields()
|
||||
{
|
||||
@@ -33,6 +34,8 @@ public class MxGatewayEndpointConfigSerializerTests
|
||||
Assert.Equal(original.ReadTimeoutMs, round.ReadTimeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null, blank or malformed JSON document deserializes to the default configuration.</summary>
|
||||
/// <param name="json">The JSON document to deserialize.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -46,6 +49,7 @@ public class MxGatewayEndpointConfigSerializerTests
|
||||
Assert.Equal(def.ReadTimeoutMs, result.ReadTimeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that converting to a flat dictionary and back round-trips the configuration.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_FromFlatDict_round_trips()
|
||||
{
|
||||
@@ -74,6 +78,7 @@ public class MxGatewayEndpointConfigSerializerTests
|
||||
Assert.Equal(original.ReadTimeoutMs, round.ReadTimeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an invalid numeric value in a flat dictionary falls back to the default.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_invalid_numeric_falls_back_to_default()
|
||||
{
|
||||
|
||||
+37
-2
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
|
||||
|
||||
public class OpcUaEndpointConfigSerializerTests
|
||||
{
|
||||
/// <summary>Verifies that a typed config round-trips through serialization with all fields preserved.</summary>
|
||||
[Fact]
|
||||
public void Serialize_TypedRoundtrip_PreservesAllFields()
|
||||
{
|
||||
@@ -44,6 +45,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(60, round.Heartbeat.MaxSilenceSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that serialization uses camelCase property names.</summary>
|
||||
[Fact]
|
||||
public void Serialize_UsesCamelCase()
|
||||
{
|
||||
@@ -53,6 +55,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.DoesNotContain("\"EndpointUrl\"", json);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the security mode is serialized as a camelCase string.</summary>
|
||||
[Fact]
|
||||
public void Serialize_SecurityModeAsCamelCaseString()
|
||||
{
|
||||
@@ -63,6 +66,8 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("signAndEncrypt", doc.RootElement.GetProperty("securityMode").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null or empty input deserializes to the default config.</summary>
|
||||
/// <param name="input">The null, empty or whitespace JSON input.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -77,6 +82,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Null(config.Heartbeat);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a legacy flat dictionary is flagged as legacy and its fields are populated.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_LegacyFlatDict_IsLegacyTrue_PopulatesFields()
|
||||
{
|
||||
@@ -105,6 +111,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(20, config.Heartbeat.MaxSilenceSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a legacy dictionary with a PascalCase <c>EndpointUrl</c> key is deserialized correctly.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_LegacyWithEndpointUrlPascalKey_Works()
|
||||
{
|
||||
@@ -114,6 +121,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("opc.tcp://x:4840", config.EndpointUrl);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a legacy dictionary with mixed value types preserves all fields.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_LegacyWithMixedTypeValues_PreservesAllFields()
|
||||
{
|
||||
@@ -135,6 +143,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(1000, config.PublishingIntervalMs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a parsed legacy config reports a legacy status.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_LegacyParsed_StatusIsLegacy()
|
||||
{
|
||||
@@ -145,6 +154,8 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(OpcUaConfigParseStatus.Legacy, result.Status);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that malformed input is reported as malformed rather than legacy.</summary>
|
||||
/// <param name="input">The malformed JSON input.</param>
|
||||
[Theory]
|
||||
[InlineData("not json at all")]
|
||||
[InlineData("[1,2,3]")]
|
||||
@@ -159,6 +170,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("", result.Config.EndpointUrl);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an object without an endpoint URL is parsed as legacy.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_ObjectWithoutEndpointUrl_ParsesAsLegacy()
|
||||
{
|
||||
@@ -168,8 +180,9 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.True(result.IsLegacy);
|
||||
}
|
||||
|
||||
// ── Commons-014 regression: a corrupt typed row must not be mislabelled Legacy ──
|
||||
// ── Regression: a corrupt typed row must not be mislabelled Legacy ──
|
||||
|
||||
/// <summary>Verifies that a typed-shaped row with an invalid enum value is reported as malformed rather than legacy.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_TypedShapeWithInvalidEnum_ReportsMalformedNotLegacy()
|
||||
{
|
||||
@@ -184,6 +197,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.False(result.IsLegacy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a typed-shaped row with a wrongly typed field is reported as malformed rather than legacy.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_TypedShapeWithWrongTypeField_ReportsMalformedNotLegacy()
|
||||
{
|
||||
@@ -196,10 +210,11 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.False(result.IsLegacy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a valid typed row is still reported as typed.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_ValidTypedRow_StillReportsTyped()
|
||||
{
|
||||
// Guard: a clean typed row is still classified Typed after the Commons-014 fix.
|
||||
// Guard: a clean typed row is still classified Typed after the fix.
|
||||
var json = """{"endpointUrl":"opc.tcp://x:4840","securityMode":"sign"}""";
|
||||
|
||||
var result = OpcUaEndpointConfigSerializer.Deserialize(json);
|
||||
@@ -209,6 +224,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(OpcUaSecurityMode.Sign, result.Config.SecurityMode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deserializing the result into a two-element deconstruction still works.</summary>
|
||||
[Fact]
|
||||
public void Deserialize_TwoElementDeconstruction_StillWorks()
|
||||
{
|
||||
@@ -219,6 +235,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("opc.tcp://x:4840", config.EndpointUrl);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null heartbeat is omitted from the flat dictionary.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_OmitsNullHeartbeat()
|
||||
{
|
||||
@@ -233,6 +250,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.False(dict.ContainsKey("HeartbeatMaxSilence"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the heartbeat is included in the flat dictionary when set.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_IncludesHeartbeat_WhenSet()
|
||||
{
|
||||
@@ -247,6 +265,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("45", dict["HeartbeatMaxSilence"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building a config from a flat dictionary round-trips all keys.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_RoundTripsAllKeys()
|
||||
{
|
||||
@@ -269,6 +288,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
|
||||
// ── Layer A/B extensions: subscription tuning, auth, deadband ──
|
||||
|
||||
/// <summary>Verifies that the new subscription scalar settings round-trip through serialization.</summary>
|
||||
[Fact]
|
||||
public void Serialize_RoundtripsNewSubscriptionScalars()
|
||||
{
|
||||
@@ -291,6 +311,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(OpcUaTimestampsToReturn.Both, round.TimestampsToReturn);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the deadband setting round-trips through serialization.</summary>
|
||||
[Fact]
|
||||
public void Serialize_RoundtripsDeadband()
|
||||
{
|
||||
@@ -312,6 +333,12 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(2.5, round.Deadband.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the user identity settings round-trip through serialization for each token type.</summary>
|
||||
/// <param name="tokenType">The user token type.</param>
|
||||
/// <param name="user">The user name.</param>
|
||||
/// <param name="pass">The password.</param>
|
||||
/// <param name="certPath">The user certificate path.</param>
|
||||
/// <param name="certPass">The user certificate password.</param>
|
||||
[Theory]
|
||||
[InlineData(OpcUaUserTokenType.UsernamePassword, "user1", "pass1", "", "")]
|
||||
[InlineData(OpcUaUserTokenType.X509Certificate, "", "", "/etc/pki/client.pfx", "pfxpass")]
|
||||
@@ -343,6 +370,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(certPass, round.UserIdentity.CertificatePassword);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null user identity and deadband are omitted from the typed JSON.</summary>
|
||||
[Fact]
|
||||
public void Serialize_NullUserIdentityAndDeadband_OmittedFromTypedJson()
|
||||
{
|
||||
@@ -356,6 +384,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Null(round.Deadband);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the new scalar settings are included in the flat dictionary.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_IncludesNewScalars()
|
||||
{
|
||||
@@ -375,6 +404,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("Server", dict["TimestampsToReturn"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null user identity and deadband are omitted from the flat dictionary.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_OmitsNullUserIdentityAndDeadband()
|
||||
{
|
||||
@@ -387,6 +417,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.False(dict.ContainsKey("Deadband.Value"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the user identity is included in the flat dictionary when set.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_IncludesUserIdentity_WhenSet()
|
||||
{
|
||||
@@ -407,6 +438,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("secret", dict["UserIdentity.Password"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the deadband is included in the flat dictionary when set.</summary>
|
||||
[Fact]
|
||||
public void ToFlatDict_IncludesDeadband_WhenSet()
|
||||
{
|
||||
@@ -421,6 +453,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("1.5", dict["Deadband.Value"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building a config from a flat dictionary materializes the user identity.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_MaterializesUserIdentity()
|
||||
{
|
||||
@@ -439,6 +472,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal("hunter2", config.UserIdentity.Password);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that building a config from a flat dictionary materializes the deadband.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_MaterializesDeadband()
|
||||
{
|
||||
@@ -455,6 +489,7 @@ public class OpcUaEndpointConfigSerializerTests
|
||||
Assert.Equal(0.25, config.Deadband.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an anonymous token type still materializes the user identity from a flat dictionary.</summary>
|
||||
[Fact]
|
||||
public void FromFlatDict_AnonymousTokenTypeStillMaterializesUserIdentity()
|
||||
{
|
||||
|
||||
+6
@@ -4,6 +4,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
|
||||
|
||||
public class OpcUaReferenceFormTests
|
||||
{
|
||||
/// <summary>Verifies that OPC UA reference strings are classified as durable or non-durable by form.</summary>
|
||||
/// <param name="reference">The OPC UA reference string under test.</param>
|
||||
/// <param name="expected">Whether the reference is expected to be durable.</param>
|
||||
[Theory]
|
||||
// durable → true
|
||||
[InlineData("nsu=https://zb.com/otopcua/raw;s=Line1.Pump.Speed", true)]
|
||||
@@ -16,6 +19,9 @@ public class OpcUaReferenceFormTests
|
||||
[InlineData(" ", true)]
|
||||
[InlineData(null, true)]
|
||||
// bare server-namespace index → false (warn)
|
||||
/// <summary>Verifies that reference forms are classified as durable or not as expected.</summary>
|
||||
/// <param name="reference">The OPC UA reference text, or null.</param>
|
||||
/// <param name="expected">The expected durability classification.</param>
|
||||
[InlineData("ns=2;s=MyDevice.Temperature", false)]
|
||||
[InlineData("ns=1;i=1001", false)]
|
||||
[InlineData("ns=10;s=x", false)]
|
||||
|
||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DynamicJsonElement"/>, including the Commons-002 regression:
|
||||
/// Tests for <see cref="DynamicJsonElement"/>, including the regression where
|
||||
/// a wrapped element must remain valid for deferred (script-time) access even after
|
||||
/// the <see cref="JsonDocument"/> it was parsed from has been disposed.
|
||||
/// </summary>
|
||||
@@ -19,8 +19,9 @@ public class DynamicJsonElementTests
|
||||
// now throw ObjectDisposedException on the first member access.
|
||||
}
|
||||
|
||||
// ── Commons-002 regression: lifetime independence from the source document ──
|
||||
// ── Regression: lifetime independence from the source document ──
|
||||
|
||||
/// <summary>Verifies that member access still works after the source document is disposed.</summary>
|
||||
[Fact]
|
||||
public void MemberAccess_WorksAfterSourceDocumentDisposed()
|
||||
{
|
||||
@@ -30,6 +31,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal(7, (int)obj.id);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that index access still works after the source document is disposed.</summary>
|
||||
[Fact]
|
||||
public void IndexAccess_WorksAfterSourceDocumentDisposed()
|
||||
{
|
||||
@@ -38,6 +40,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal("b", obj.items[1]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nested member access still works after the source document is disposed.</summary>
|
||||
[Fact]
|
||||
public void NestedAccess_WorksAfterSourceDocumentDisposed()
|
||||
{
|
||||
@@ -46,6 +49,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal(42, (int)obj.outer.inner.value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <c>ToString</c> still works after the source document is disposed.</summary>
|
||||
[Fact]
|
||||
public void ToString_WorksAfterSourceDocumentDisposed()
|
||||
{
|
||||
@@ -54,6 +58,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal("site-1", obj.label.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that access survives garbage collection of the source document.</summary>
|
||||
[Fact]
|
||||
public void Access_SurvivesGarbageCollection_OfSourceDocument()
|
||||
{
|
||||
@@ -78,6 +83,7 @@ public class DynamicJsonElementTests
|
||||
|
||||
// ── Basic conversion / access behavior ──────────────────────────
|
||||
|
||||
/// <summary>Verifies that a JSON number converts to an int.</summary>
|
||||
[Fact]
|
||||
public void Convert_NumberToInt()
|
||||
{
|
||||
@@ -85,6 +91,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal(123, (int)obj.n);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a JSON boolean converts to a bool.</summary>
|
||||
[Fact]
|
||||
public void Convert_BoolFromJson()
|
||||
{
|
||||
@@ -92,6 +99,7 @@ public class DynamicJsonElementTests
|
||||
Assert.True((bool)obj.flag);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that accessing a missing member throws a RuntimeBinderException.</summary>
|
||||
[Fact]
|
||||
public void MissingMember_Throws()
|
||||
{
|
||||
@@ -102,8 +110,9 @@ public class DynamicJsonElementTests
|
||||
() => { var _ = obj.doesNotExist; });
|
||||
}
|
||||
|
||||
// ── Commons-006 regression: TryConvert(object) must never null out a present value ──
|
||||
// ── Regression: TryConvert(object) must never null out a present value ──
|
||||
|
||||
/// <summary>Verifies that converting a present value to <c>object</c> returns a non-null result.</summary>
|
||||
[Fact]
|
||||
public void TryConvert_ObjectTarget_OnPresentValue_ReturnsNonNull()
|
||||
{
|
||||
@@ -118,6 +127,7 @@ public class DynamicJsonElementTests
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that converting a JSON null to <c>object</c> succeeds with a null result.</summary>
|
||||
[Fact]
|
||||
public void TryConvert_ObjectTarget_OnJsonNull_ReturnsNull()
|
||||
{
|
||||
@@ -131,6 +141,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that converting an unconvertible value to a non-object target reports failure.</summary>
|
||||
[Fact]
|
||||
public void TryConvert_NonObjectTarget_OnUnconvertibleValue_ReportsFailure()
|
||||
{
|
||||
@@ -145,8 +156,9 @@ public class DynamicJsonElementTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ── Commons-013 regression: integral index values other than int must work ──
|
||||
// ── Regression: integral index values other than int must work ──
|
||||
|
||||
/// <summary>Verifies that index access works with a long index.</summary>
|
||||
[Fact]
|
||||
public void IndexAccess_WithLongIndex_Works()
|
||||
{
|
||||
@@ -158,16 +170,20 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal("b", obj.items[idx]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that index access works with an index derived from a wrapped JSON number.</summary>
|
||||
[Fact]
|
||||
public void IndexAccess_WithIndexDerivedFromWrappedJsonNumber_Works()
|
||||
{
|
||||
// The exact failing case from Commons-013: count is a wrapped JSON number
|
||||
// The exact failing case from that regression: count is a wrapped JSON number
|
||||
// (unwrapped as long), so count - 1 is a long.
|
||||
dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ], "count": 3 }""");
|
||||
|
||||
Assert.Equal("c", obj.items[obj.count - 1]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that index access works with integral indexes that widen to an int.</summary>
|
||||
/// <param name="index">The test input.</param>
|
||||
/// <param name="expected">The test input.</param>
|
||||
[Theory]
|
||||
[InlineData((byte)0, "a")]
|
||||
[InlineData((short)1, "b")]
|
||||
@@ -178,6 +194,7 @@ public class DynamicJsonElementTests
|
||||
Assert.Equal(expected, obj.items[index]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a long index outside the array range throws.</summary>
|
||||
[Fact]
|
||||
public void IndexAccess_WithLongIndexOutOfRange_Throws()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
public class EnumTests
|
||||
{
|
||||
/// <summary>Verifies that an enum has exactly the expected member names, in order.</summary>
|
||||
/// <param name="enumType">The enum type under test.</param>
|
||||
/// <param name="expectedNames">The expected member names in declaration order.</param>
|
||||
[Theory]
|
||||
[InlineData(typeof(DataType), new[] { "Boolean", "Int32", "Float", "Double", "String", "DateTime", "Binary", "List" })]
|
||||
[InlineData(typeof(InstanceState), new[] { "NotDeployed", "Enabled", "Disabled" })]
|
||||
@@ -20,6 +23,8 @@ public class EnumTests
|
||||
Assert.Equal(expectedNames, actualNames);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an enum type has a singular name rather than a plural one.</summary>
|
||||
/// <param name="enumType">The enum type under test.</param>
|
||||
[Theory]
|
||||
[InlineData(typeof(DataType))]
|
||||
[InlineData(typeof(InstanceState))]
|
||||
|
||||
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Asserts the exact member sets of the Audit Log (#23) enums.
|
||||
/// Asserts the exact member sets of the Audit Log enums.
|
||||
/// Lock-in tests; any addition/removal/rename is a deliberate design change
|
||||
/// that must come with a corresponding update to alog.md §4.
|
||||
/// </summary>
|
||||
public class AuditEnumTests
|
||||
{
|
||||
/// <summary>Verifies that audit channel has exactly expected members.</summary>
|
||||
[Fact]
|
||||
public void AuditChannel_HasExactlyExpectedMembers()
|
||||
{
|
||||
@@ -22,6 +23,7 @@ public class AuditEnumTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that audit kind has exactly twenty two expected members.</summary>
|
||||
[Fact]
|
||||
public void AuditKind_HasExactlyTwentyTwoExpectedMembers()
|
||||
{
|
||||
@@ -45,6 +47,7 @@ public class AuditEnumTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that audit status has exactly eight expected members.</summary>
|
||||
[Fact]
|
||||
public void AuditStatus_HasExactlyEightExpectedMembers()
|
||||
{
|
||||
@@ -62,6 +65,7 @@ public class AuditEnumTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that audit forward state has exactly expected members.</summary>
|
||||
[Fact]
|
||||
public void AuditForwardState_HasExactlyExpectedMembers()
|
||||
{
|
||||
|
||||
@@ -4,11 +4,12 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Commons-010: coverage for the small computed-property logic on
|
||||
/// Coverage for the small computed-property logic on
|
||||
/// <see cref="ConfigurationDiff"/> and <see cref="ScriptScope"/>.
|
||||
/// </summary>
|
||||
public class FlatteningAndScriptScopeTests
|
||||
{
|
||||
/// <summary>Verifies that a configuration diff with no changes reports HasChanges as false.</summary>
|
||||
[Fact]
|
||||
public void ConfigurationDiff_NoChanges_HasChangesIsFalse()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public class FlatteningAndScriptScopeTests
|
||||
Assert.False(diff.HasChanges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a configuration diff containing an attribute change reports HasChanges as true.</summary>
|
||||
[Fact]
|
||||
public void ConfigurationDiff_WithAttributeChange_HasChangesIsTrue()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ public class FlatteningAndScriptScopeTests
|
||||
Assert.True(diff.HasChanges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a root script scope has no parent.</summary>
|
||||
[Fact]
|
||||
public void ScriptScope_Root_HasNoParent()
|
||||
{
|
||||
@@ -39,6 +42,7 @@ public class FlatteningAndScriptScopeTests
|
||||
Assert.Null(ScriptScope.Root.ParentPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script scope with a parent path reports HasParent as true.</summary>
|
||||
[Fact]
|
||||
public void ScriptScope_WithParentPath_HasParentIsTrue()
|
||||
{
|
||||
|
||||
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #36: <see cref="HandshakeResult"/> lets a script tell WHICH leg of a reset-aware
|
||||
/// <see cref="HandshakeResult"/> lets a script tell WHICH leg of a reset-aware
|
||||
/// handshake failed without naming the <see cref="HandshakePhase"/> enum (script bodies
|
||||
/// import only System/System.Collections.Generic/System.Linq/System.Threading.Tasks).
|
||||
/// </summary>
|
||||
public class HandshakeResultTests
|
||||
{
|
||||
/// <summary>Verifies that ResetFailed is true only when the reset phase did not match.</summary>
|
||||
[Fact]
|
||||
public void ResetFailed_IsTrue_OnlyForAnUnmatchedResetPhase()
|
||||
{
|
||||
|
||||
+16
-1
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.InboundApi;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the M9-T32b JSON-Schema <c>$ref</c> resolver: the caller-supplied
|
||||
/// Tests for the JSON-Schema <c>$ref</c> resolver: the caller-supplied
|
||||
/// resolution seam (<c>lib:Name</c> pointer convention), cycle/depth guarding,
|
||||
/// and dangling-ref reporting. The resolver is OPTIONAL — schemas without a
|
||||
/// <c>$ref</c> parse exactly as before (verified by the no-ref-unchanged tests).
|
||||
@@ -13,6 +13,7 @@ public class InboundApiSchemaRefTests
|
||||
{
|
||||
// ── Pointer convention: {"$ref":"lib:Name"} resolves through the seam ──────
|
||||
|
||||
/// <summary>Verifies that a library reference resolves to the referenced shape.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_LibRef_ResolvesToReferencedShape()
|
||||
{
|
||||
@@ -33,6 +34,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Equal("integer", field.Schema.Type);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a reference on a nested field is resolved inline.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_NestedFieldRef_ResolvesInline()
|
||||
{
|
||||
@@ -52,6 +54,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Equal("zip", Assert.Single(shipTo.Schema.Fields).Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing a library reference with a resolver yields the referenced shape.</summary>
|
||||
[Fact]
|
||||
public void Parse_LibRef_WithResolver_ResolvesToReferencedShape()
|
||||
{
|
||||
@@ -68,6 +71,7 @@ public class InboundApiSchemaRefTests
|
||||
|
||||
// ── Dangling ref: resolver returns null → reported, not silently dropped ──
|
||||
|
||||
/// <summary>Verifies that a dangling reference is reported.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DanglingRef_IsReported()
|
||||
{
|
||||
@@ -78,6 +82,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Contains("Missing", result.UnresolvedRefs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a dangling nested reference is reported.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DanglingNestedRef_IsReported()
|
||||
{
|
||||
@@ -88,6 +93,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Contains("Gone", result.UnresolvedRefs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a reference parsed without a resolver is reported as dangling.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_RefWithNoResolver_IsDangling()
|
||||
{
|
||||
@@ -100,6 +106,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Contains("Whatever", result.UnresolvedRefs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing a dangling reference with a resolver throws.</summary>
|
||||
[Fact]
|
||||
public void Parse_DanglingRef_WithResolver_Throws()
|
||||
{
|
||||
@@ -111,6 +118,7 @@ public class InboundApiSchemaRefTests
|
||||
|
||||
// ── Cycle / depth guard: controlled error, no stack overflow ──────────────
|
||||
|
||||
/// <summary>Verifies that a direct reference cycle is reported instead of overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DirectCycle_IsReportedNotStackOverflow()
|
||||
{
|
||||
@@ -128,6 +136,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Contains(result.UnresolvedRefs, r => r.Contains("Foo", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an indirect reference cycle is reported instead of overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_IndirectCycle_IsReportedNotStackOverflow()
|
||||
{
|
||||
@@ -145,6 +154,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.NotEmpty(result.UnresolvedRefs);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing a direct cycle with a resolver throws instead of overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void Parse_DirectCycle_WithResolver_ThrowsNotStackOverflow()
|
||||
{
|
||||
@@ -159,6 +169,7 @@ public class InboundApiSchemaRefTests
|
||||
name => schemas.GetValueOrDefault(name)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deep reference chain respects the depth cap without overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DeepRefChain_RespectsDepthCapWithoutOverflow()
|
||||
{
|
||||
@@ -179,6 +190,7 @@ public class InboundApiSchemaRefTests
|
||||
|
||||
// ── Behavior unchanged for schemas WITHOUT a $ref ─────────────────────────
|
||||
|
||||
/// <summary>Verifies that parsing a schema with no references gives the same result as the plain parse.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_NoRef_MatchesParse()
|
||||
{
|
||||
@@ -196,6 +208,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.False(withRefs.Schema.Fields[1].Required);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a schema with no references parses identically with or without a resolver.</summary>
|
||||
[Fact]
|
||||
public void Parse_NoRef_UnchangedWithOrWithoutResolver()
|
||||
{
|
||||
@@ -212,6 +225,7 @@ public class InboundApiSchemaRefTests
|
||||
Assert.Equal("number", withResolver.Items!.Type);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty input returns a null schema and no references.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_Empty_ReturnsNullSchemaNoRefs()
|
||||
{
|
||||
@@ -222,6 +236,7 @@ public class InboundApiSchemaRefTests
|
||||
|
||||
// ── Diamond: two sibling refs to the SAME name resolve (not a cycle) ───────
|
||||
|
||||
/// <summary>Verifies that sibling references to the same target resolve without being reported as a false cycle.</summary>
|
||||
[Fact]
|
||||
public void ParseWithRefs_DiamondSiblingRefs_ResolveBothNotFalseCycle()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
/// </summary>
|
||||
public class PurgeTimerScheduleTests
|
||||
{
|
||||
/// <summary>Verifies that a long interval is capped at the short first tick.</summary>
|
||||
[Fact]
|
||||
public void InitialDelay_LongInterval_IsCappedAtShortFirstTick()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public class PurgeTimerScheduleTests
|
||||
Assert.Equal(PurgeTimerSchedule.ShortFirstTick, PurgeTimerSchedule.InitialDelay(TimeSpan.FromHours(24)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an interval shorter than the short first tick is left unchanged.</summary>
|
||||
[Fact]
|
||||
public void InitialDelay_IntervalShorterThanShortFirstTick_IsUnchanged()
|
||||
{
|
||||
@@ -26,12 +28,14 @@ public class PurgeTimerScheduleTests
|
||||
Assert.Equal(fast, PurgeTimerSchedule.InitialDelay(fast));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an interval equal to the short first tick yields that value.</summary>
|
||||
[Fact]
|
||||
public void InitialDelay_IntervalEqualToShortFirstTick_IsThatValue()
|
||||
{
|
||||
Assert.Equal(PurgeTimerSchedule.ShortFirstTick, PurgeTimerSchedule.InitialDelay(PurgeTimerSchedule.ShortFirstTick));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the short first tick is five minutes.</summary>
|
||||
[Fact]
|
||||
public void ShortFirstTick_IsFiveMinutes()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
public class ResultTests
|
||||
{
|
||||
/// <summary>Verifies that creating a success produces a successful result.</summary>
|
||||
[Fact]
|
||||
public void Success_ShouldCreateSuccessfulResult()
|
||||
{
|
||||
@@ -14,6 +15,7 @@ public class ResultTests
|
||||
Assert.Equal(42, result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a failure produces a failed result.</summary>
|
||||
[Fact]
|
||||
public void Failure_ShouldCreateFailedResult()
|
||||
{
|
||||
@@ -24,6 +26,7 @@ public class ResultTests
|
||||
Assert.Equal("something went wrong", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading the value of a failed result throws.</summary>
|
||||
[Fact]
|
||||
public void Value_OnFailure_ShouldThrow()
|
||||
{
|
||||
@@ -32,6 +35,7 @@ public class ResultTests
|
||||
Assert.Throws<InvalidOperationException>(() => result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reading the error of a successful result throws.</summary>
|
||||
[Fact]
|
||||
public void Error_OnSuccess_ShouldThrow()
|
||||
{
|
||||
@@ -40,6 +44,7 @@ public class ResultTests
|
||||
Assert.Throws<InvalidOperationException>(() => result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that matching a successful result invokes the success handler.</summary>
|
||||
[Fact]
|
||||
public void Match_OnSuccess_ShouldCallOnSuccess()
|
||||
{
|
||||
@@ -52,6 +57,7 @@ public class ResultTests
|
||||
Assert.Equal("value=42", output);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that matching a failed result invokes the failure handler.</summary>
|
||||
[Fact]
|
||||
public void Match_OnFailure_ShouldCallOnFailure()
|
||||
{
|
||||
@@ -64,6 +70,7 @@ public class ResultTests
|
||||
Assert.Equal("error=bad", output);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a success result works with a nullable reference type.</summary>
|
||||
[Fact]
|
||||
public void Success_WithNullableReferenceType_ShouldWork()
|
||||
{
|
||||
@@ -73,14 +80,17 @@ public class ResultTests
|
||||
Assert.Equal("hello", result.Value);
|
||||
}
|
||||
|
||||
// ── Commons-011 regression: a failed Result must always carry a message ──
|
||||
// ── Regression: a failed Result must always carry a message ──
|
||||
|
||||
/// <summary>Verifies that creating a failure with a null error throws.</summary>
|
||||
[Fact]
|
||||
public void Failure_WithNullError_ShouldThrow()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => Result<int>.Failure(null!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a failure with a blank error throws.</summary>
|
||||
/// <param name="error">A blank or whitespace-only error message.</param>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
public class RetryPolicyTests
|
||||
{
|
||||
/// <summary>Verifies that retry policy should be immutable record.</summary>
|
||||
[Fact]
|
||||
public void RetryPolicy_ShouldBeImmutableRecord()
|
||||
{
|
||||
@@ -13,6 +14,7 @@ public class RetryPolicyTests
|
||||
Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that retry policy with expression should create new instance.</summary>
|
||||
[Fact]
|
||||
public void RetryPolicy_WithExpression_ShouldCreateNewInstance()
|
||||
{
|
||||
@@ -24,6 +26,7 @@ public class RetryPolicyTests
|
||||
Assert.Equal(original.Delay, modified.Delay);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that retry policy equal values should be equal.</summary>
|
||||
[Fact]
|
||||
public void RetryPolicy_EqualValues_ShouldBeEqual()
|
||||
{
|
||||
|
||||
@@ -4,17 +4,19 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Commons-010: coverage for <see cref="ScriptArgs.Normalize"/> — the script-call
|
||||
/// Coverage for <see cref="ScriptArgs.Normalize"/> — the script-call
|
||||
/// parameter normalizer (dictionary / anonymous-object / primitive-rejection paths).
|
||||
/// </summary>
|
||||
public class ScriptArgsTests
|
||||
{
|
||||
/// <summary>Verifies that normalizing null returns null.</summary>
|
||||
[Fact]
|
||||
public void Normalize_Null_ReturnsNull()
|
||||
{
|
||||
Assert.Null(ScriptArgs.Normalize(null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a read-only dictionary is returned as is.</summary>
|
||||
[Fact]
|
||||
public void Normalize_ReadOnlyDictionary_ReturnedAsIs()
|
||||
{
|
||||
@@ -26,6 +28,7 @@ public class ScriptArgsTests
|
||||
Assert.Same(input, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a plain dictionary is returned as is.</summary>
|
||||
[Fact]
|
||||
public void Normalize_PlainDictionary_ReturnedAsIs()
|
||||
{
|
||||
@@ -39,6 +42,7 @@ public class ScriptArgsTests
|
||||
Assert.Equal(1, result!["a"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the keys of a non-generic dictionary are converted to strings.</summary>
|
||||
[Fact]
|
||||
public void Normalize_NonGenericDictionary_KeysStringified()
|
||||
{
|
||||
@@ -49,6 +53,7 @@ public class ScriptArgsTests
|
||||
Assert.Equal("answer", result!["42"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the properties of an anonymous object become dictionary entries.</summary>
|
||||
[Fact]
|
||||
public void Normalize_AnonymousObject_PropertiesBecomeEntries()
|
||||
{
|
||||
@@ -58,6 +63,8 @@ public class ScriptArgsTests
|
||||
Assert.Equal(3, result["count"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that normalizing a primitive value throws.</summary>
|
||||
/// <param name="primitive">The primitive value to normalize.</param>
|
||||
[Theory]
|
||||
[InlineData(42)]
|
||||
[InlineData(true)]
|
||||
@@ -67,12 +74,14 @@ public class ScriptArgsTests
|
||||
Assert.Throws<ArgumentException>(() => ScriptArgs.Normalize(primitive));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that normalizing a string throws.</summary>
|
||||
[Fact]
|
||||
public void Normalize_String_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => ScriptArgs.Normalize("hello"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that normalizing a decimal throws.</summary>
|
||||
[Fact]
|
||||
public void Normalize_Decimal_Throws()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ public class ScriptParametersTests
|
||||
{
|
||||
// ── Non-nullable scalar Get<T> ──────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting an int returns the value when the stored type is already an int.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_ExactTypeMatch()
|
||||
{
|
||||
@@ -14,6 +15,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int converts a stored long.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_FromLong()
|
||||
{
|
||||
@@ -21,6 +23,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int parses a stored string.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_FromString()
|
||||
{
|
||||
@@ -28,6 +31,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int throws when the key is missing.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_MissingKey_Throws()
|
||||
{
|
||||
@@ -36,6 +40,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("Parameter 'x' not found", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int throws when the stored value is null.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_NullValue_Throws()
|
||||
{
|
||||
@@ -44,6 +49,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("Parameter 'x' value is null", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int throws when the stored value cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_Unparsable_Throws()
|
||||
{
|
||||
@@ -52,6 +58,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("could not be parsed as Int32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string returns a stored string directly.</summary>
|
||||
[Fact]
|
||||
public void Get_String_DirectReturn()
|
||||
{
|
||||
@@ -59,6 +66,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal("hello", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string throws when the stored value is null.</summary>
|
||||
[Fact]
|
||||
public void Get_String_NullValue_Throws()
|
||||
{
|
||||
@@ -67,6 +75,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("value is null", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string converts a stored int.</summary>
|
||||
[Fact]
|
||||
public void Get_String_FromInt()
|
||||
{
|
||||
@@ -74,6 +83,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal("42", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a bool returns true for a stored true value.</summary>
|
||||
[Fact]
|
||||
public void Get_Bool_True()
|
||||
{
|
||||
@@ -81,6 +91,7 @@ public class ScriptParametersTests
|
||||
Assert.True(p.Get<bool>("b"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double converts a stored long.</summary>
|
||||
[Fact]
|
||||
public void Get_Double_FromLong()
|
||||
{
|
||||
@@ -88,6 +99,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42.0, p.Get<double>("d"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double converts a stored float.</summary>
|
||||
[Fact]
|
||||
public void Get_Double_FromFloat()
|
||||
{
|
||||
@@ -95,6 +107,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(3.14, p.Get<double>("d"), 2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a long converts a stored int.</summary>
|
||||
[Fact]
|
||||
public void Get_Long_FromInt()
|
||||
{
|
||||
@@ -102,6 +115,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42L, p.Get<long>("l"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a float converts a stored double.</summary>
|
||||
[Fact]
|
||||
public void Get_Float_FromDouble()
|
||||
{
|
||||
@@ -109,6 +123,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(3.14f, p.Get<float>("f"), 2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a date and time parses a stored string.</summary>
|
||||
[Fact]
|
||||
public void Get_DateTime_FromString()
|
||||
{
|
||||
@@ -117,6 +132,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new DateTime(2026, 3, 22, 10, 30, 0, DateTimeKind.Utc), result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a date and time returns the value when the stored type is already a date and time.</summary>
|
||||
[Fact]
|
||||
public void Get_DateTime_ExactType()
|
||||
{
|
||||
@@ -127,6 +143,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── Nullable scalar Get<T?> ─────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting a nullable int returns null when the key is missing.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_MissingKey_ReturnsNull()
|
||||
{
|
||||
@@ -134,6 +151,7 @@ public class ScriptParametersTests
|
||||
Assert.Null(p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable int returns null when the stored value is null.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_NullValue_ReturnsNull()
|
||||
{
|
||||
@@ -141,6 +159,7 @@ public class ScriptParametersTests
|
||||
Assert.Null(p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable int returns the value when it is valid.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_ValidValue_ReturnsValue()
|
||||
{
|
||||
@@ -148,9 +167,10 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
// Commons-003: a parameter that is *present but unconvertible* is a caller/script
|
||||
// A parameter that is *present but unconvertible* is a caller/script
|
||||
// bug and must throw — not be silently mapped to null (which a script would
|
||||
// misread as "not supplied"). Genuinely absent/null still returns null.
|
||||
/// <summary>Verifies that getting a nullable int throws when the value is present but cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_PresentButUnparsable_Throws()
|
||||
{
|
||||
@@ -159,6 +179,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("could not be parsed as Int32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable int throws when the value is present but overflows.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableInt_PresentButOverflowing_Throws()
|
||||
{
|
||||
@@ -167,6 +188,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("could not be parsed as Int32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable date and time throws when the value is present but cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableDateTime_PresentButUnparsable_Throws()
|
||||
{
|
||||
@@ -174,6 +196,7 @@ public class ScriptParametersTests
|
||||
Assert.Throws<ScriptParameterException>(() => p.Get<DateTime?>("dt"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable double returns the value when it is valid.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableDouble_ValidValue()
|
||||
{
|
||||
@@ -181,6 +204,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(3.14, p.Get<double?>("d"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a nullable bool returns the value when it is valid.</summary>
|
||||
[Fact]
|
||||
public void Get_NullableBool_ValidValue()
|
||||
{
|
||||
@@ -190,6 +214,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── Array Get<T[]> ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting an int array converts a stored list of longs.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_FromListOfLongs()
|
||||
{
|
||||
@@ -198,6 +223,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { 1, 2, 3 }, p.Get<int[]>("arr"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array from an empty list returns an empty array.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_EmptyList()
|
||||
{
|
||||
@@ -205,6 +231,7 @@ public class ScriptParametersTests
|
||||
Assert.Empty(p.Get<int[]>("arr"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array throws when the stored value is not a list.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_NonList_Throws()
|
||||
{
|
||||
@@ -213,6 +240,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("is not a list or array", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array throws with the element index when an element cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_UnparsableElement_ThrowsWithIndex()
|
||||
{
|
||||
@@ -224,6 +252,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("Int32", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array throws when an element is null.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_NullElement_Throws()
|
||||
{
|
||||
@@ -233,6 +262,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("element at index 1 is null", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string array converts a stored list of strings.</summary>
|
||||
[Fact]
|
||||
public void Get_StringArray_FromListOfStrings()
|
||||
{
|
||||
@@ -241,6 +271,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { "a", "b", "c" }, p.Get<string[]>("arr"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double array converts a stored list of numbers.</summary>
|
||||
[Fact]
|
||||
public void Get_DoubleArray_FromListOfNumbers()
|
||||
{
|
||||
@@ -249,6 +280,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { 1.1, 2.2, 3.3 }, p.Get<double[]>("arr"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array throws when the key is missing.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_MissingKey_Throws()
|
||||
{
|
||||
@@ -259,6 +291,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── List<T> Get<List<T>> ────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting an int list converts a stored list of longs.</summary>
|
||||
[Fact]
|
||||
public void Get_ListInt_FromListOfLongs()
|
||||
{
|
||||
@@ -268,6 +301,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new List<int> { 10, 20, 30 }, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string list converts a stored list of strings.</summary>
|
||||
[Fact]
|
||||
public void Get_ListString_FromListOfStrings()
|
||||
{
|
||||
@@ -276,6 +310,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new List<string> { "x", "y" }, p.Get<List<string>>("items"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int list throws with the element index when an element cannot be parsed.</summary>
|
||||
[Fact]
|
||||
public void Get_ListInt_UnparsableElement_ThrowsWithIndex()
|
||||
{
|
||||
@@ -285,6 +320,7 @@ public class ScriptParametersTests
|
||||
Assert.Contains("element at index 1", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double list converts a stored list of mixed numeric types.</summary>
|
||||
[Fact]
|
||||
public void Get_ListDouble_FromMixedNumbers()
|
||||
{
|
||||
@@ -299,6 +335,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── Backward compatibility ──────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that the indexer returns the stored value for a key.</summary>
|
||||
[Fact]
|
||||
public void Indexer_Works()
|
||||
{
|
||||
@@ -306,6 +343,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal("val", p["key"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <c>ContainsKey</c> reports whether a key is present.</summary>
|
||||
[Fact]
|
||||
public void ContainsKey_Works()
|
||||
{
|
||||
@@ -314,6 +352,7 @@ public class ScriptParametersTests
|
||||
Assert.False(p.ContainsKey("missing"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <c>TryGetValue</c> returns the stored value for a present key.</summary>
|
||||
[Fact]
|
||||
public void TryGetValue_Works()
|
||||
{
|
||||
@@ -322,6 +361,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, val);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the count reflects the number of stored entries.</summary>
|
||||
[Fact]
|
||||
public void Count_Works()
|
||||
{
|
||||
@@ -329,6 +369,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(2, p.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that enumeration yields every stored entry.</summary>
|
||||
[Fact]
|
||||
public void Enumeration_Works()
|
||||
{
|
||||
@@ -338,6 +379,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { "a", "b" }, keys);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the empty constructor produces an empty dictionary.</summary>
|
||||
[Fact]
|
||||
public void EmptyConstructor_ProducesEmptyDictionary()
|
||||
{
|
||||
@@ -348,6 +390,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── Edge cases ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that getting a double converts a stored int.</summary>
|
||||
[Fact]
|
||||
public void Get_Double_FromInt()
|
||||
{
|
||||
@@ -357,6 +400,7 @@ public class ScriptParametersTests
|
||||
|
||||
// ── JsonElement values (from JSON deserialization) ─────────────
|
||||
|
||||
/// <summary>Verifies that getting an int reads a stored JSON element.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_FromJsonElement()
|
||||
{
|
||||
@@ -366,6 +410,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int array reads a stored list of JSON elements.</summary>
|
||||
[Fact]
|
||||
public void Get_IntArray_FromJsonElementList()
|
||||
{
|
||||
@@ -376,6 +421,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new[] { 10, 20, 30 }, p.Get<int[]>("items"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int list reads a stored list of JSON elements.</summary>
|
||||
[Fact]
|
||||
public void Get_ListInt_FromJsonElementList()
|
||||
{
|
||||
@@ -385,6 +431,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(new List<int> { 1, 2, 3 }, p.Get<List<int>>("items"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a string reads a stored JSON element.</summary>
|
||||
[Fact]
|
||||
public void Get_String_FromJsonElement()
|
||||
{
|
||||
@@ -394,6 +441,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal("hello", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a double reads a stored JSON element.</summary>
|
||||
[Fact]
|
||||
public void Get_Double_FromJsonElement()
|
||||
{
|
||||
@@ -403,6 +451,7 @@ public class ScriptParametersTests
|
||||
Assert.Equal(3.14, p.Get<double>("d"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a bool reads a stored JSON element.</summary>
|
||||
[Fact]
|
||||
public void Get_Bool_FromJsonElement()
|
||||
{
|
||||
@@ -412,6 +461,7 @@ public class ScriptParametersTests
|
||||
Assert.True(p.Get<bool>("b"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an int throws when the stored long overflows an int.</summary>
|
||||
[Fact]
|
||||
public void Get_Int_OverflowFromLong_Throws()
|
||||
{
|
||||
|
||||
+11
-1
@@ -12,6 +12,7 @@ public class SecretReferenceScannerTests
|
||||
private static DataConnectionArtifact Dc(string name, string? primary, string? backup = null)
|
||||
=> new(name, "MxGateway", primary, backup, 3);
|
||||
|
||||
/// <summary>Verifies that nested secret references are found case-insensitively across the primary and backup configuration.</summary>
|
||||
[Fact]
|
||||
public void FindsNestedRefs_CaseInsensitive_AcrossPrimaryAndBackup()
|
||||
{
|
||||
@@ -23,6 +24,8 @@ public class SecretReferenceScannerTests
|
||||
Assert.Equal(["GW1", "GW2"], refs.Single(r => r.Name == "mxgw-key").ReferencedBy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that values that are not secret references are ignored.</summary>
|
||||
/// <param name="json">The connection JSON that contains no secret reference.</param>
|
||||
[Theory]
|
||||
[InlineData("""{"ApiKey":"${secret:node-local}"}""")]
|
||||
[InlineData("""{"Note":"see secret:x in docs"}""")]
|
||||
@@ -34,6 +37,7 @@ public class SecretReferenceScannerTests
|
||||
public void IgnoresNonReferences(string json)
|
||||
=> Assert.Empty(SecretReferenceScanner.Scan([Dc("C", json)]));
|
||||
|
||||
/// <summary>Verifies that a connection referencing the same secret several times is reported once.</summary>
|
||||
[Fact]
|
||||
public void ReferencedBy_DeduplicatesWhenOneConnectionRefsTheSameSecretMultipleTimes()
|
||||
{
|
||||
@@ -47,8 +51,11 @@ public class SecretReferenceScannerTests
|
||||
Assert.Equal(["GW1"], refs.Single(r => r.Name == "mxgw-key").ReferencedBy);
|
||||
}
|
||||
|
||||
// ── Shared parser (final review M3) ──
|
||||
// ── Shared parser ──
|
||||
|
||||
/// <summary>Verifies that the parser accepts trimmed, case-insensitive secret references.</summary>
|
||||
/// <param name="value">The raw value to parse.</param>
|
||||
/// <param name="expected">The secret name the parser is expected to return.</param>
|
||||
[Theory]
|
||||
[InlineData("secret:mxgw-key", "mxgw-key")]
|
||||
[InlineData("SECRET:mxgw-key", "mxgw-key")]
|
||||
@@ -60,6 +67,8 @@ public class SecretReferenceScannerTests
|
||||
Assert.Equal(expected, name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the parser rejects values that are not secret references.</summary>
|
||||
/// <param name="value">The value that is not a secret reference.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -74,6 +83,7 @@ public class SecretReferenceScannerTests
|
||||
Assert.Equal(string.Empty, name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the scanner and the parser agree on the name of a padded, differently-cased reference.</summary>
|
||||
[Fact]
|
||||
public void Scan_AndParser_AgreeOnThePaddedCaseVariantName()
|
||||
{
|
||||
|
||||
@@ -11,12 +11,13 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
/// </summary>
|
||||
public class SiteCallOperationalTests
|
||||
{
|
||||
/// <summary>Verifies that a site call operational record carries its source node.</summary>
|
||||
[Fact]
|
||||
public void SiteCallOperational_carries_SourceNode()
|
||||
{
|
||||
// SourceNode identifies the cluster node that emitted the cached call
|
||||
// (site node-a/node-b or central-a/central-b). Nullable — callsites
|
||||
// pass null until INodeIdentityProvider stamping arrives in Task 14.
|
||||
// pass null until INodeIdentityProvider stamping arrives.
|
||||
var trackedId = TrackedOperationId.New();
|
||||
var nowUtc = new DateTime(2026, 5, 23, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
public class SiteNotificationKpiSnapshotTests
|
||||
{
|
||||
/// <summary>Verifies that the constructor assigns all members.</summary>
|
||||
[Fact]
|
||||
public void Constructor_AssignsAllMembers()
|
||||
{
|
||||
@@ -23,6 +24,7 @@ public class SiteNotificationKpiSnapshotTests
|
||||
Assert.Equal(TimeSpan.FromMinutes(12), snapshot.OldestPendingAge);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the oldest pending age is nullable for sites with no backlog.</summary>
|
||||
[Fact]
|
||||
public void OldestPendingAge_IsNullableForSitesWithNoBacklog()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for Commons-001: the check-then-act race between the timer
|
||||
/// Regression tests for the check-then-act race between the timer
|
||||
/// callback (<c>OnTimerElapsed</c>) and <c>OnValueReceived</c> / <c>Stop</c> / <c>Start</c>.
|
||||
/// Placed in the same non-parallel collection as <see cref="StaleTagMonitorTests"/> to
|
||||
/// avoid CPU contention from other timer-based tests running concurrently.
|
||||
|
||||
@@ -29,18 +29,22 @@ public class StaleTagMonitorTests
|
||||
return condition(); // final check
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing the monitor with a zero time span throws.</summary>
|
||||
[Fact]
|
||||
public void Constructor_ZeroTimeSpan_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StaleTagMonitor(TimeSpan.Zero));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing the monitor with a negative time span throws.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NegativeTimeSpan_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StaleTagMonitor(TimeSpan.FromSeconds(-1)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the stale event fires after the maximum silence elapses.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Stale_FiresAfterMaxSilence()
|
||||
{
|
||||
@@ -54,6 +58,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the stale event fires only once during continued silence.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Stale_FiresOnlyOnce()
|
||||
{
|
||||
@@ -72,6 +78,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that receiving a value resets the silence timer.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnValueReceived_ResetsTimer()
|
||||
{
|
||||
@@ -93,6 +101,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the stale event can still fire after silence following a received value.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnValueReceived_AllowsStaleAfterSilence()
|
||||
{
|
||||
@@ -110,6 +120,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that receiving a value resets the stale flag so the event can fire a second time.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task OnValueReceived_ResetsStaleFlag_AllowsSecondFire()
|
||||
{
|
||||
@@ -130,6 +142,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(2, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stopping the monitor prevents the stale event from firing.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Stop_PreventsStale()
|
||||
{
|
||||
@@ -143,6 +157,8 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the monitor prevents the stale event from firing.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_PreventsStale()
|
||||
{
|
||||
@@ -156,6 +172,7 @@ public class StaleTagMonitorTests
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the maximum silence property returns the configured value.</summary>
|
||||
[Fact]
|
||||
public void MaxSilence_ReturnsConfiguredValue()
|
||||
{
|
||||
|
||||
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3): tests for the strongly-typed cached-operation identifier
|
||||
/// Tests for the strongly-typed cached-operation identifier
|
||||
/// produced by <c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c> and
|
||||
/// surfaced to scripts via <c>Tracking.Status(id)</c>.
|
||||
/// </summary>
|
||||
public class TrackedOperationIdTests
|
||||
{
|
||||
/// <summary>Verifies that each new identifier is unique and non-empty.</summary>
|
||||
[Fact]
|
||||
public void New_ProducesUniqueIds()
|
||||
{
|
||||
@@ -20,6 +21,7 @@ public class TrackedOperationIdTests
|
||||
Assert.NotEqual(Guid.Empty, b.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that parsing the string form of an identifier restores the original value.</summary>
|
||||
[Fact]
|
||||
public void Parse_RoundTrip_PreservesValue()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ public class TrackedOperationIdTests
|
||||
Assert.Equal(original.Value, parsed.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invalid, null and empty input fail to parse and yield a default identifier.</summary>
|
||||
[Fact]
|
||||
public void TryParse_InvalidInput_ReturnsFalse()
|
||||
{
|
||||
@@ -45,6 +48,7 @@ public class TrackedOperationIdTests
|
||||
Assert.Equal(default, emptyResult);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that valid input parses successfully and yields the original identifier.</summary>
|
||||
[Fact]
|
||||
public void TryParse_ValidInput_ReturnsTrueAndId()
|
||||
{
|
||||
@@ -55,6 +59,7 @@ public class TrackedOperationIdTests
|
||||
Assert.Equal(original, parsed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two identifiers wrapping the same value are equal and share a hash code.</summary>
|
||||
[Fact]
|
||||
public void Equality_BasedOnValue()
|
||||
{
|
||||
@@ -68,6 +73,7 @@ public class TrackedOperationIdTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the string form uses the standard hyphenated GUID format.</summary>
|
||||
[Fact]
|
||||
public void ToString_StandardGuidFormat()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// M8 A1: focused shape / value-equality / round-trip tests for the Transport (#24)
|
||||
/// Focused shape / value-equality / round-trip tests for the Transport
|
||||
/// name-mapping DTOs — <see cref="SiteMapping"/>, <see cref="ConnectionMapping"/>,
|
||||
/// and <see cref="BundleNameMap"/>. These records cross the Central UI ⇆ ManagementActor
|
||||
/// boundary (and may be persisted in an import session) via System.Text.Json, so a
|
||||
@@ -16,6 +16,7 @@ public sealed class BundleNameMapTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>Verifies that the empty map has no sites or connections.</summary>
|
||||
[Fact]
|
||||
public void Empty_HasNoSitesOrConnections()
|
||||
{
|
||||
@@ -25,6 +26,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Empty(map.Connections);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the empty map is a stable shared instance.</summary>
|
||||
[Fact]
|
||||
public void Empty_IsStableInstance()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Same(BundleNameMap.Empty, BundleNameMap.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that site mappings with identical fields are equal.</summary>
|
||||
[Fact]
|
||||
public void SiteMapping_ValueEquality_HoldsForIdenticalFields()
|
||||
{
|
||||
@@ -42,6 +45,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that site mappings with different actions are not equal.</summary>
|
||||
[Fact]
|
||||
public void SiteMapping_ValueEquality_DiffersWhenActionDiffers()
|
||||
{
|
||||
@@ -51,6 +55,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.NotEqual(mapTo, createNew);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connection mappings with identical fields are equal.</summary>
|
||||
[Fact]
|
||||
public void ConnectionMapping_ValueEquality_HoldsForIdenticalFields()
|
||||
{
|
||||
@@ -61,6 +66,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a map-to-existing mapping carries a non-null target.</summary>
|
||||
[Fact]
|
||||
public void MapToExisting_CarriesNonNullTarget()
|
||||
{
|
||||
@@ -76,6 +82,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Equal("OPC-Primary", conn.TargetConnectionName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a create-new mapping leaves the target null.</summary>
|
||||
[Fact]
|
||||
public void CreateNew_LeavesTargetNull()
|
||||
{
|
||||
@@ -86,6 +93,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.Null(conn.TargetConnectionName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a JSON round trip preserves the sites and connections of a name map.</summary>
|
||||
[Fact]
|
||||
public void BundleNameMap_JsonRoundTrip_PreservesSitesAndConnections()
|
||||
{
|
||||
@@ -114,6 +122,7 @@ public sealed class BundleNameMapTests
|
||||
Assert.True(map.Connections.SequenceEqual(rt.Connections));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a JSON round trip of an empty name map stays empty.</summary>
|
||||
[Fact]
|
||||
public void BundleNameMap_Empty_JsonRoundTrip_PreservesEmptiness()
|
||||
{
|
||||
|
||||
+13
-1
@@ -3,12 +3,13 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Commons-015: <see cref="EncryptionMetadata"/> must reject malformed envelopes at
|
||||
/// <see cref="EncryptionMetadata"/> must reject malformed envelopes at
|
||||
/// the type boundary (unknown algorithm, unsupported KDF, sub-minimum or over-cap
|
||||
/// iteration counts, null salt/IV). Valid construction must round-trip the fields.
|
||||
/// </summary>
|
||||
public sealed class EncryptionMetadataTests
|
||||
{
|
||||
/// <summary>Verifies that the constructor accepts the documented values.</summary>
|
||||
[Fact]
|
||||
public void Constructor_WithDocumentedValues_Succeeds()
|
||||
{
|
||||
@@ -28,6 +29,8 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Equal("def", meta.IvB64);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown algorithm is rejected.</summary>
|
||||
/// <param name="algorithm">The unsupported algorithm name.</param>
|
||||
[Theory]
|
||||
[InlineData("AES-128-CBC")] // weaker algorithm
|
||||
[InlineData("AES-256-CBC")] // unauthenticated mode
|
||||
@@ -47,6 +50,8 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Contains("AES-256-GCM", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown key-derivation function is rejected.</summary>
|
||||
/// <param name="kdf">The unsupported key-derivation function name.</param>
|
||||
[Theory]
|
||||
[InlineData("PBKDF2-SHA1")] // weaker hash
|
||||
[InlineData("argon2id")] // unsupported KDF
|
||||
@@ -65,6 +70,8 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Contains("PBKDF2-SHA256", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an out-of-range iteration count is rejected.</summary>
|
||||
/// <param name="iterations">The out-of-range iteration count.</param>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
@@ -84,6 +91,8 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Equal("Iterations", ex.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an iteration count at the allowed boundary is accepted.</summary>
|
||||
/// <param name="iterations">The boundary iteration count.</param>
|
||||
[Theory]
|
||||
[InlineData(100_000)] // OWASP minimum (exact)
|
||||
[InlineData(600_000)] // design-doc production value
|
||||
@@ -101,6 +110,7 @@ public sealed class EncryptionMetadataTests
|
||||
Assert.Equal(iterations, meta.Iterations);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null salt is rejected.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullSalt_Throws()
|
||||
{
|
||||
@@ -113,6 +123,7 @@ public sealed class EncryptionMetadataTests
|
||||
IvB64: "def"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null initialization vector is rejected.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullIv_Throws()
|
||||
{
|
||||
@@ -124,6 +135,7 @@ public sealed class EncryptionMetadataTests
|
||||
IvB64: null!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty salt and initialization vector are accepted for the seed pattern.</summary>
|
||||
[Fact]
|
||||
public void Constructor_EmptySaltAndIv_Succeeds_ForSeedPattern()
|
||||
{
|
||||
|
||||
@@ -4,18 +4,16 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Commons-020: focused shape / round-trip tests for the Transport (#24) record DTOs
|
||||
/// Focused shape / round-trip tests for the Transport record DTOs
|
||||
/// — <see cref="BundleManifest"/>, <see cref="ExportSelection"/>,
|
||||
/// <see cref="ImportPreview"/>, <see cref="ImportResolution"/>, and
|
||||
/// <see cref="ImportResult"/>. These records cross the Central UI ⇆ bundle file boundary
|
||||
/// via System.Text.Json, so a positional/tuple slip would break bundles in the field.
|
||||
/// EncryptionMetadata has its own focused tests under EncryptionMetadataTests.cs
|
||||
/// (Commons-015) and is reused here only to populate manifest fixtures.
|
||||
/// and is reused here only to populate manifest fixtures.
|
||||
/// </summary>
|
||||
public sealed class TransportRecordsTests
|
||||
{
|
||||
// STM: TransportRecordsTests-Commons-020 marker — used by grep verification.
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
@@ -25,6 +23,7 @@ public sealed class TransportRecordsTests
|
||||
// BundleManifest
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the <c>BundleManifest</c> constructor preserves all fields.</summary>
|
||||
[Fact]
|
||||
public void BundleManifest_Constructor_RoundTripsAllFields()
|
||||
{
|
||||
@@ -62,6 +61,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.Equal("Pump", manifest.Contents[0].Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a <c>BundleManifest</c> survives a JSON round trip with all fields intact.</summary>
|
||||
[Fact]
|
||||
public void BundleManifest_JsonRoundTrip_PreservesAllFields()
|
||||
{
|
||||
@@ -106,6 +106,7 @@ public sealed class TransportRecordsTests
|
||||
// ExportSelection
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the <c>ExportSelection</c> constructor preserves every id list.</summary>
|
||||
[Fact]
|
||||
public void ExportSelection_Constructor_PreservesAllIdLists()
|
||||
{
|
||||
@@ -126,6 +127,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.True(sel.IncludeDependencies);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>ExportSelection</c> JSON round trip preserves the include-dependencies flag and ids.</summary>
|
||||
[Fact]
|
||||
public void ExportSelection_JsonRoundTrip_PreservesIncludeDependenciesAndIds()
|
||||
{
|
||||
@@ -152,6 +154,7 @@ public sealed class TransportRecordsTests
|
||||
// ImportPreview
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an <c>ImportPreview</c> can be constructed with every conflict kind.</summary>
|
||||
[Fact]
|
||||
public void ImportPreview_Constructor_AllowsAllConflictKinds()
|
||||
{
|
||||
@@ -175,6 +178,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.Equal("Parameters property mismatch", preview.Items[3].BlockerReason);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>ImportPreview</c> JSON round trip preserves the conflict kind and optional fields.</summary>
|
||||
[Fact]
|
||||
public void ImportPreview_JsonRoundTrip_PreservesConflictKindAndOptionalFields()
|
||||
{
|
||||
@@ -201,6 +205,9 @@ public sealed class TransportRecordsTests
|
||||
// ImportResolution
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the <c>ImportResolution</c> constructor preserves each resolution action and rename target.</summary>
|
||||
/// <param name="action">The resolution action under test.</param>
|
||||
/// <param name="renameTo">The rename target, or null when the action is not a rename.</param>
|
||||
[Theory]
|
||||
[InlineData(ResolutionAction.Add, null)]
|
||||
[InlineData(ResolutionAction.Overwrite, null)]
|
||||
@@ -215,6 +222,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.Equal(renameTo, res.RenameTo);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>ImportResolution</c> JSON round trip preserves the rename target.</summary>
|
||||
[Fact]
|
||||
public void ImportResolution_JsonRoundTrip_PreservesRenameTo()
|
||||
{
|
||||
@@ -232,6 +240,7 @@ public sealed class TransportRecordsTests
|
||||
// ImportResult
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the <c>ImportResult</c> constructor preserves all counters and stale instance ids.</summary>
|
||||
[Fact]
|
||||
public void ImportResult_Constructor_PreservesAllCountersAndStaleIds()
|
||||
{
|
||||
@@ -254,6 +263,7 @@ public sealed class TransportRecordsTests
|
||||
Assert.Equal("audit-corr-001", result.AuditEventCorrelation);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an <c>ImportResult</c> JSON round trip preserves the counts and correlation id.</summary>
|
||||
[Fact]
|
||||
public void ImportResult_JsonRoundTrip_PreservesCountsAndCorrelation()
|
||||
{
|
||||
@@ -280,6 +290,7 @@ public sealed class TransportRecordsTests
|
||||
// Record equality sanity (catches positional/tuple slip)
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that transport records compare equal by value.</summary>
|
||||
[Fact]
|
||||
public void TransportRecords_RecordValueEquality()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ValidationResult.SummarizeErrors"/> (followup #8) — the grouped,
|
||||
/// Tests for <see cref="ValidationResult.SummarizeErrors"/> — the grouped,
|
||||
/// capped summary that replaces the flat semicolon-joined deploy error dump.
|
||||
/// </summary>
|
||||
public class ValidationResultSummaryTests
|
||||
@@ -14,12 +14,14 @@ public class ValidationResultSummaryTests
|
||||
$"Attribute '{canonicalName}' has a data source reference but no connection binding.",
|
||||
canonicalName);
|
||||
|
||||
/// <summary>Verifies that a result with no errors summarizes to an empty string.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_NoErrors_ReturnsEmpty()
|
||||
{
|
||||
Assert.Equal(string.Empty, ValidationResult.Success().SummarizeErrors());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the summary leads with the total error count.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_LeadsWithTotalCount()
|
||||
{
|
||||
@@ -33,6 +35,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.StartsWith("3 errors:", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a single error uses the singular noun.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_SingleError_UsesSingularNoun()
|
||||
{
|
||||
@@ -40,6 +43,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.StartsWith("1 error:", result.SummarizeErrors());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unbound attributes are rolled up by module with counts.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_RollsUpUnboundAttributesByModuleWithCounts()
|
||||
{
|
||||
@@ -58,6 +62,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.DoesNotContain("Attr11' has a data source reference", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the number of listed modules is capped with an "and N more" suffix.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_CapsModuleBreadthWithAndNMore()
|
||||
{
|
||||
@@ -70,6 +75,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.Contains("Module0 (1)", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a top-level member is grouped under the root.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_TopLevelMember_GroupsUnderRoot()
|
||||
{
|
||||
@@ -77,6 +83,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.Contains("(root) (1)", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that entries without an entity name fall back to a capped message list.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_EntriesWithoutEntityName_FallBackToCappedMessageList()
|
||||
{
|
||||
@@ -91,6 +98,7 @@ public class ValidationResultSummaryTests
|
||||
Assert.Contains("… and 3 more", summary);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that mixed error categories are listed as separate category groups.</summary>
|
||||
[Fact]
|
||||
public void SummarizeErrors_MixedCategories_ListsEachCategoryGroup()
|
||||
{
|
||||
|
||||
@@ -4,32 +4,36 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ValueFormatter"/>. Includes the Commons-012 regression:
|
||||
/// Tests for <see cref="ValueFormatter"/>. Includes the culture-invariance regression:
|
||||
/// formatting must be culture-invariant because the formatter feeds non-UI contexts
|
||||
/// (gRPC stream events, logs) where locale-dependent output would be inconsistent.
|
||||
/// </summary>
|
||||
public class ValueFormatterTests
|
||||
{
|
||||
/// <summary>Verifies that formatting a null value returns an empty string.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_Null_ReturnsEmptyString()
|
||||
{
|
||||
Assert.Equal("", ValueFormatter.FormatDisplayValue(null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that formatting a string returns the value unchanged.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_String_ReturnsValueUnchanged()
|
||||
{
|
||||
Assert.Equal("hello", ValueFormatter.FormatDisplayValue("hello"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that formatting a collection joins the elements with a comma.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_Collection_JoinsWithComma()
|
||||
{
|
||||
Assert.Equal("1,2,3", ValueFormatter.FormatDisplayValue(new[] { 1, 2, 3 }));
|
||||
}
|
||||
|
||||
// ── Commons-012 regression: culture-invariant numeric/date formatting ──
|
||||
// ── Culture-invariance regression: numeric/date formatting ──
|
||||
|
||||
/// <summary>Verifies that formatting a double uses the invariant culture regardless of the thread culture.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_Double_UsesInvariantCulture_RegardlessOfThreadCulture()
|
||||
{
|
||||
@@ -46,6 +50,7 @@ public class ValueFormatterTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that formatting a DateTime uses the invariant culture regardless of the thread culture.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_DateTime_UsesInvariantCulture_RegardlessOfThreadCulture()
|
||||
{
|
||||
@@ -63,6 +68,7 @@ public class ValueFormatterTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that formatting a collection of doubles uses the invariant culture.</summary>
|
||||
[Fact]
|
||||
public void FormatDisplayValue_CollectionOfDoubles_UsesInvariantCulture()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user