using System.CommandLine; using JdeScoping.ConfigManager.Cli.Commands; using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Services; using Microsoft.Extensions.DependencyInjection; namespace JdeScoping.ConfigManager.Cli.Tests.Commands; [Collection("Console Tests")] public class ConnectionCommandsTests { private readonly IServiceProvider _serviceProvider; private readonly IConfigFileService _configFileService; private readonly IAutoDiscoveryService _autoDiscoveryService; private readonly Option _configPathOption; private readonly Option _verboseOption; private readonly Option _quietOption; public ConnectionCommandsTests() { _configFileService = Substitute.For(); _autoDiscoveryService = Substitute.For(); var services = new ServiceCollection(); services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); 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 = ConnectionCommands.CreateListCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.ShouldNotBeNull(); command.Name.ShouldBe("list"); } [Fact] public void CreateShowCommand_ReturnsCommandWithNameArgument() { // Act var command = ConnectionCommands.CreateShowCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.ShouldNotBeNull(); command.Name.ShouldBe("show"); command.Arguments.ShouldContain(a => a.Name == "name"); } [Fact] public void CreateAddCommand_ReturnsCommandWithOptions() { // Act var command = ConnectionCommands.CreateAddCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.ShouldNotBeNull(); command.Name.ShouldBe("add"); command.Arguments.ShouldContain(a => a.Name == "name"); command.Options.ShouldContain(o => o.Name == "provider"); command.Options.ShouldContain(o => o.Name == "server"); command.Options.ShouldContain(o => o.Name == "database"); command.Options.ShouldContain(o => o.Name == "user"); command.Options.ShouldContain(o => o.Name == "password"); command.Options.ShouldContain(o => o.Name == "port"); } [Fact] public void CreateRemoveCommand_ReturnsCommandWithNameArgument() { // Act var command = ConnectionCommands.CreateRemoveCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.ShouldNotBeNull(); command.Name.ShouldBe("remove"); command.Arguments.ShouldContain(a => a.Name == "name"); } [Fact] public async Task ListCommand_WithNoConfigFolder_ReturnsError() { // Arrange _autoDiscoveryService.FindConfigFolderAsync(Arg.Any()) .Returns(Task.FromResult(null)); var command = ConnectionCommands.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 void ListCommand_HasCorrectDescription() { // Arrange & Act var command = ConnectionCommands.CreateListCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.Description.ShouldBe("List all connection names and providers"); } [Fact] public async Task ShowCommand_WithValidConnection_ShowsDetailsWithMaskedPassword() { // 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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel { ConnectionStrings = new ConnectionStringsSection { Entries = [ new ConnectionStringEntry { Name = "TestConnection", Provider = ConnectionProvider.SqlServer, Server = "localhost", Database = "TestDb", UserId = "sa", Password = "SuperSecretPassword123!" } ] } }; _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConnectionCommands.CreateShowCommand( _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(["show", "TestConnection"]); // Assert var output = writer.ToString(); output.ShouldContain("TestConnection"); output.ShouldContain("SqlServer"); output.ShouldContain("localhost"); output.ShouldContain("TestDb"); output.ShouldContain("********"); // Password should be masked output.ShouldNotContain("SuperSecretPassword123!"); // Actual password should not appear } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public async Task ShowCommand_WithNonExistentConnection_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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel(); _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConnectionCommands.CreateShowCommand( _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(["show", "NonExistent"]); // Assert var output = writer.ToString(); output.ShouldContain("not found"); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public async Task AddCommand_WithNewConnection_AddsConnection() { // 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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel(); _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConnectionCommands.CreateAddCommand( _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(["add", "NewConnection", "--server", "localhost", "--database", "TestDb"]); // Assert var output = writer.ToString(); output.ShouldContain("added successfully"); await _configFileService.Received(1).SaveAppSettingsAsync(appSettingsPath, Arg.Any(), Arg.Any()); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public async Task AddCommand_WithExistingConnection_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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel { ConnectionStrings = new ConnectionStringsSection { Entries = [ new ConnectionStringEntry { Name = "ExistingConnection" } ] } }; _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConnectionCommands.CreateAddCommand( _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(["add", "ExistingConnection", "--server", "localhost"]); // Assert var output = writer.ToString(); output.ShouldContain("already exists"); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public void RemoveCommand_HasCorrectDescription() { // Arrange & Act var command = ConnectionCommands.CreateRemoveCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.Description.ShouldBe("Remove a connection"); } [Fact] public async Task RemoveCommand_WithNonExistentConnection_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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel(); _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConnectionCommands.CreateRemoveCommand( _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(["remove", "NonExistent"]); // Assert var output = writer.ToString(); output.ShouldContain("not found"); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } // ===== Connection Update Command Tests ===== [Fact] public void CreateUpdateCommand_ReturnsCommandWithOptions() { // Act var command = ConnectionCommands.CreateUpdateCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.ShouldNotBeNull(); command.Name.ShouldBe("update"); command.Arguments.ShouldContain(a => a.Name == "name"); command.Options.ShouldContain(o => o.Name == "provider"); command.Options.ShouldContain(o => o.Name == "server"); command.Options.ShouldContain(o => o.Name == "database"); command.Options.ShouldContain(o => o.Name == "user"); command.Options.ShouldContain(o => o.Name == "password"); command.Options.ShouldContain(o => o.Name == "port"); command.Options.ShouldContain(o => o.Name == "service-name"); command.Options.ShouldContain(o => o.Name == "raw"); } [Fact] public void UpdateCommand_HasCorrectDescription() { // Arrange & Act var command = ConnectionCommands.CreateUpdateCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.Description.ShouldBe("Update an existing connection"); } [Fact] public async Task UpdateCommand_WithNonExistentConnection_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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel(); _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConnectionCommands.CreateUpdateCommand( _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(["update", "NonExistent", "--server", "newhost"]); // Assert var output = writer.ToString(); output.ShouldContain("not found"); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public void UpdateCommand_AllOptionsExist() { // Arrange & Act var command = ConnectionCommands.CreateUpdateCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert - Verify all options exist command.Options.ShouldContain(o => o.Name == "provider"); command.Options.ShouldContain(o => o.Name == "server"); command.Options.ShouldContain(o => o.Name == "database"); command.Options.ShouldContain(o => o.Name == "user"); command.Options.ShouldContain(o => o.Name == "password"); command.Options.ShouldContain(o => o.Name == "port"); command.Options.ShouldContain(o => o.Name == "service-name"); command.Options.ShouldContain(o => o.Name == "raw"); } [Fact] public async Task UpdateCommand_WithValidChange_UpdatesAndSaves() { // 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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel { ConnectionStrings = new ConnectionStringsSection { Entries = [ new ConnectionStringEntry { Name = "TestConnection", Provider = ConnectionProvider.SqlServer, Server = "localhost", Database = "OldDb" } ] } }; _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConnectionCommands.CreateUpdateCommand( _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(["update", "TestConnection", "--database", "NewDb"]); // Assert var output = writer.ToString(); output.ShouldContain("updated successfully"); await _configFileService.Received(1).SaveAppSettingsAsync(appSettingsPath, Arg.Any(), Arg.Any()); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public async Task UpdateCommand_WithMultipleChanges_UpdatesAllProperties() { // 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()) .Returns(Task.FromResult(tempDir)); var config = new ConfigModel { ConnectionStrings = new ConnectionStringsSection { Entries = [ new ConnectionStringEntry { Name = "TestConnection", Provider = ConnectionProvider.SqlServer, Server = "localhost", Database = "OldDb", UserId = "olduser" } ] } }; _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConnectionCommands.CreateUpdateCommand( _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(["update", "TestConnection", "--server", "newhost", "--database", "NewDb", "--user", "newuser"]); // Assert var output = writer.ToString(); output.ShouldContain("updated successfully"); // Verify the config was updated config.ConnectionStrings.Entries[0].Server.ShouldBe("newhost"); config.ConnectionStrings.Entries[0].Database.ShouldBe("NewDb"); config.ConnectionStrings.Entries[0].UserId.ShouldBe("newuser"); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } }