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 _configPathOption; private readonly Option _verboseOption; private readonly Option _quietOption; public SecretCommandsTests() { _configFileService = Substitute.For(); _autoDiscoveryService = Substitute.For(); _secureStoreManager = Substitute.For(); var services = new ServiceCollection(); services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_secureStoreManager); services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); _verboseOption = new Option(["--verbose", "-v"]); _quietOption = new Option(["--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()) .Returns(Task.FromResult(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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel { SecureStore = new SecureStoreSection { StorePath = "data/secrets.json", KeyFilePath = "data/secrets.key" } }; _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .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(); } }