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);
+ }
+ }
}