7b0b9c7365
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
153 lines
6.8 KiB
C#
153 lines
6.8 KiB
C#
using System.CommandLine;
|
|
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests;
|
|
|
|
/// <summary>
|
|
/// Regression tests for CLI-013 — the command-tree wiring was untested. These tests
|
|
/// build every command group and assert the tree is well-formed (every leaf has an
|
|
/// action, no group is empty), and that every management command record the CLI sends
|
|
/// resolves via <see cref="ManagementCommandRegistry"/> (so command-name derivation
|
|
/// never throws at runtime).
|
|
/// </summary>
|
|
public class CommandTreeTests
|
|
{
|
|
private static readonly Option<string> Url = new("--url") { Recursive = true };
|
|
private static readonly Option<string> Username = new("--username") { Recursive = true };
|
|
private static readonly Option<string> Password = new("--password") { Recursive = true };
|
|
private static readonly Option<string> Format = CliOptions.CreateFormatOption();
|
|
|
|
// NOTE: this list MUST stay in sync with the rootCommand.Add(...) calls in
|
|
// src/ZB.MOM.WW.ScadaBridge.CLI/Program.cs. When a new command group is added (or one is
|
|
// removed/renamed), update this array and bump the count assertion in
|
|
// AllCommandGroups_Build_WithoutThrowing accordingly.
|
|
private static IEnumerable<Command> AllCommandGroups() => new[]
|
|
{
|
|
TemplateCommands.Build(Url, Format, Username, Password),
|
|
InstanceCommands.Build(Url, Format, Username, Password),
|
|
SiteCommands.Build(Url, Format, Username, Password),
|
|
DeployCommands.Build(Url, Format, Username, Password),
|
|
DataConnectionCommands.Build(Url, Format, Username, Password),
|
|
ExternalSystemCommands.Build(Url, Format, Username, Password),
|
|
NotificationCommands.Build(Url, Format, Username, Password),
|
|
SecurityCommands.Build(Url, Format, Username, Password),
|
|
AuditLogCommands.Build(Url, Format, Username, Password),
|
|
AuditCommands.Build(Url, Format, Username, Password),
|
|
HealthCommands.Build(Url, Format, Username, Password),
|
|
DebugCommands.Build(Url, Format, Username, Password),
|
|
SharedScriptCommands.Build(Url, Format, Username, Password),
|
|
DbConnectionCommands.Build(Url, Format, Username, Password),
|
|
ApiMethodCommands.Build(Url, Format, Username, Password),
|
|
BundleCommands.Build(Url, Format, Username, Password),
|
|
};
|
|
|
|
private static IEnumerable<Command> LeafCommands(Command command)
|
|
{
|
|
if (command.Subcommands.Count == 0)
|
|
{
|
|
yield return command;
|
|
yield break;
|
|
}
|
|
|
|
foreach (var sub in command.Subcommands)
|
|
foreach (var leaf in LeafCommands(sub))
|
|
yield return leaf;
|
|
}
|
|
|
|
[Fact]
|
|
public void AllCommandGroups_Build_WithoutThrowing()
|
|
{
|
|
var groups = AllCommandGroups().ToList();
|
|
// CLI-022: bump this count whenever a new top-level command group is
|
|
// registered in Program.cs. Current registered groups (16):
|
|
// template, instance, site, deploy, data-connection, external-system,
|
|
// notification, security, audit-config, audit, health, debug,
|
|
// shared-script, db-connection, api-method, bundle.
|
|
Assert.Equal(16, groups.Count);
|
|
Assert.All(groups, g => Assert.False(string.IsNullOrWhiteSpace(g.Name)));
|
|
}
|
|
|
|
[Fact]
|
|
public void AllCommandGroups_Contains_AuditAndBundle()
|
|
{
|
|
// CLI-022: explicit group-presence assertion so the harness does not
|
|
// silently drift back to excluding new groups. Use names because that
|
|
// is what users actually type at the prompt.
|
|
var groupNames = AllCommandGroups().Select(g => g.Name).ToHashSet();
|
|
Assert.Contains("audit", groupNames);
|
|
Assert.Contains("bundle", groupNames);
|
|
}
|
|
|
|
[Fact]
|
|
public void AuditCommandGroup_HasQueryExportAndVerifyChain()
|
|
{
|
|
// CLI-022: pin the audit sub-command surface so a rename / accidental
|
|
// removal of one of these is caught.
|
|
var audit = AuditCommands.Build(Url, Format, Username, Password);
|
|
var subNames = audit.Subcommands.Select(c => c.Name).ToHashSet();
|
|
Assert.Contains("query", subNames);
|
|
Assert.Contains("export", subNames);
|
|
Assert.Contains("verify-chain", subNames);
|
|
}
|
|
|
|
[Fact]
|
|
public void BundleCommandGroup_HasExportPreviewAndImport()
|
|
{
|
|
// CLI-022: pin the bundle sub-command surface.
|
|
var bundle = BundleCommands.Build(Url, Format, Username, Password);
|
|
var subNames = bundle.Subcommands.Select(c => c.Name).ToHashSet();
|
|
Assert.Contains("export", subNames);
|
|
Assert.Contains("preview", subNames);
|
|
Assert.Contains("import", subNames);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryLeafCommand_HasAnAction()
|
|
{
|
|
// A leaf command with no action is dead wiring — invoking it would do nothing.
|
|
var leaves = AllCommandGroups().SelectMany(LeafCommands).ToList();
|
|
|
|
Assert.NotEmpty(leaves);
|
|
Assert.All(leaves, leaf =>
|
|
Assert.True(leaf.Action != null, $"Leaf command '{leaf.Name}' has no action."));
|
|
}
|
|
|
|
[Fact]
|
|
public void TemplateCompositionDelete_IsKeyedByIdOnly()
|
|
{
|
|
// CLI-015: the in-repo README documented `template composition delete` with
|
|
// --template-id / --instance-name, but the implementation keys deletion by the
|
|
// composition's own integer ID via a single --id option. Pin the real surface.
|
|
var template = TemplateCommands.Build(Url, Format, Username, Password);
|
|
var composition = template.Subcommands.Single(c => c.Name == "composition");
|
|
var delete = composition.Subcommands.Single(c => c.Name == "delete");
|
|
|
|
var optionNames = delete.Options.Select(o => o.Name).ToList();
|
|
Assert.Contains("--id", optionNames);
|
|
Assert.DoesNotContain("--template-id", optionNames);
|
|
Assert.DoesNotContain("--instance-name", optionNames);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(typeof(GetInstanceCommand))]
|
|
[InlineData(typeof(ListSitesCommand))]
|
|
[InlineData(typeof(CreateTemplateCommand))]
|
|
[InlineData(typeof(SetConnectionBindingsCommand))]
|
|
[InlineData(typeof(SetInstanceOverridesCommand))]
|
|
[InlineData(typeof(DebugSnapshotCommand))]
|
|
[InlineData(typeof(MgmtDeployInstanceCommand))]
|
|
[InlineData(typeof(QueryAuditLogCommand))]
|
|
[InlineData(typeof(ExportBundleCommand))]
|
|
[InlineData(typeof(PreviewBundleCommand))]
|
|
[InlineData(typeof(ImportBundleCommand))]
|
|
public void CommandPayloadTypes_ResolveViaRegistry(Type commandType)
|
|
{
|
|
// GetCommandName throws ArgumentException for an unregistered type — the CLI
|
|
// calls it for every command it sends, so each must round-trip.
|
|
var name = ManagementCommandRegistry.GetCommandName(commandType);
|
|
Assert.False(string.IsNullOrWhiteSpace(name));
|
|
Assert.Equal(commandType, ManagementCommandRegistry.Resolve(name));
|
|
}
|
|
}
|