using System.CommandLine; using ZB.MOM.WW.ScadaBridge.CLI.Commands; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; namespace ZB.MOM.WW.ScadaBridge.CLI.Tests; /// /// Regression tests: malformed --bindings / --overrides JSON /// previously threw unhandled exceptions instead of producing a clean validation error. /// public class InstanceArgumentParsingTests { /// Verifies that valid bindings JSON is parsed into attribute and connection id pairs. [Fact] public void ParseBindings_ValidJson_ReturnsPairs() { var ok = InstanceCommands.TryParseBindings( "[[\"Speed\", 5], [\"Mode\", 7]]", out var bindings, out var error); Assert.True(ok); Assert.Null(error); Assert.Equal(2, bindings!.Count); Assert.Equal(new ConnectionBinding("Speed", 5), bindings[0]); Assert.Equal(new ConnectionBinding("Mode", 7), bindings[1]); } /// Verifies that malformed bindings JSON yields an error message instead of throwing an exception. [Fact] public void ParseBindings_MalformedJson_ReturnsErrorNotException() { var ok = InstanceCommands.TryParseBindings("not json", out var bindings, out var error); Assert.False(ok); Assert.Null(bindings); Assert.NotNull(error); } /// Verifies that a binding pair with too few elements yields an error instead of throwing. [Fact] public void ParseBindings_ShortPair_ReturnsErrorNotException() { // A pair with fewer than two elements previously threw ArgumentOutOfRangeException. var ok = InstanceCommands.TryParseBindings("[[\"Speed\"]]", out var bindings, out var error); Assert.False(ok); Assert.Null(bindings); Assert.NotNull(error); } /// Verifies that binding elements of the wrong JSON types yield an error instead of throwing. [Fact] public void ParseBindings_WrongElementTypes_ReturnsErrorNotException() { // A non-string name / non-int id previously threw InvalidOperationException. var ok = InstanceCommands.TryParseBindings("[[5, \"Speed\"]]", out var bindings, out var error); Assert.False(ok); Assert.Null(bindings); Assert.NotNull(error); } /// Verifies that a bindings document of JSON null yields an error instead of throwing. [Fact] public void ParseBindings_JsonNull_ReturnsErrorNotException() { var ok = InstanceCommands.TryParseBindings("null", out var bindings, out var error); Assert.False(ok); Assert.Null(bindings); Assert.NotNull(error); } /// Verifies that the optional third binding element is captured as a data source reference override. [Fact] public void ParseBindings_ThreeElementForm_CapturesReferenceOverride() { // The optional 3rd element carries the per-instance data-source reference // override so the CLI can SET it instead of always wiping it. var ok = InstanceCommands.TryParseBindings( "[[\"Speed\", 5, \"ns=2;s=Spd\"], [\"Mode\", 7]]", out var bindings, out var error); Assert.True(ok); Assert.Null(error); Assert.Equal(2, bindings!.Count); Assert.Equal(new ConnectionBinding("Speed", 5, "ns=2;s=Spd"), bindings[0]); // A 2-element entry still leaves the override null (template default applies). Assert.Equal(new ConnectionBinding("Mode", 7, null), bindings[1]); Assert.Null(bindings[1].DataSourceReferenceOverride); } /// Verifies that an explicit null third element leaves the reference override unset. [Fact] public void ParseBindings_ThirdElementExplicitNull_LeavesOverrideUnset() { var ok = InstanceCommands.TryParseBindings( "[[\"Speed\", 5, null]]", out var bindings, out var error); Assert.True(ok); Assert.Null(error); Assert.Single(bindings!); Assert.Null(bindings![0].DataSourceReferenceOverride); } /// Verifies that a third binding element of the wrong type yields an error instead of throwing. [Fact] public void ParseBindings_ThirdElementWrongType_ReturnsErrorNotException() { // A non-string, non-null 3rd element is rejected with a clean error. var ok = InstanceCommands.TryParseBindings( "[[\"Speed\", 5, 99]]", out var bindings, out var error); Assert.False(ok); Assert.Null(bindings); Assert.NotNull(error); } /// Verifies that a binding with four elements yields an error instead of throwing. [Fact] public void ParseBindings_FourElements_ReturnsErrorNotException() { var ok = InstanceCommands.TryParseBindings( "[[\"Speed\", 5, \"ref\", \"extra\"]]", out var bindings, out var error); Assert.False(ok); Assert.Null(bindings); Assert.NotNull(error); } /// Verifies that valid overrides JSON is parsed into a dictionary. [Fact] public void ParseOverrides_ValidJson_ReturnsDictionary() { var ok = InstanceCommands.TryParseOverrides( "{\"Speed\": \"100\", \"Mode\": null}", out var overrides, out var error); Assert.True(ok); Assert.Null(error); Assert.Equal(2, overrides!.Count); Assert.Equal("100", overrides["Speed"]); Assert.Null(overrides["Mode"]); } /// Verifies that malformed overrides JSON yields an error instead of throwing. [Fact] public void ParseOverrides_MalformedJson_ReturnsErrorNotException() { var ok = InstanceCommands.TryParseOverrides("{bad json", out var overrides, out var error); Assert.False(ok); Assert.Null(overrides); Assert.NotNull(error); } /// Verifies that an overrides document of JSON null yields an error instead of throwing. [Fact] public void ParseOverrides_JsonNull_ReturnsErrorNotException() { var ok = InstanceCommands.TryParseOverrides("null", out var overrides, out var error); Assert.False(ok); Assert.Null(overrides); Assert.NotNull(error); } // --- instance bind-module (grouped connection bindings, 2026-08-20) --- /// Verifies that a module rewrite changes only the named module's bindings and applies the requested connection. [Fact] public void BuildModuleRewrite_RewritesOnlyTheModule_AndKeepsConnection() { var existing = new[] { new ConnectionBinding("RightMESReceiver.MoveInFlag", 1, "MESReceiver_024.MoveInFlag"), new ConnectionBinding("RightMESReceiver.ScanStateCmd", 1, "MESReceiver_024.ScanStateCmd"), new ConnectionBinding("LeftMESReceiver.MoveInFlag", 1, "MESReceiver_023.MoveInFlag"), }; var ok = InstanceCommands.BuildModuleRewrite(existing, "RightMESReceiver", connectionId: 1, objectPath: "MESReceiver_022", out var rewritten, out var error); Assert.True(ok, error); Assert.Equal(2, rewritten!.Count); Assert.All(rewritten, b => Assert.StartsWith("MESReceiver_022.", b.DataSourceReferenceOverride!)); Assert.Contains(rewritten, b => b.AttributeName == "RightMESReceiver.MoveInFlag" && b.DataSourceReferenceOverride == "MESReceiver_022.MoveInFlag"); Assert.DoesNotContain(rewritten, b => b.AttributeName.StartsWith("Left")); } /// Verifies that the instance-level group matches attribute names that contain no dot. [Fact] public void BuildModuleRewrite_InstanceGroup_MatchesDotlessNames() { var existing = new[] { new ConnectionBinding("ChlorineFlow", 1, "Z28061.ChlorineFlow"), new ConnectionBinding("M.A", 1, "O.A") }; Assert.True(InstanceCommands.BuildModuleRewrite(existing, "(instance)", 1, "Z28063", out var r, out _)); var b = Assert.Single(r!); Assert.Equal("Z28063.ChlorineFlow", b.DataSourceReferenceOverride); } /// Verifies that an empty object path clears the data source reference override. [Fact] public void BuildModuleRewrite_EmptyObject_ClearsOverride() { var existing = new[] { new ConnectionBinding("M.A", 1, "O.A") }; Assert.True(InstanceCommands.BuildModuleRewrite(existing, "M", 2, "", out var r, out _)); var b = Assert.Single(r!); Assert.Null(b.DataSourceReferenceOverride); Assert.Equal(2, b.DataConnectionId); } /// Verifies that surrounding whitespace and a trailing dot on the object path are trimmed. [Fact] public void BuildModuleRewrite_TrailingDotOnObject_IsTrimmed() { var existing = new[] { new ConnectionBinding("M.A", 1, "O.A") }; Assert.True(InstanceCommands.BuildModuleRewrite(existing, "M", 1, " X. ", out var r, out _)); Assert.Equal("X.A", Assert.Single(r!).DataSourceReferenceOverride); } /// Verifies that an unknown module name produces an error listing the available modules. [Fact] public void BuildModuleRewrite_UnknownModule_ListsAvailable() { var existing = new[] { new ConnectionBinding("M.A", 1, "O.A"), new ConnectionBinding("N.B", 1, "O.B"), new ConnectionBinding("Direct", 1, null) }; Assert.False(InstanceCommands.BuildModuleRewrite(existing, "Q", 1, "X", out var rewritten, out var error)); Assert.Null(rewritten); Assert.Contains("M", error); Assert.Contains("N", error); Assert.Contains("(instance)", error); } /// Verifies that existing bindings are read from camelCase instance JSON. [Fact] public void ParseExistingBindings_ReadsCamelCaseInstanceJson() { const string json = """ {"id":7,"uniqueName":"X","connectionBindings":[ {"id":1,"instanceId":7,"attributeName":"M.A","dataConnectionId":3,"dataSourceReferenceOverride":"O.A"}, {"id":2,"instanceId":7,"attributeName":"Direct","dataConnectionId":4,"dataSourceReferenceOverride":null}, {"id":3,"instanceId":7,"attributeName":"NoOverrideProp","dataConnectionId":5} ]} """; var bindings = InstanceCommands.ParseExistingBindings(json); Assert.Equal(3, bindings.Count); Assert.Equal(new ConnectionBinding("M.A", 3, "O.A"), bindings[0]); Assert.Equal(new ConnectionBinding("Direct", 4), bindings[1]); Assert.Equal(new ConnectionBinding("NoOverrideProp", 5), bindings[2]); } /// Verifies that instance JSON without a bindings array yields an empty list. [Fact] public void ParseExistingBindings_MissingArray_ReturnsEmpty() { Assert.Empty(InstanceCommands.ParseExistingBindings("""{"id":7}""")); Assert.Empty(InstanceCommands.ParseExistingBindings("""{"id":7,"connectionBindings":null}""")); } /// Verifies that the bind-module command parses its options and rejects a missing object path. [Fact] public void BindModule_ParsesOptions() { var instance = InstanceCommands.Build( new Option("--url"), new Option("--format"), new Option("--username"), new Option("--password")); var bindModule = instance.Subcommands.Single(c => c.Name == "bind-module"); var result = bindModule.Parse(["--id", "1", "--module", "RightMESReceiver", "--connection", "1", "--object", "MESReceiver_022", "--dry-run"]); Assert.Empty(result.Errors); var missingObject = bindModule.Parse(["--id", "1", "--module", "M", "--connection", "1"]); Assert.NotEmpty(missingObject.Errors); } }