Files
Joseph Doherty 6f3e12b3b4 refactor(configmanager): convert CLI to structured logging with Serilog
Replace Console.WriteLine calls with ILogger usage across all CLI commands.
Serilog is configured via DI with clean message-only output suitable for
CLI tooling. Log levels map to --quiet (Warning), default (Information),
and --verbose (Debug) flags.

- Add Serilog packages and configure in Program.cs
- Convert all 7 command files to use ILoggerFactory from DI
- Add BeginScope with context properties (Command, ConfigPath, etc.)
- Create logging_style.md documenting patterns and best practices
- Update tests with TestLoggingHelper for Serilog test configuration
2026-01-28 15:53:08 -05:00

301 lines
11 KiB
C#

using System.CommandLine;
using JdeScoping.ConfigManager.Cli.Commands;
using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class BackupCommandsTests
{
private readonly IServiceProvider _serviceProvider;
private readonly IBackupService _backupService;
private readonly IAutoDiscoveryService _autoDiscoveryService;
private readonly Option<string?> _configPathOption;
private readonly Option<bool> _verboseOption;
private readonly Option<bool> _quietOption;
public BackupCommandsTests()
{
_backupService = Substitute.For<IBackupService>();
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
var services = new ServiceCollection();
services.AddSingleton(_backupService);
services.AddSingleton(_autoDiscoveryService);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]);
_verboseOption = new Option<bool>(["--verbose", "-v"]);
_quietOption = new Option<bool>(["--quiet", "-q"]);
}
[Fact]
public void CreateCreateCommand_ReturnsCommand()
{
// Act
var command = BackupCommands.CreateCreateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("create");
}
[Fact]
public void CreateListCommand_ReturnsCommand()
{
// Act
var command = BackupCommands.CreateListCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("list");
}
[Fact]
public void CreateRestoreCommand_ReturnsCommandWithTimestampArgument()
{
// Act
var command = BackupCommands.CreateRestoreCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("restore");
command.Arguments.ShouldContain(a => a.Name == "timestamp");
}
[Fact]
public void CreateCleanupCommand_ReturnsCommandWithKeepOption()
{
// Act
var command = BackupCommands.CreateCleanupCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("cleanup");
command.Options.ShouldContain(o => o.Name == "keep");
}
[Fact]
public async Task CreateCommand_WithNoConfigFolder_ReturnsError()
{
// Arrange
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(null));
var command = BackupCommands.CreateCreateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
var exitCode = await rootCommand.InvokeAsync(["create"]);
// Assert - command completes without throwing
command.ShouldNotBeNull();
}
[Fact]
public async Task CreateCommand_WithValidConfig_CreatesBackup()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
var backupPath = Path.Combine(tempDir, "appsettings.2026-01-15_120000.bak");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
_backupService.CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backupPath));
var command = BackupCommands.CreateCreateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["create"]);
// Assert
var output = writer.ToString();
output.ShouldContain("Backup created successfully");
await _backupService.Received(1).CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>());
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public void ListCommand_HasCorrectDescription()
{
// Arrange & Act
var command = BackupCommands.CreateListCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.Description.ShouldBe("List available backups");
}
[Fact]
public async Task ListCommand_WithNoBackups_ShowsEmptyMessage()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
_backupService.GetBackupsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<BackupInfo>>(new List<BackupInfo>()));
var command = BackupCommands.CreateListCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["list"]);
// Assert
var output = writer.ToString();
output.ShouldContain("No backups found");
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task CleanupCommand_CallsCleanupService()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
_backupService.GetBackupsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<BackupInfo>>(new List<BackupInfo>()));
var command = BackupCommands.CreateCleanupCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
await rootCommand.InvokeAsync(["cleanup", "--keep", "5"]);
// Assert
await _backupService.Received(1).CleanupOldBackupsAsync(appSettingsPath, 5, Arg.Any<CancellationToken>());
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task RestoreCommand_WithNonExistentBackup_ReturnsError()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
_backupService.GetBackupsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<BackupInfo>>(new List<BackupInfo>()));
var command = BackupCommands.CreateRestoreCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output (Serilog writes all output to stdout)
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["restore", "2026-01-99_999999"]);
// Assert
var output = writer.ToString();
output.ShouldContain("not found");
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
}