From c7ab17cda53bbf23e47142f0f838e24312192ec4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 14:34:12 -0400 Subject: [PATCH] test(playwright): add external-system/notification-list/shared-script CLI helpers (Wave 3 foundation) --- .../Cluster/CliRunner.Helpers.cs | 104 ++++++++++++++++++ .../Cluster/CliRunnerHelpersTests.cs | 54 +++++++++ 2 files changed, 158 insertions(+) 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); } + } }