test(e2e): add CliRunner helpers for data-connection, api-method, api-key teardown, instance read-back

This commit is contained in:
Joseph Doherty
2026-06-06 11:37:57 -04:00
parent c23e2bf227
commit a8a515ec8a
2 changed files with 129 additions and 0 deletions
@@ -224,6 +224,94 @@ public static partial class CliRunner
/// <param name="id">Site id.</param>
public static Task DeleteSiteAsync(int id) => BestEffortAsync("site", "delete", id);
/// <summary>
/// Creates a data connection on a site via <c>data-connection create</c> and returns its new <c>id</c>.
/// </summary>
public static async Task<int> CreateDataConnectionAsync(int siteId, string name, string protocol = "OpcUa", string? primaryConfig = null)
{
var inv = System.Globalization.CultureInfo.InvariantCulture;
var args = new List<string>
{
"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");
}
/// <summary>Best-effort delete of a data connection via <c>data-connection delete</c> for teardown.</summary>
public static Task DeleteDataConnectionAsync(int id) => BestEffortAsync("data-connection", "delete", id);
/// <summary>
/// Creates an inbound API method via <c>api-method create</c> (so it appears as a checkbox in the
/// API-key form) and returns its new <c>id</c>.
/// </summary>
public static async Task<int> 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");
}
/// <summary>Best-effort delete of an API method via <c>api-method delete</c> for teardown.</summary>
public static Task DeleteApiMethodAsync(int id) => BestEffortAsync("api-method", "delete", id);
/// <summary>
/// Resolves an API key's opaque string <c>keyId</c> from its display name via
/// <c>security api-key list</c>; returns <see langword="null"/> if no key matches.
/// </summary>
public static async Task<string?> 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;
}
/// <summary>
/// Best-effort delete of an API key via <c>security api-key delete --key-id</c> for teardown.
/// The key id is an opaque string, so this cannot use the int-based <see cref="BestEffortAsync"/>.
/// </summary>
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.
}
}
/// <summary>
/// Reads an instance's full configuration via <c>instance get</c>; the returned document exposes
/// <c>connectionBindings</c>, <c>attributeOverrides</c>, and <c>areaId</c> for persistence read-back.
/// Caller owns the returned <see cref="JsonDocument"/>.
/// </summary>
public static Task<JsonDocument> GetInstanceAsync(int id) =>
RunJsonAsync("instance", "get", "--id", id.ToString(System.Globalization.CultureInfo.InvariantCulture));
/// <summary>
/// Returns the ids of all templates whose <c>name</c> starts with
/// <paramref name="prefix"/>, via <c>template list</c>.
@@ -43,4 +43,45 @@ public class CliRunnerHelpersTests
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
Assert.True(await CliRunner.ResolveSiteIdAsync("site-a") > 0);
}
/// <summary>
/// A freshly created data connection returns a positive id and is cleanly
/// deleted in teardown, exercising <see cref="CliRunner.CreateDataConnectionAsync"/>
/// and <see cref="CliRunner.DeleteDataConnectionAsync"/> as a round-trip.
/// </summary>
[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);
}
}
/// <summary>
/// A freshly created API method returns a positive id and is cleanly deleted in
/// teardown, exercising <see cref="CliRunner.CreateApiMethodAsync"/> and
/// <see cref="CliRunner.DeleteApiMethodAsync"/> as a round-trip.
/// </summary>
[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);
}
}
}