6f3e12b3b4
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
213 lines
7.6 KiB
C#
213 lines
7.6 KiB
C#
using System.CommandLine;
|
|
using JdeScoping.ConfigManager.Cli.Commands;
|
|
using JdeScoping.ConfigManager.Core.Models;
|
|
using JdeScoping.ConfigManager.Core.Services;
|
|
using JdeScoping.ConfigManager.Core.Services.SecureStore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
|
|
|
|
[Collection("Console Tests")]
|
|
public class SecretCommandsTests
|
|
{
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly IConfigFileService _configFileService;
|
|
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
|
private readonly ISecureStoreManager _secureStoreManager;
|
|
private readonly Option<string?> _configPathOption;
|
|
private readonly Option<bool> _verboseOption;
|
|
private readonly Option<bool> _quietOption;
|
|
|
|
public SecretCommandsTests()
|
|
{
|
|
_configFileService = Substitute.For<IConfigFileService>();
|
|
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
|
_secureStoreManager = Substitute.For<ISecureStoreManager>();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(_configFileService);
|
|
services.AddSingleton(_autoDiscoveryService);
|
|
services.AddSingleton(_secureStoreManager);
|
|
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 CreateListCommand_ReturnsCommand()
|
|
{
|
|
// Act
|
|
var command = SecretCommands.CreateListCommand(
|
|
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
|
|
|
// Assert
|
|
command.ShouldNotBeNull();
|
|
command.Name.ShouldBe("list");
|
|
command.Description.ShouldBe("List all secret keys");
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateGetCommand_ReturnsCommandWithKeyArgument()
|
|
{
|
|
// Act
|
|
var command = SecretCommands.CreateGetCommand(
|
|
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
|
|
|
// Assert
|
|
command.ShouldNotBeNull();
|
|
command.Name.ShouldBe("get");
|
|
command.Description.ShouldBe("Get a secret value");
|
|
command.Arguments.ShouldContain(a => a.Name == "key");
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateSetCommand_ReturnsCommandWithKeyAndValueArguments()
|
|
{
|
|
// Act
|
|
var command = SecretCommands.CreateSetCommand(
|
|
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
|
|
|
// Assert
|
|
command.ShouldNotBeNull();
|
|
command.Name.ShouldBe("set");
|
|
command.Description.ShouldBe("Set or update a secret");
|
|
command.Arguments.ShouldContain(a => a.Name == "key");
|
|
command.Arguments.ShouldContain(a => a.Name == "value");
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateRemoveCommand_ReturnsCommandWithKeyArgument()
|
|
{
|
|
// Act
|
|
var command = SecretCommands.CreateRemoveCommand(
|
|
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
|
|
|
// Assert
|
|
command.ShouldNotBeNull();
|
|
command.Name.ShouldBe("remove");
|
|
command.Description.ShouldBe("Remove a secret");
|
|
command.Arguments.ShouldContain(a => a.Name == "key");
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateInitCommand_ReturnsCommandWithOptions()
|
|
{
|
|
// Act
|
|
var command = SecretCommands.CreateInitCommand(
|
|
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
|
|
|
// Assert
|
|
command.ShouldNotBeNull();
|
|
command.Name.ShouldBe("init");
|
|
command.Description.ShouldBe("Initialize a new SecureStore");
|
|
command.Options.ShouldContain(o => o.Name == "store");
|
|
command.Options.ShouldContain(o => o.Name == "key");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListCommand_WithNoConfigFolder_ReturnsError()
|
|
{
|
|
// Arrange
|
|
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult<string?>(null));
|
|
|
|
var command = SecretCommands.CreateListCommand(
|
|
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
|
var rootCommand = new RootCommand { command };
|
|
rootCommand.AddGlobalOption(_configPathOption);
|
|
rootCommand.AddGlobalOption(_verboseOption);
|
|
rootCommand.AddGlobalOption(_quietOption);
|
|
|
|
// Act
|
|
var exitCode = await rootCommand.InvokeAsync(["list"]);
|
|
|
|
// Assert - command completes without throwing
|
|
command.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCommand_WithMissingKey_ReturnsError()
|
|
{
|
|
// Arrange
|
|
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
|
Directory.CreateDirectory(tempDir);
|
|
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
|
|
File.WriteAllText(appSettingsPath, "{}");
|
|
|
|
var storePath = Path.Combine(tempDir, "data", "secrets.json");
|
|
var keyPath = Path.Combine(tempDir, "data", "secrets.key");
|
|
Directory.CreateDirectory(Path.Combine(tempDir, "data"));
|
|
File.WriteAllText(storePath, "{}");
|
|
File.WriteAllText(keyPath, "test-key");
|
|
|
|
try
|
|
{
|
|
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult<string?>(tempDir));
|
|
|
|
var config = new ConfigModel
|
|
{
|
|
SecureStore = new SecureStoreSection
|
|
{
|
|
StorePath = "data/secrets.json",
|
|
KeyFilePath = "data/secrets.key"
|
|
}
|
|
};
|
|
|
|
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult(config));
|
|
|
|
_secureStoreManager.When(m => m.GetSecret("nonexistent"))
|
|
.Throw(new KeyNotFoundException("Secret 'nonexistent' not found"));
|
|
|
|
var command = SecretCommands.CreateGetCommand(
|
|
_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(["get", "nonexistent"]);
|
|
|
|
// Assert
|
|
var output = writer.ToString();
|
|
output.ShouldContain("not found");
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(tempDir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void InitCommand_HasCorrectOptions()
|
|
{
|
|
// Arrange & Act
|
|
var command = SecretCommands.CreateInitCommand(
|
|
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
|
|
|
// Assert
|
|
var storeOption = command.Options.FirstOrDefault(o => o.Name == "store");
|
|
var keyOption = command.Options.FirstOrDefault(o => o.Name == "key");
|
|
|
|
storeOption.ShouldNotBeNull();
|
|
keyOption.ShouldNotBeNull();
|
|
}
|
|
}
|