From ba8ddcc032c204fbdffe22ff4b31264b47c3e4f1 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 20 May 2026 22:02:19 -0400 Subject: [PATCH] refactor(cli): rename audit-log to audit-config with deprecation alias (#23 M8) --- .../Commands/AuditLogCommands.cs | 41 ++++++- src/ScadaLink.CLI/Program.cs | 5 + .../Commands/AuditConfigDeprecationTests.cs | 100 ++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 tests/ScadaLink.CLI.Tests/Commands/AuditConfigDeprecationTests.cs diff --git a/src/ScadaLink.CLI/Commands/AuditLogCommands.cs b/src/ScadaLink.CLI/Commands/AuditLogCommands.cs index 00fc084..eee6143 100644 --- a/src/ScadaLink.CLI/Commands/AuditLogCommands.cs +++ b/src/ScadaLink.CLI/Commands/AuditLogCommands.cs @@ -4,11 +4,50 @@ using ScadaLink.Commons.Messages.Management; namespace ScadaLink.CLI.Commands; +/// +/// The scadalink audit-config command group: views the configuration-change +/// audit log (the IAuditService trail of admin edits — distinct from the +/// centralized append-only Audit Log served by ). +/// +/// +/// Renamed from audit-log in #23 M8-T7 to avoid confusion with the new +/// scadalink audit group. The old audit-log name is retained as a +/// deprecated alias; still resolves the full subcommand +/// tree, and Program.cs prints a deprecation warning when it is used. +/// public static class AuditLogCommands { + /// The deprecated alias kept for backward compatibility with the old command name. + public const string DeprecatedAlias = "audit-log"; + + /// The deprecation warning emitted when the old audit-log name is used. + public const string DeprecationWarning = + "Warning: 'audit-log' is deprecated and will be removed in a future release. " + + "Use 'audit-config' instead."; + + /// + /// Writes the to when the + /// CLI was invoked via the deprecated audit-log command name (i.e. the first + /// argument is ). The command itself still works — it is + /// an alias of audit-config — so this only adds the migration warning. + /// Factored out of Program.cs so it is unit-testable without spawning a process. + /// + public static void WriteDeprecationWarningIfNeeded(string[] args, TextWriter stderr) + { + if (args.Length > 0 + && string.Equals(args[0], DeprecatedAlias, StringComparison.Ordinal)) + { + stderr.WriteLine(DeprecationWarning); + } + } + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { - var command = new Command("audit-log") { Description = "Query audit logs" }; + var command = new Command("audit-config") { Description = "Query the configuration-change audit log" }; + // Backward-compatible alias for the pre-M8 `audit-log` name. The alias keeps + // full subcommand parity automatically; the deprecation warning is emitted by + // the args[0] check in Program.cs. + command.Aliases.Add(DeprecatedAlias); command.Add(BuildQuery(urlOption, formatOption, usernameOption, passwordOption)); diff --git a/src/ScadaLink.CLI/Program.cs b/src/ScadaLink.CLI/Program.cs index a79f678..240b410 100644 --- a/src/ScadaLink.CLI/Program.cs +++ b/src/ScadaLink.CLI/Program.cs @@ -39,5 +39,10 @@ rootCommand.SetAction(_ => Console.WriteLine("Use --help to see available commands."); }); +// Deprecation notice for the pre-M8 `audit-log` command name. The command itself +// still works (it is an alias of `audit-config`), but using the old name emits a +// warning to stderr so scripts can be migrated. +AuditLogCommands.WriteDeprecationWarningIfNeeded(args, Console.Error); + var parseResult = CommandLineParser.Parse(rootCommand, args); return await parseResult.InvokeAsync(); diff --git a/tests/ScadaLink.CLI.Tests/Commands/AuditConfigDeprecationTests.cs b/tests/ScadaLink.CLI.Tests/Commands/AuditConfigDeprecationTests.cs new file mode 100644 index 0000000..3cc4d20 --- /dev/null +++ b/tests/ScadaLink.CLI.Tests/Commands/AuditConfigDeprecationTests.cs @@ -0,0 +1,100 @@ +using System.CommandLine; +using ScadaLink.CLI.Commands; + +namespace ScadaLink.CLI.Tests.Commands; + +/// +/// Tests for the audit-logaudit-config rename (Audit Log #23 M8-T7): +/// the new name parses, the deprecated audit-log alias still resolves the full +/// subcommand tree and emits a stderr deprecation warning, and the renamed group does +/// not collide with the distinct audit group from Bundle A. +/// +public class AuditConfigDeprecationTests +{ + private static readonly Option Url = new("--url") { Recursive = true }; + private static readonly Option Username = new("--username") { Recursive = true }; + private static readonly Option Password = new("--password") { Recursive = true }; + private static readonly Option Format = CliOptions.CreateFormatOption(); + + private static RootCommand BuildRoot() + { + var root = new RootCommand(); + root.Add(Url); + root.Add(Username); + root.Add(Password); + root.Add(Format); + root.Add(AuditCommands.Build(Url, Format, Username, Password)); + root.Add(AuditLogCommands.Build(Url, Format, Username, Password)); + return root; + } + + [Fact] + public void AuditConfig_Query_Works() + { + // The new `audit-config query` name parses cleanly with no errors. + var root = BuildRoot(); + var parse = root.Parse(new[] { "audit-config", "query", "--user", "alice" }); + Assert.Empty(parse.Errors); + } + + [Fact] + public void AuditLog_Query_StillWorks_ButEmitsDeprecationWarning_ToStderr() + { + // The deprecated `audit-log` alias still resolves the full subcommand tree... + var root = BuildRoot(); + var parse = root.Parse(new[] { "audit-log", "query", "--user", "alice" }); + Assert.Empty(parse.Errors); + + // ...and invoking via the old name emits the deprecation warning to stderr. + var stderr = new StringWriter(); + AuditLogCommands.WriteDeprecationWarningIfNeeded( + new[] { "audit-log", "query" }, stderr); + var warning = stderr.ToString(); + Assert.Contains("deprecated", warning); + Assert.Contains("audit-config", warning); + } + + [Fact] + public void DeprecationWarning_NotEmitted_ForNewName() + { + // The new `audit-config` name must not trigger the deprecation warning. + var stderr = new StringWriter(); + AuditLogCommands.WriteDeprecationWarningIfNeeded( + new[] { "audit-config", "query" }, stderr); + Assert.Equal("", stderr.ToString()); + } + + [Fact] + public void DeprecationWarning_NotEmitted_ForUnrelatedCommand() + { + var stderr = new StringWriter(); + AuditLogCommands.WriteDeprecationWarningIfNeeded( + new[] { "audit", "query" }, stderr); + Assert.Equal("", stderr.ToString()); + } + + [Fact] + public void Audit_And_AuditConfig_AreDistinctCommands_NoConflict() + { + var root = BuildRoot(); + + var auditNames = new[] { "audit", "audit-config" }; + foreach (var name in auditNames) + { + var match = root.Subcommands.SingleOrDefault(c => c.Name == name); + Assert.NotNull(match); + } + + // The two groups are distinct objects with distinct subcommand sets: + // `audit` has query/export/verify-chain; `audit-config` has only query. + var audit = root.Subcommands.Single(c => c.Name == "audit"); + var auditConfig = root.Subcommands.Single(c => c.Name == "audit-config"); + Assert.NotSame(audit, auditConfig); + Assert.Contains(audit.Subcommands, c => c.Name == "verify-chain"); + Assert.DoesNotContain(auditConfig.Subcommands, c => c.Name == "verify-chain"); + + // `audit-config` carries the deprecated `audit-log` alias; `audit` does not. + Assert.Contains(AuditLogCommands.DeprecatedAlias, auditConfig.Aliases); + Assert.DoesNotContain(AuditLogCommands.DeprecatedAlias, audit.Aliases); + } +}