test(playwright): add external-system/notification-list/shared-script CLI helpers (Wave 3 foundation)

This commit is contained in:
Joseph Doherty
2026-06-06 14:34:12 -04:00
parent e5bd8d9707
commit c7ab17cda5
2 changed files with 158 additions and 0 deletions
@@ -435,6 +435,85 @@ public static partial class CliRunner
return ids;
}
/// <summary>
/// Creates an external system via <c>external-system create</c> and returns its new <c>id</c>.
/// </summary>
/// <param name="name">External system name (typically from <see cref="UniqueName"/>).</param>
/// <param name="endpointUrl">Endpoint base URL (defaults to an unreachable placeholder).</param>
/// <param name="authType">Auth type token; one of <c>ApiKey</c> or <c>BasicAuth</c>.</param>
/// <exception cref="InvalidOperationException">
/// The CLI failed, or the response did not carry an integer <c>id</c>.
/// </exception>
public static async Task<int> 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");
}
/// <summary>
/// Creates a notification list via <c>notification create</c> and returns its new <c>id</c>.
/// </summary>
/// <param name="name">Notification list name (typically from <see cref="UniqueName"/>).</param>
/// <param name="emails">Comma-separated recipient emails.</param>
/// <exception cref="InvalidOperationException">
/// The CLI failed, or the response did not carry an integer <c>id</c>.
/// </exception>
public static async Task<int> CreateNotificationListAsync(string name, string emails = "noreply@example.invalid")
{
using var doc = await RunJsonAsync("notification", "create", "--name", name, "--emails", emails);
return RequireId(doc, "notification create");
}
/// <summary>
/// Creates a shared script via <c>shared-script create</c> and returns its new <c>id</c>.
/// </summary>
/// <param name="name">Shared script name (typically from <see cref="UniqueName"/>).</param>
/// <param name="code">Script body.</param>
/// <exception cref="InvalidOperationException">
/// The CLI failed, or the response did not carry an integer <c>id</c>.
/// </exception>
public static async Task<int> 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");
}
/// <summary>
/// Returns the ids of all external systems whose <c>name</c> starts with
/// <paramref name="prefix"/>, via <c>external-system list</c>. Used to delete an
/// external system a test created through the UI (where the new id is never surfaced).
/// </summary>
/// <param name="prefix">Name prefix to filter by (ordinal comparison).</param>
public static async Task<IReadOnlyList<int>> ListExternalSystemIdsByNamePrefixAsync(string prefix)
{
using var doc = await RunJsonAsync("external-system", "list");
return IdsWhereNameStartsWith(doc, prefix);
}
/// <summary>
/// Returns the ids of all notification lists whose <c>name</c> starts with
/// <paramref name="prefix"/>, via <c>notification list</c>. Used to delete a
/// notification list a test created through the UI (where the new id is never surfaced).
/// </summary>
/// <param name="prefix">Name prefix to filter by (ordinal comparison).</param>
public static async Task<IReadOnlyList<int>> ListNotificationListIdsByNamePrefixAsync(string prefix)
{
using var doc = await RunJsonAsync("notification", "list");
return IdsWhereNameStartsWith(doc, prefix);
}
/// <summary>Best-effort delete of an external system via <c>external-system delete</c> for teardown.</summary>
public static Task DeleteExternalSystemAsync(int id) => BestEffortAsync("external-system", "delete", id);
/// <summary>Best-effort delete of a notification list via <c>notification delete</c> for teardown.</summary>
public static Task DeleteNotificationListAsync(int id) => BestEffortAsync("notification", "delete", id);
/// <summary>Best-effort delete of a shared script via <c>shared-script delete</c> for teardown.</summary>
public static Task DeleteSharedScriptAsync(int id) => BestEffortAsync("shared-script", "delete", id);
/// <summary>
/// Exports a Transport bundle scoped to a single template via
/// <c>bundle export</c>.
@@ -534,6 +613,31 @@ public static partial class CliRunner
}
}
/// <summary>
/// Returns the integer ids of every element in a JSON array document whose
/// <c>name</c> starts with <paramref name="prefix"/> (ordinal comparison),
/// tolerating both camelCase (<c>id</c>/<c>name</c>) and PascalCase
/// (<c>Id</c>/<c>Name</c>) keys. Shared by the external-system and
/// notification-list list helpers, whose <c>list</c> responses use PascalCase.
/// </summary>
private static IReadOnlyList<int> IdsWhereNameStartsWith(JsonDocument doc, string prefix)
{
var ids = new List<int>();
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;
}
/// <summary>
/// Extracts a required integer <c>id</c> from a create-command response,
/// throwing a descriptive error if it is missing or non-integral.
@@ -111,4 +111,58 @@ public class CliRunnerHelpersTests
await CliRunner.DeleteAreaAsync(areaId);
}
}
/// <summary>
/// A freshly created external system is discoverable by name prefix and is cleanly
/// deleted in teardown, exercising <see cref="CliRunner.CreateExternalSystemAsync"/>,
/// <see cref="CliRunner.ListExternalSystemIdsByNamePrefixAsync"/>, and
/// <see cref="CliRunner.DeleteExternalSystemAsync"/> as a round-trip.
/// </summary>
[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); }
}
/// <summary>
/// A freshly created notification list is discoverable by name prefix and is cleanly
/// deleted in teardown, exercising <see cref="CliRunner.CreateNotificationListAsync"/>,
/// <see cref="CliRunner.ListNotificationListIdsByNamePrefixAsync"/>, and
/// <see cref="CliRunner.DeleteNotificationListAsync"/> as a round-trip.
/// </summary>
[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); }
}
/// <summary>
/// A freshly created shared script returns a positive id and is cleanly deleted in
/// teardown, exercising <see cref="CliRunner.CreateSharedScriptAsync"/> and
/// <see cref="CliRunner.DeleteSharedScriptAsync"/> as a round-trip.
/// </summary>
[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); }
}
}