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 f505490b..22146f07 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
@@ -435,6 +435,85 @@ public static partial class CliRunner
return ids;
}
+ ///
+ /// Creates an external system via external-system create and returns its new id.
+ ///
+ /// External system name (typically from ).
+ /// Endpoint base URL (defaults to an unreachable placeholder).
+ /// Auth type token; one of ApiKey or BasicAuth.
+ ///
+ /// The CLI failed, or the response did not carry an integer id.
+ ///
+ public static async Task CreateExternalSystemAsync(
+ string name, string endpointUrl = "https://example.invalid/api", string authType = "ApiKey")
+ {
+ using var doc = await RunJsonAsync(
+ "external-system", "create",
+ "--name", name, "--endpoint-url", endpointUrl, "--auth-type", authType);
+ return RequireId(doc, "external-system create");
+ }
+
+ ///
+ /// Creates a notification list via notification create and returns its new id.
+ ///
+ /// Notification list name (typically from ).
+ /// Comma-separated recipient emails.
+ ///
+ /// The CLI failed, or the response did not carry an integer id.
+ ///
+ public static async Task CreateNotificationListAsync(string name, string emails = "noreply@example.invalid")
+ {
+ using var doc = await RunJsonAsync("notification", "create", "--name", name, "--emails", emails);
+ return RequireId(doc, "notification create");
+ }
+
+ ///
+ /// Creates a shared script via shared-script create and returns its new id.
+ ///
+ /// Shared script name (typically from ).
+ /// Script body.
+ ///
+ /// The CLI failed, or the response did not carry an integer id.
+ ///
+ public static async Task CreateSharedScriptAsync(string name, string code = "return null;")
+ {
+ using var doc = await RunJsonAsync("shared-script", "create", "--name", name, "--code", code);
+ return RequireId(doc, "shared-script create");
+ }
+
+ ///
+ /// Returns the ids of all external systems whose name starts with
+ /// , via external-system list. Used to delete an
+ /// external system a test created through the UI (where the new id is never surfaced).
+ ///
+ /// Name prefix to filter by (ordinal comparison).
+ public static async Task> ListExternalSystemIdsByNamePrefixAsync(string prefix)
+ {
+ using var doc = await RunJsonAsync("external-system", "list");
+ return IdsWhereNameStartsWith(doc, prefix);
+ }
+
+ ///
+ /// Returns the ids of all notification lists whose name starts with
+ /// , via notification list. Used to delete a
+ /// notification list a test created through the UI (where the new id is never surfaced).
+ ///
+ /// Name prefix to filter by (ordinal comparison).
+ public static async Task> ListNotificationListIdsByNamePrefixAsync(string prefix)
+ {
+ using var doc = await RunJsonAsync("notification", "list");
+ return IdsWhereNameStartsWith(doc, prefix);
+ }
+
+ /// Best-effort delete of an external system via external-system delete for teardown.
+ public static Task DeleteExternalSystemAsync(int id) => BestEffortAsync("external-system", "delete", id);
+
+ /// Best-effort delete of a notification list via notification delete for teardown.
+ public static Task DeleteNotificationListAsync(int id) => BestEffortAsync("notification", "delete", id);
+
+ /// Best-effort delete of a shared script via shared-script delete for teardown.
+ public static Task DeleteSharedScriptAsync(int id) => BestEffortAsync("shared-script", "delete", id);
+
///
/// Exports a Transport bundle scoped to a single template via
/// bundle export.
@@ -534,6 +613,31 @@ public static partial class CliRunner
}
}
+ ///
+ /// Returns the integer ids of every element in a JSON array document whose
+ /// name starts with (ordinal comparison),
+ /// tolerating both camelCase (id/name) and PascalCase
+ /// (Id/Name) keys. Shared by the external-system and
+ /// notification-list list helpers, whose list responses use PascalCase.
+ ///
+ private static IReadOnlyList IdsWhereNameStartsWith(JsonDocument doc, string prefix)
+ {
+ var ids = new List();
+ if (doc.RootElement.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var el in doc.RootElement.EnumerateArray())
+ {
+ if (!el.TryGetProperty("name", out var name) && !el.TryGetProperty("Name", out name)) continue;
+ if (name.ValueKind != JsonValueKind.String) continue;
+ if (!(name.GetString()?.StartsWith(prefix, StringComparison.Ordinal) ?? false)) continue;
+ if ((el.TryGetProperty("id", out var id) || el.TryGetProperty("Id", out id)) &&
+ id.TryGetInt32(out var n)) ids.Add(n);
+ }
+ }
+
+ return ids;
+ }
+
///
/// Extracts a required integer id from a create-command response,
/// throwing a descriptive error if it is missing or non-integral.
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 82659a91..75d458ef 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunnerHelpersTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Cluster/CliRunnerHelpersTests.cs
@@ -111,4 +111,58 @@ public class CliRunnerHelpersTests
await CliRunner.DeleteAreaAsync(areaId);
}
}
+
+ ///
+ /// A freshly created external system is discoverable by name prefix and is cleanly
+ /// deleted in teardown, exercising ,
+ /// , and
+ /// as a round-trip.
+ ///
+ [SkippableFact]
+ public async Task CreateThenDeleteExternalSystem_RoundTrips()
+ {
+ Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
+ var name = CliRunner.UniqueName("extsys");
+ var id = await CliRunner.CreateExternalSystemAsync(name);
+ try
+ {
+ Assert.True(id > 0);
+ Assert.Contains(id, await CliRunner.ListExternalSystemIdsByNamePrefixAsync(name));
+ }
+ finally { await CliRunner.DeleteExternalSystemAsync(id); }
+ }
+
+ ///
+ /// A freshly created notification list is discoverable by name prefix and is cleanly
+ /// deleted in teardown, exercising ,
+ /// , and
+ /// as a round-trip.
+ ///
+ [SkippableFact]
+ public async Task CreateThenDeleteNotificationList_RoundTrips()
+ {
+ Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
+ var name = CliRunner.UniqueName("notiflist");
+ var id = await CliRunner.CreateNotificationListAsync(name);
+ try
+ {
+ Assert.True(id > 0);
+ Assert.Contains(id, await CliRunner.ListNotificationListIdsByNamePrefixAsync(name));
+ }
+ finally { await CliRunner.DeleteNotificationListAsync(id); }
+ }
+
+ ///
+ /// A freshly created shared script returns a positive id and is cleanly deleted in
+ /// teardown, exercising and
+ /// as a round-trip.
+ ///
+ [SkippableFact]
+ public async Task CreateThenDeleteSharedScript_RoundTrips()
+ {
+ Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
+ var id = await CliRunner.CreateSharedScriptAsync(CliRunner.UniqueName("script"));
+ try { Assert.True(id > 0); }
+ finally { await CliRunner.DeleteSharedScriptAsync(id); }
+ }
}