diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunner.Helpers.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunner.Helpers.cs index 1c732205..d0195447 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunner.Helpers.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunner.Helpers.cs @@ -224,6 +224,94 @@ public static partial class CliRunner /// Site id. public static Task DeleteSiteAsync(int id) => BestEffortAsync("site", "delete", id); + /// + /// Creates a data connection on a site via data-connection create and returns its new id. + /// + public static async Task CreateDataConnectionAsync(int siteId, string name, string protocol = "OpcUa", string? primaryConfig = null) + { + var inv = System.Globalization.CultureInfo.InvariantCulture; + var args = new List + { + "data-connection", "create", + "--site-id", siteId.ToString(inv), + "--name", name, + "--protocol", protocol, + }; + if (!string.IsNullOrEmpty(primaryConfig)) + { + args.Add("--primary-config"); + args.Add(primaryConfig); + } + + using var doc = await RunJsonAsync([.. args]); + return RequireId(doc, "data-connection create"); + } + + /// Best-effort delete of a data connection via data-connection delete for teardown. + public static Task DeleteDataConnectionAsync(int id) => BestEffortAsync("data-connection", "delete", id); + + /// + /// Creates an inbound API method via api-method create (so it appears as a checkbox in the + /// API-key form) and returns its new id. + /// + public static async Task CreateApiMethodAsync(string name, string script = "return null;") + { + using var doc = await RunJsonAsync("api-method", "create", "--name", name, "--script", script); + return RequireId(doc, "api-method create"); + } + + /// Best-effort delete of an API method via api-method delete for teardown. + public static Task DeleteApiMethodAsync(int id) => BestEffortAsync("api-method", "delete", id); + + /// + /// Resolves an API key's opaque string keyId from its display name via + /// security api-key list; returns if no key matches. + /// + public static async Task ResolveApiKeyIdByNameAsync(string name) + { + using var doc = await RunJsonAsync("security", "api-key", "list"); + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + foreach (var key in doc.RootElement.EnumerateArray()) + { + if (key.TryGetProperty("name", out var n) + && n.ValueKind == JsonValueKind.String + && string.Equals(n.GetString(), name, StringComparison.Ordinal) + && key.TryGetProperty("keyId", out var k) + && k.ValueKind == JsonValueKind.String) + { + return k.GetString(); + } + } + } + + return null; + } + + /// + /// Best-effort delete of an API key via security api-key delete --key-id for teardown. + /// The key id is an opaque string, so this cannot use the int-based . + /// + public static async Task DeleteApiKeyAsync(string keyId) + { + try + { + await RunAsync("security", "api-key", "delete", "--key-id", keyId); + } + catch + { + // Best-effort teardown — never mask the test's own failure. + } + } + + /// + /// Reads an instance's full configuration via instance get; the returned document exposes + /// connectionBindings, attributeOverrides, and areaId for persistence read-back. + /// Caller owns the returned . + /// + public static Task GetInstanceAsync(int id) => + RunJsonAsync("instance", "get", "--id", id.ToString(System.Globalization.CultureInfo.InvariantCulture)); + /// /// Returns the ids of all templates whose name starts with /// , via template list. diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunnerHelpersTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunnerHelpersTests.cs index 672104c6..38f2142d 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunnerHelpersTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunnerHelpersTests.cs @@ -43,4 +43,45 @@ public class CliRunnerHelpersTests Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); Assert.True(await CliRunner.ResolveSiteIdAsync("site-a") > 0); } + + /// + /// A freshly created data connection returns a positive id and is cleanly + /// deleted in teardown, exercising + /// and as a round-trip. + /// + [SkippableFact] + public async Task CreateThenDeleteDataConnection_RoundTrips() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + var siteId = await CliRunner.ResolveSiteIdAsync("site-a"); + var id = await CliRunner.CreateDataConnectionAsync(siteId, CliRunner.UniqueName("conn")); + try + { + Assert.True(id > 0); + } + finally + { + await CliRunner.DeleteDataConnectionAsync(id); + } + } + + /// + /// A freshly created API method returns a positive id and is cleanly deleted in + /// teardown, exercising and + /// as a round-trip. + /// + [SkippableFact] + public async Task CreateThenDeleteApiMethod_RoundTrips() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + var id = await CliRunner.CreateApiMethodAsync(CliRunner.UniqueName("method")); + try + { + Assert.True(id > 0); + } + finally + { + await CliRunner.DeleteApiMethodAsync(id); + } + } }