d190345ef0
13 well-bounded test-coverage gaps closed across 11 test projects.
Net +35 regression tests; no production code changes except the
SiteEventLogger src reference unchanged (W3 redacted only test code).
Test additions:
- CLI-022: CommandTreeTests pinned-count assertion bumped 14→16 and
3 InlineData rows added for the audit + bundle command groups.
- Commons-020: new TransportRecordsTests covers BundleManifest /
ExportSelection / ImportPreview / ImportResolution / ImportResult —
ctor + System.Text.Json round-trip + record-equality (14 tests).
- CD-024: SPLIT-RANGE failure-continuation now under
EnsureLookahead_SecondSplitThrows_LoopAborts_FirstBoundaryStillCommitted
(Skippable MS-SQL fixture); production-shape rowversion delete
asserted by DeleteDeploymentRecord_CurrentRowVersion_StubAttachPath_DeleteSucceeds.
- CentralUI-033: new QueryStringDrillInTests with 4 bUnit cases for
Transport + SiteCalls drill-in / query-string handling.
- DM-024: probe actors (ReconcileProbeActor, SerializationProbeActor,
ArtifactProbeActor) refactored from static fields to per-test instances
(Interlocked on counter) — all 31 callers updated; no production
changes required.
- HM-022: real-time PeriodicTimer test flake fixed by replacing
fixed-budget Task.Delay with a RunLoopUntil poll-until-condition
helper (5s/25ms). Production loop untouched.
- InboundAPI-023: new EndpointExtensionsTests covers the
POST /api/{methodName} composition wiring via TestServer (7 cases:
happy path, missing key 401, unknown method 403, invalid JSON 400,
missing param 400, script-throws 500 sanitised, AuditActorItemKey
stash invariant).
- MgmtSvc-021: 6 new ManagementActorTests cover the Transport bundle
handlers (role gate for Export/Preview/Import, unknown-name
ManagementCommandException, blocker-rejection, dedupe last-write-wins).
- SCA-006: SiteCallQueryRequest_StuckOnly_CursorAtNonStuckBoundary_SkipsToNextStuckRow
pins the missing boundary case.
- SEL-023: stress-test `bool stop` promoted to `volatile bool` for
cross-thread visibility under release/JIT.
Verify-only resolutions:
- NS-024: closed by NS-019 (commit ac96b83 deletion of
NotificationDeliveryService + its test file). No edits needed.
- NotifOutbox-008: FallbackMaxRetries/FallbackRetryDelay are private
forward-compat constants returned only when no SMTP-config row exists
(in which case EmailNotificationDeliveryAdapter returns Permanent,
bypassing the values entirely). Marked Resolved with note.
- Transport-010: Overwrite child-collection sync covered by the T-001/
T-002 tests added in commit e3ca9af; per-IP throttle by
BundleUnlockRateLimiterTests; failed-session retention by
BundleSessionStoreTests; T-009 closed structurally via AsyncLocal.
Marked Resolved by reference.
Build clean; all 11 affected test suites green. README regenerated:
33 open (was 46).
153 lines
6.8 KiB
C#
153 lines
6.8 KiB
C#
using System.CommandLine;
|
|
using ScadaLink.CLI.Commands;
|
|
using ScadaLink.Commons.Messages.Management;
|
|
|
|
namespace ScadaLink.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/ScadaLink.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));
|
|
}
|
|
}
|