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 ConfigCommandsTests { private readonly IServiceProvider _serviceProvider; private readonly IConfigFileService _configFileService; private readonly IAutoDiscoveryService _autoDiscoveryService; private readonly IBackupService _backupService; private readonly Option _configPathOption; private readonly Option _verboseOption; private readonly Option _quietOption; public ConfigCommandsTests() { _configFileService = Substitute.For(); _autoDiscoveryService = Substitute.For(); _backupService = Substitute.For(); var services = new ServiceCollection(); services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_backupService); services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); _verboseOption = new Option(["--verbose", "-v"]); _quietOption = new Option(["--quiet", "-q"]); } [Fact] public void CreateShowCommand_ReturnsCommandWithSubcommands() { // Act var command = ConfigCommands.CreateShowCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.ShouldNotBeNull(); command.Name.ShouldBe("show"); command.Subcommands.ShouldContain(c => c.Name == "datasync"); command.Subcommands.ShouldContain(c => c.Name == "dataaccess"); command.Subcommands.ShouldContain(c => c.Name == "auth"); command.Subcommands.ShouldContain(c => c.Name == "ldap"); command.Subcommands.ShouldContain(c => c.Name == "search"); command.Subcommands.ShouldContain(c => c.Name == "excelexport"); command.Subcommands.ShouldContain(c => c.Name == "securestore"); command.Subcommands.ShouldContain(c => c.Name == "connections"); command.Subcommands.ShouldContain(c => c.Name == "all"); } [Fact] public void CreateShowCommand_HasJsonOption() { // Act var command = ConfigCommands.CreateShowCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.Options.ShouldContain(o => o.Name == "json"); } [Fact] public async Task ShowDataSyncCommand_WithNoConfigFolder_ReturnsError() { // Arrange _autoDiscoveryService.FindConfigFolderAsync(Arg.Any()) .Returns(Task.FromResult(null)); var command = ConfigCommands.CreateShowCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); var rootCommand = new RootCommand { command }; rootCommand.AddGlobalOption(_configPathOption); rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); // Act var exitCode = await rootCommand.InvokeAsync(["show", "datasync"]); // Assert - command completes without throwing command.ShouldNotBeNull(); } [Fact] public void DataSyncSubcommand_Exists() { // Arrange var command = ConfigCommands.CreateShowCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Act var datasyncSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "datasync"); // Assert datasyncSubcommand.ShouldNotBeNull(); datasyncSubcommand.Description.ShouldBe("Show DataSync configuration"); } [Fact] public async Task ShowConnectionsCommand_MasksNoPasswords() { // 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", Password = "secret123" } ] } }; _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); var command = ConfigCommands.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", "connections"]); // Assert var output = writer.ToString(); output.ShouldContain("TestConnection"); output.ShouldNotContain("secret123"); // Password should not be visible } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public void AllSubcommand_HasJsonOption() { // Arrange var command = ConfigCommands.CreateShowCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Act var allSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "all"); // Assert allSubcommand.ShouldNotBeNull(); // The json option is on the parent show command command.Options.ShouldContain(o => o.Name == "json"); } // ===== Config Set Command Tests ===== [Fact] public void CreateSetCommand_ReturnsCommandWithSubcommands() { // Act var command = ConfigCommands.CreateSetCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Assert command.ShouldNotBeNull(); command.Name.ShouldBe("set"); command.Subcommands.ShouldContain(c => c.Name == "datasync"); command.Subcommands.ShouldContain(c => c.Name == "dataaccess"); command.Subcommands.ShouldContain(c => c.Name == "auth"); command.Subcommands.ShouldContain(c => c.Name == "ldap"); command.Subcommands.ShouldContain(c => c.Name == "search"); command.Subcommands.ShouldContain(c => c.Name == "excelexport"); } [Fact] public void SetDataSyncCommand_HasExpectedOptions() { // Arrange var command = ConfigCommands.CreateSetCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Act var datasyncSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "datasync"); // Assert datasyncSubcommand.ShouldNotBeNull(); datasyncSubcommand.Options.ShouldContain(o => o.Name == "enabled"); datasyncSubcommand.Options.ShouldContain(o => o.Name == "check-interval"); datasyncSubcommand.Options.ShouldContain(o => o.Name == "max-parallelism"); datasyncSubcommand.Options.ShouldContain(o => o.Name == "batch-size"); datasyncSubcommand.Options.ShouldContain(o => o.Name == "bulk-copy-batch-size"); datasyncSubcommand.Options.ShouldContain(o => o.Name == "lookback-multiplier"); datasyncSubcommand.Options.ShouldContain(o => o.Name == "purge-retention-days"); datasyncSubcommand.Options.ShouldContain(o => o.Name == "sync-timeout"); } [Fact] public void SetDataAccessCommand_HasExpectedOptions() { // Arrange var command = ConfigCommands.CreateSetCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Act var dataAccessSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "dataaccess"); // Assert dataAccessSubcommand.ShouldNotBeNull(); dataAccessSubcommand.Options.ShouldContain(o => o.Name == "default-timeout"); dataAccessSubcommand.Options.ShouldContain(o => o.Name == "lot-usage-timeout"); dataAccessSubcommand.Options.ShouldContain(o => o.Name == "mis-data-timeout"); dataAccessSubcommand.Options.ShouldContain(o => o.Name == "detailed-logging"); dataAccessSubcommand.Options.ShouldContain(o => o.Name == "production-schema"); dataAccessSubcommand.Options.ShouldContain(o => o.Name == "archive-schema"); dataAccessSubcommand.Options.ShouldContain(o => o.Name == "stage-schema"); } [Fact] public void SetAuthCommand_HasExpectedOptions() { // Arrange var command = ConfigCommands.CreateSetCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Act var authSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "auth"); // Assert authSubcommand.ShouldNotBeNull(); authSubcommand.Options.ShouldContain(o => o.Name == "cookie-name"); authSubcommand.Options.ShouldContain(o => o.Name == "cookie-expiration"); } [Fact] public void SetLdapCommand_HasExpectedOptions() { // Arrange var command = ConfigCommands.CreateSetCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Act var ldapSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "ldap"); // Assert ldapSubcommand.ShouldNotBeNull(); ldapSubcommand.Options.ShouldContain(o => o.Name == "server-urls"); ldapSubcommand.Options.ShouldContain(o => o.Name == "group-dn"); ldapSubcommand.Options.ShouldContain(o => o.Name == "search-base"); ldapSubcommand.Options.ShouldContain(o => o.Name == "connection-timeout"); ldapSubcommand.Options.ShouldContain(o => o.Name == "use-fake-auth"); ldapSubcommand.Options.ShouldContain(o => o.Name == "admin-bypass-users"); } [Fact] public void SetSearchCommand_HasExpectedOptions() { // Arrange var command = ConfigCommands.CreateSetCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Act var searchSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "search"); // Assert searchSubcommand.ShouldNotBeNull(); searchSubcommand.Options.ShouldContain(o => o.Name == "max-result-rows"); searchSubcommand.Options.ShouldContain(o => o.Name == "timeout"); searchSubcommand.Options.ShouldContain(o => o.Name == "max-concurrent"); } [Fact] public void SetExcelExportCommand_HasExpectedOptions() { // Arrange var command = ConfigCommands.CreateSetCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); // Act var excelExportSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "excelexport"); // Assert excelExportSubcommand.ShouldNotBeNull(); excelExportSubcommand.Options.ShouldContain(o => o.Name == "max-rows-per-sheet"); excelExportSubcommand.Options.ShouldContain(o => o.Name == "date-format"); excelExportSubcommand.Options.ShouldContain(o => o.Name == "timezone-id"); excelExportSubcommand.Options.ShouldContain(o => o.Name == "debug-write-to-file"); excelExportSubcommand.Options.ShouldContain(o => o.Name == "debug-output-dir"); } [Fact] public async Task SetDataSyncCommand_WithNoConfigFolder_ReturnsError() { // Arrange _autoDiscoveryService.FindConfigFolderAsync(Arg.Any()) .Returns(Task.FromResult(null)); var command = ConfigCommands.CreateSetCommand( _serviceProvider, _configPathOption, _verboseOption, _quietOption); var rootCommand = new RootCommand { command }; rootCommand.AddGlobalOption(_configPathOption); rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); // Act var exitCode = await rootCommand.InvokeAsync(["set", "datasync", "--enabled", "true"]); // Assert - command completes without throwing command.ShouldNotBeNull(); } [Fact] public async Task SetDataSyncCommand_WithNoChanges_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 = ConfigCommands.CreateSetCommand( _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 - run without any options await rootCommand.InvokeAsync(["set", "datasync"]); // Assert var output = writer.ToString(); output.ShouldContain("No changes specified"); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public async Task SetDataSyncCommand_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(); _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); _backupService.CreateBackupAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(Path.Combine(tempDir, "backup.json"))); var command = ConfigCommands.CreateSetCommand( _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(["set", "datasync", "--enabled", "false"]); // Assert var output = writer.ToString(); output.ShouldContain("updated successfully"); await _configFileService.Received(1).SaveAppSettingsAsync(appSettingsPath, Arg.Any(), Arg.Any()); await _backupService.Received(1).CreateBackupAsync(appSettingsPath, Arg.Any()); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } [Fact] public async Task SetSearchCommand_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(); _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(config)); _backupService.CreateBackupAsync(appSettingsPath, Arg.Any()) .Returns(Task.FromResult(Path.Combine(tempDir, "backup.json"))); var command = ConfigCommands.CreateSetCommand( _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(["set", "search", "--max-result-rows", "50000"]); // Assert var output = writer.ToString(); output.ShouldContain("updated successfully"); await _configFileService.Received(1).SaveAppSettingsAsync( appSettingsPath, Arg.Is(c => c.Search.MaxResultRows == 50000), Arg.Any()); } finally { Console.SetOut(originalOut); } } finally { Directory.Delete(tempDir, true); } } }