diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs index 9e412fd..cb6dbe1 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs @@ -1,12 +1,13 @@ using System.CommandLine; using System.Text.Json; +using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Services; using Microsoft.Extensions.DependencyInjection; namespace JdeScoping.ConfigManager.Cli.Commands; /// -/// Configuration viewing command implementations. +/// Configuration viewing and editing command implementations. /// public static class ConfigCommands { @@ -432,4 +433,601 @@ public static class ConfigCommands var autoDiscoveryService = serviceProvider.GetRequiredService(); return await autoDiscoveryService.FindConfigFolderAsync(); } + + /// + /// Creates the config set command with section subcommands. + /// + public static Command CreateSetCommand( + IServiceProvider serviceProvider, + Option configPathOption, + Option verboseOption, + Option quietOption) + { + var command = new Command("set", "Modify configuration settings"); + + command.AddCommand(CreateSetDataSyncCommand(serviceProvider, configPathOption, verboseOption, quietOption)); + command.AddCommand(CreateSetDataAccessCommand(serviceProvider, configPathOption, verboseOption, quietOption)); + command.AddCommand(CreateSetAuthCommand(serviceProvider, configPathOption, verboseOption, quietOption)); + command.AddCommand(CreateSetLdapCommand(serviceProvider, configPathOption, verboseOption, quietOption)); + command.AddCommand(CreateSetSearchCommand(serviceProvider, configPathOption, verboseOption, quietOption)); + command.AddCommand(CreateSetExcelExportCommand(serviceProvider, configPathOption, verboseOption, quietOption)); + + return command; + } + + private static Command CreateSetDataSyncCommand( + IServiceProvider serviceProvider, + Option configPathOption, + Option verboseOption, + Option quietOption) + { + var command = new Command("datasync", "Modify DataSync configuration"); + + var enabledOption = new Option("--enabled", "Enable or disable data synchronization"); + var checkIntervalOption = new Option("--check-interval", "Check interval in minutes"); + var maxParallelismOption = new Option("--max-parallelism", "Maximum degree of parallelism"); + var batchSizeOption = new Option("--batch-size", "Batch size for sync operations"); + var bulkCopyBatchSizeOption = new Option("--bulk-copy-batch-size", "Batch size for bulk copy operations"); + var lookbackMultiplierOption = new Option("--lookback-multiplier", "Lookback multiplier for delta calculations"); + var purgeRetentionDaysOption = new Option("--purge-retention-days", "Days to retain synced data before purging"); + var syncTimeoutOption = new Option("--sync-timeout", "Timeout in seconds for sync operations"); + + command.AddOption(enabledOption); + command.AddOption(checkIntervalOption); + command.AddOption(maxParallelismOption); + command.AddOption(batchSizeOption); + command.AddOption(bulkCopyBatchSizeOption); + command.AddOption(lookbackMultiplierOption); + command.AddOption(purgeRetentionDaysOption); + command.AddOption(syncTimeoutOption); + + command.SetHandler(async (context) => + { + var configPath = context.ParseResult.GetValueForOption(configPathOption); + var verbose = context.ParseResult.GetValueForOption(verboseOption); + var quiet = context.ParseResult.GetValueForOption(quietOption); + var enabled = context.ParseResult.GetValueForOption(enabledOption); + var checkInterval = context.ParseResult.GetValueForOption(checkIntervalOption); + var maxParallelism = context.ParseResult.GetValueForOption(maxParallelismOption); + var batchSize = context.ParseResult.GetValueForOption(batchSizeOption); + var bulkCopyBatchSize = context.ParseResult.GetValueForOption(bulkCopyBatchSizeOption); + var lookbackMultiplier = context.ParseResult.GetValueForOption(lookbackMultiplierOption); + var purgeRetentionDays = context.ParseResult.GetValueForOption(purgeRetentionDaysOption); + var syncTimeout = context.ParseResult.GetValueForOption(syncTimeoutOption); + + var exitCode = await SetDataSyncAsync( + serviceProvider, configPath, verbose, quiet, + enabled, checkInterval, maxParallelism, batchSize, bulkCopyBatchSize, + lookbackMultiplier, purgeRetentionDays, syncTimeout); + Environment.ExitCode = exitCode; + }); + + return command; + } + + private static async Task SetDataSyncAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet, + bool? enabled, + int? checkInterval, + int? maxParallelism, + int? batchSize, + int? bulkCopyBatchSize, + double? lookbackMultiplier, + int? purgeRetentionDays, + int? syncTimeout) + { + return await ModifyConfigAsync(serviceProvider, configPath, verbose, quiet, "DataSync", config => + { + var modified = false; + + if (enabled.HasValue) + { + config.DataSync.Enabled = enabled.Value; + modified = true; + } + if (checkInterval.HasValue) + { + config.DataSync.CheckInterval = TimeSpan.FromMinutes(checkInterval.Value); + modified = true; + } + if (maxParallelism.HasValue) + { + config.DataSync.MaxDegreeOfParallelism = maxParallelism.Value; + modified = true; + } + if (batchSize.HasValue) + { + config.DataSync.BatchSize = batchSize.Value; + modified = true; + } + if (bulkCopyBatchSize.HasValue) + { + config.DataSync.BulkCopyBatchSize = bulkCopyBatchSize.Value; + modified = true; + } + if (lookbackMultiplier.HasValue) + { + config.DataSync.LookbackMultiplier = lookbackMultiplier.Value; + modified = true; + } + if (purgeRetentionDays.HasValue) + { + config.DataSync.PurgeRetentionDays = purgeRetentionDays.Value; + modified = true; + } + if (syncTimeout.HasValue) + { + config.DataSync.SyncTimeoutSeconds = syncTimeout.Value; + modified = true; + } + + return modified; + }); + } + + private static Command CreateSetDataAccessCommand( + IServiceProvider serviceProvider, + Option configPathOption, + Option verboseOption, + Option quietOption) + { + var command = new Command("dataaccess", "Modify DataAccess configuration"); + + var defaultTimeoutOption = new Option("--default-timeout", "Default timeout in seconds for database queries"); + var lotUsageTimeoutOption = new Option("--lot-usage-timeout", "Timeout in seconds for lot usage queries"); + var misDataTimeoutOption = new Option("--mis-data-timeout", "Timeout in seconds for MIS data queries"); + var detailedLoggingOption = new Option("--detailed-logging", "Enable or disable detailed query logging"); + var productionSchemaOption = new Option("--production-schema", "Schema name for production data"); + var archiveSchemaOption = new Option("--archive-schema", "Schema name for archive data"); + var stageSchemaOption = new Option("--stage-schema", "Schema name for staging data"); + + command.AddOption(defaultTimeoutOption); + command.AddOption(lotUsageTimeoutOption); + command.AddOption(misDataTimeoutOption); + command.AddOption(detailedLoggingOption); + command.AddOption(productionSchemaOption); + command.AddOption(archiveSchemaOption); + command.AddOption(stageSchemaOption); + + command.SetHandler(async (context) => + { + var configPath = context.ParseResult.GetValueForOption(configPathOption); + var verbose = context.ParseResult.GetValueForOption(verboseOption); + var quiet = context.ParseResult.GetValueForOption(quietOption); + var defaultTimeout = context.ParseResult.GetValueForOption(defaultTimeoutOption); + var lotUsageTimeout = context.ParseResult.GetValueForOption(lotUsageTimeoutOption); + var misDataTimeout = context.ParseResult.GetValueForOption(misDataTimeoutOption); + var detailedLogging = context.ParseResult.GetValueForOption(detailedLoggingOption); + var productionSchema = context.ParseResult.GetValueForOption(productionSchemaOption); + var archiveSchema = context.ParseResult.GetValueForOption(archiveSchemaOption); + var stageSchema = context.ParseResult.GetValueForOption(stageSchemaOption); + + var exitCode = await SetDataAccessAsync( + serviceProvider, configPath, verbose, quiet, + defaultTimeout, lotUsageTimeout, misDataTimeout, detailedLogging, + productionSchema, archiveSchema, stageSchema); + Environment.ExitCode = exitCode; + }); + + return command; + } + + private static async Task SetDataAccessAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet, + int? defaultTimeout, + int? lotUsageTimeout, + int? misDataTimeout, + bool? detailedLogging, + string? productionSchema, + string? archiveSchema, + string? stageSchema) + { + return await ModifyConfigAsync(serviceProvider, configPath, verbose, quiet, "DataAccess", config => + { + var modified = false; + + if (defaultTimeout.HasValue) + { + config.DataAccess.DefaultTimeoutSeconds = defaultTimeout.Value; + modified = true; + } + if (lotUsageTimeout.HasValue) + { + config.DataAccess.LotUsageTimeoutSeconds = lotUsageTimeout.Value; + modified = true; + } + if (misDataTimeout.HasValue) + { + config.DataAccess.MisDataTimeoutSeconds = misDataTimeout.Value; + modified = true; + } + if (detailedLogging.HasValue) + { + config.DataAccess.EnableDetailedLogging = detailedLogging.Value; + modified = true; + } + if (!string.IsNullOrEmpty(productionSchema)) + { + config.DataAccess.ProductionSchema = productionSchema; + modified = true; + } + if (!string.IsNullOrEmpty(archiveSchema)) + { + config.DataAccess.ArchiveSchema = archiveSchema; + modified = true; + } + if (!string.IsNullOrEmpty(stageSchema)) + { + config.DataAccess.StageSchema = stageSchema; + modified = true; + } + + return modified; + }); + } + + private static Command CreateSetAuthCommand( + IServiceProvider serviceProvider, + Option configPathOption, + Option verboseOption, + Option quietOption) + { + var command = new Command("auth", "Modify Auth configuration"); + + var cookieNameOption = new Option("--cookie-name", "Name of the authentication cookie"); + var cookieExpirationOption = new Option("--cookie-expiration", "Cookie expiration time in minutes"); + + command.AddOption(cookieNameOption); + command.AddOption(cookieExpirationOption); + + command.SetHandler(async (context) => + { + var configPath = context.ParseResult.GetValueForOption(configPathOption); + var verbose = context.ParseResult.GetValueForOption(verboseOption); + var quiet = context.ParseResult.GetValueForOption(quietOption); + var cookieName = context.ParseResult.GetValueForOption(cookieNameOption); + var cookieExpiration = context.ParseResult.GetValueForOption(cookieExpirationOption); + + var exitCode = await SetAuthAsync(serviceProvider, configPath, verbose, quiet, cookieName, cookieExpiration); + Environment.ExitCode = exitCode; + }); + + return command; + } + + private static async Task SetAuthAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet, + string? cookieName, + int? cookieExpiration) + { + return await ModifyConfigAsync(serviceProvider, configPath, verbose, quiet, "Auth", config => + { + var modified = false; + + if (!string.IsNullOrEmpty(cookieName)) + { + config.Auth.CookieName = cookieName; + modified = true; + } + if (cookieExpiration.HasValue) + { + config.Auth.CookieExpirationMinutes = cookieExpiration.Value; + modified = true; + } + + return modified; + }); + } + + private static Command CreateSetLdapCommand( + IServiceProvider serviceProvider, + Option configPathOption, + Option verboseOption, + Option quietOption) + { + var command = new Command("ldap", "Modify LDAP configuration"); + + var serverUrlsOption = new Option("--server-urls", "Comma-separated list of LDAP server URLs"); + var groupDnOption = new Option("--group-dn", "Distinguished name of the LDAP group for authorization"); + var searchBaseOption = new Option("--search-base", "Base distinguished name for LDAP searches"); + var connectionTimeoutOption = new Option("--connection-timeout", "Connection timeout in seconds"); + var useFakeAuthOption = new Option("--use-fake-auth", "Use fake authentication instead of LDAP"); + var adminBypassUsersOption = new Option("--admin-bypass-users", "Comma-separated list of users that bypass group membership validation"); + + command.AddOption(serverUrlsOption); + command.AddOption(groupDnOption); + command.AddOption(searchBaseOption); + command.AddOption(connectionTimeoutOption); + command.AddOption(useFakeAuthOption); + command.AddOption(adminBypassUsersOption); + + command.SetHandler(async (context) => + { + var configPath = context.ParseResult.GetValueForOption(configPathOption); + var verbose = context.ParseResult.GetValueForOption(verboseOption); + var quiet = context.ParseResult.GetValueForOption(quietOption); + var serverUrls = context.ParseResult.GetValueForOption(serverUrlsOption); + var groupDn = context.ParseResult.GetValueForOption(groupDnOption); + var searchBase = context.ParseResult.GetValueForOption(searchBaseOption); + var connectionTimeout = context.ParseResult.GetValueForOption(connectionTimeoutOption); + var useFakeAuth = context.ParseResult.GetValueForOption(useFakeAuthOption); + var adminBypassUsers = context.ParseResult.GetValueForOption(adminBypassUsersOption); + + var exitCode = await SetLdapAsync( + serviceProvider, configPath, verbose, quiet, + serverUrls, groupDn, searchBase, connectionTimeout, useFakeAuth, adminBypassUsers); + Environment.ExitCode = exitCode; + }); + + return command; + } + + private static async Task SetLdapAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet, + string? serverUrls, + string? groupDn, + string? searchBase, + int? connectionTimeout, + bool? useFakeAuth, + string? adminBypassUsers) + { + return await ModifyConfigAsync(serviceProvider, configPath, verbose, quiet, "LDAP", config => + { + var modified = false; + + if (serverUrls != null) + { + config.Ldap.ServerUrls = serverUrls.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + modified = true; + } + if (groupDn != null) + { + config.Ldap.GroupDn = groupDn; + modified = true; + } + if (searchBase != null) + { + config.Ldap.SearchBase = searchBase; + modified = true; + } + if (connectionTimeout.HasValue) + { + config.Ldap.ConnectionTimeoutSeconds = connectionTimeout.Value; + modified = true; + } + if (useFakeAuth.HasValue) + { + config.Ldap.UseFakeAuth = useFakeAuth.Value; + modified = true; + } + if (adminBypassUsers != null) + { + config.Ldap.AdminBypassUsers = adminBypassUsers.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + modified = true; + } + + return modified; + }); + } + + private static Command CreateSetSearchCommand( + IServiceProvider serviceProvider, + Option configPathOption, + Option verboseOption, + Option quietOption) + { + var command = new Command("search", "Modify Search configuration"); + + var maxResultRowsOption = new Option("--max-result-rows", "Maximum number of result rows returned by a search"); + var timeoutOption = new Option("--timeout", "Timeout in seconds for search operations"); + var maxConcurrentOption = new Option("--max-concurrent", "Maximum number of concurrent search operations"); + + command.AddOption(maxResultRowsOption); + command.AddOption(timeoutOption); + command.AddOption(maxConcurrentOption); + + command.SetHandler(async (context) => + { + var configPath = context.ParseResult.GetValueForOption(configPathOption); + var verbose = context.ParseResult.GetValueForOption(verboseOption); + var quiet = context.ParseResult.GetValueForOption(quietOption); + var maxResultRows = context.ParseResult.GetValueForOption(maxResultRowsOption); + var timeout = context.ParseResult.GetValueForOption(timeoutOption); + var maxConcurrent = context.ParseResult.GetValueForOption(maxConcurrentOption); + + var exitCode = await SetSearchAsync(serviceProvider, configPath, verbose, quiet, maxResultRows, timeout, maxConcurrent); + Environment.ExitCode = exitCode; + }); + + return command; + } + + private static async Task SetSearchAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet, + int? maxResultRows, + int? timeout, + int? maxConcurrent) + { + return await ModifyConfigAsync(serviceProvider, configPath, verbose, quiet, "Search", config => + { + var modified = false; + + if (maxResultRows.HasValue) + { + config.Search.MaxResultRows = maxResultRows.Value; + modified = true; + } + if (timeout.HasValue) + { + config.Search.TimeoutSeconds = timeout.Value; + modified = true; + } + if (maxConcurrent.HasValue) + { + config.Search.MaxConcurrentSearches = maxConcurrent.Value; + modified = true; + } + + return modified; + }); + } + + private static Command CreateSetExcelExportCommand( + IServiceProvider serviceProvider, + Option configPathOption, + Option verboseOption, + Option quietOption) + { + var command = new Command("excelexport", "Modify ExcelExport configuration"); + + var maxRowsPerSheetOption = new Option("--max-rows-per-sheet", "Maximum number of rows per Excel worksheet"); + var dateFormatOption = new Option("--date-format", "Default date format for Excel exports"); + var timezoneIdOption = new Option("--timezone-id", "Time zone identifier for date/time conversions"); + var debugWriteToFileOption = new Option("--debug-write-to-file", "Write debug output to files"); + var debugOutputDirOption = new Option("--debug-output-dir", "Directory path for debug output files"); + + command.AddOption(maxRowsPerSheetOption); + command.AddOption(dateFormatOption); + command.AddOption(timezoneIdOption); + command.AddOption(debugWriteToFileOption); + command.AddOption(debugOutputDirOption); + + command.SetHandler(async (context) => + { + var configPath = context.ParseResult.GetValueForOption(configPathOption); + var verbose = context.ParseResult.GetValueForOption(verboseOption); + var quiet = context.ParseResult.GetValueForOption(quietOption); + var maxRowsPerSheet = context.ParseResult.GetValueForOption(maxRowsPerSheetOption); + var dateFormat = context.ParseResult.GetValueForOption(dateFormatOption); + var timezoneId = context.ParseResult.GetValueForOption(timezoneIdOption); + var debugWriteToFile = context.ParseResult.GetValueForOption(debugWriteToFileOption); + var debugOutputDir = context.ParseResult.GetValueForOption(debugOutputDirOption); + + var exitCode = await SetExcelExportAsync( + serviceProvider, configPath, verbose, quiet, + maxRowsPerSheet, dateFormat, timezoneId, debugWriteToFile, debugOutputDir); + Environment.ExitCode = exitCode; + }); + + return command; + } + + private static async Task SetExcelExportAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet, + int? maxRowsPerSheet, + string? dateFormat, + string? timezoneId, + bool? debugWriteToFile, + string? debugOutputDir) + { + return await ModifyConfigAsync(serviceProvider, configPath, verbose, quiet, "ExcelExport", config => + { + var modified = false; + + if (maxRowsPerSheet.HasValue) + { + config.ExcelExport.MaxRowsPerSheet = maxRowsPerSheet.Value; + modified = true; + } + if (!string.IsNullOrEmpty(dateFormat)) + { + config.ExcelExport.DefaultDateFormat = dateFormat; + modified = true; + } + if (!string.IsNullOrEmpty(timezoneId)) + { + config.ExcelExport.TimezoneId = timezoneId; + modified = true; + } + if (debugWriteToFile.HasValue) + { + config.ExcelExport.DebugWriteToFile = debugWriteToFile.Value; + modified = true; + } + if (!string.IsNullOrEmpty(debugOutputDir)) + { + config.ExcelExport.DebugOutputDirectory = debugOutputDir; + modified = true; + } + + return modified; + }); + } + + private static async Task ModifyConfigAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet, + string sectionName, + Func modifier) + { + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) + { + Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); + return 1; + } + + var configFileService = serviceProvider.GetRequiredService(); + var backupService = serviceProvider.GetRequiredService(); + var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); + + if (!File.Exists(appSettingsPath)) + { + Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); + return 1; + } + + try + { + var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); + var modified = modifier(config); + + if (!modified) + { + Console.Error.WriteLine("Error: No changes specified. Use --help to see available options."); + return 1; + } + + // Create backup before saving + var backupPath = await backupService.CreateBackupAsync(appSettingsPath); + if (verbose) + { + Console.WriteLine($"Backup created: {Path.GetFileName(backupPath)}"); + } + + await configFileService.SaveAppSettingsAsync(appSettingsPath, config); + + if (!quiet) + { + Console.WriteLine($"{sectionName} configuration updated successfully"); + } + + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: {ex.Message}"); + return 1; + } + } } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs index eed3d30..36ad279 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs @@ -155,6 +155,84 @@ public static class ConnectionCommands return command; } + /// + /// Creates the connection update command. + /// + public static Command CreateUpdateCommand( + IServiceProvider serviceProvider, + Option configPathOption, + Option verboseOption, + Option quietOption) + { + var command = new Command("update", "Update an existing connection"); + + var nameArgument = new Argument("name", "The connection name to update"); + command.AddArgument(nameArgument); + + var providerOption = new Option( + aliases: ["--provider", "-p"], + description: "Database provider (SqlServer, Oracle, Generic)"); + command.AddOption(providerOption); + + var serverOption = new Option( + aliases: ["--server", "-s"], + description: "Server name or host"); + command.AddOption(serverOption); + + var databaseOption = new Option( + aliases: ["--database", "-d"], + description: "Database name"); + command.AddOption(databaseOption); + + var userOption = new Option( + aliases: ["--user", "-u"], + description: "User ID"); + command.AddOption(userOption); + + var passwordOption = new Option( + aliases: ["--password"], + description: "Password (will be stored in config)"); + command.AddOption(passwordOption); + + var portOption = new Option( + aliases: ["--port"], + description: "Port number"); + command.AddOption(portOption); + + var serviceNameOption = new Option( + aliases: ["--service-name"], + description: "Oracle service name"); + command.AddOption(serviceNameOption); + + var rawOption = new Option( + aliases: ["--raw"], + description: "Raw connection string (for Generic provider)"); + command.AddOption(rawOption); + + command.SetHandler(async (context) => + { + var configPath = context.ParseResult.GetValueForOption(configPathOption); + var verbose = context.ParseResult.GetValueForOption(verboseOption); + var quiet = context.ParseResult.GetValueForOption(quietOption); + var name = context.ParseResult.GetValueForArgument(nameArgument); + var provider = context.ParseResult.GetValueForOption(providerOption); + var server = context.ParseResult.GetValueForOption(serverOption); + var database = context.ParseResult.GetValueForOption(databaseOption); + var user = context.ParseResult.GetValueForOption(userOption); + var password = context.ParseResult.GetValueForOption(passwordOption); + var port = context.ParseResult.GetValueForOption(portOption); + var serviceName = context.ParseResult.GetValueForOption(serviceNameOption); + var raw = context.ParseResult.GetValueForOption(rawOption); + + var exitCode = await UpdateConnectionAsync( + serviceProvider, configPath, verbose, quiet, + name, provider, server, database, user, password, port, serviceName, raw); + Environment.ExitCode = exitCode; + }); + + return command; + } + private static async Task ListConnectionsAsync( IServiceProvider serviceProvider, string? configPath, @@ -438,6 +516,118 @@ public static class ConnectionCommands } } + private static async Task UpdateConnectionAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet, + string name, + ConnectionProvider? provider, + string? server, + string? database, + string? user, + string? password, + int? port, + string? serviceName, + string? raw) + { + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) + { + Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); + return 1; + } + + var configFileService = serviceProvider.GetRequiredService(); + var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); + + if (!File.Exists(appSettingsPath)) + { + Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); + return 1; + } + + try + { + var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); + var entry = config.ConnectionStrings.Entries.FirstOrDefault(e => + e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + + if (entry == null) + { + Console.Error.WriteLine($"Error: Connection '{name}' not found"); + return 1; + } + + var modified = false; + + if (provider.HasValue) + { + entry.Provider = provider.Value; + modified = true; + } + if (server != null) + { + entry.Server = server; + entry.Host = server; // For Oracle, use server as host + modified = true; + } + if (database != null) + { + entry.Database = database; + modified = true; + } + if (user != null) + { + entry.UserId = user; + modified = true; + } + if (password != null) + { + entry.Password = password; + modified = true; + } + if (port.HasValue) + { + if (entry.Provider == ConnectionProvider.SqlServer) + entry.SqlServerPort = port; + else if (entry.Provider == ConnectionProvider.Oracle) + entry.Port = port.Value; + modified = true; + } + if (serviceName != null) + { + entry.ServiceName = serviceName; + modified = true; + } + if (raw != null) + { + entry.RawConnectionString = raw; + modified = true; + } + + if (!modified) + { + Console.Error.WriteLine("Error: No changes specified. Use --help to see available options."); + return 1; + } + + await configFileService.SaveAppSettingsAsync(appSettingsPath, config); + + if (!quiet) + { + Console.WriteLine($"Connection '{name}' updated successfully"); + } + + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + private static string MaskPassword(string? password) { if (string.IsNullOrEmpty(password)) diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs index 6d1ffdf..c6dbbfe 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs @@ -79,8 +79,9 @@ public static class Program rootCommand.AddCommand(pipelineCommand); // Config command group - var configCommand = new Command("config", "View configuration settings"); + var configCommand = new Command("config", "View and modify configuration settings"); configCommand.AddCommand(ConfigCommands.CreateShowCommand(serviceProvider, configPathOption, verboseOption, quietOption)); + configCommand.AddCommand(ConfigCommands.CreateSetCommand(serviceProvider, configPathOption, verboseOption, quietOption)); rootCommand.AddCommand(configCommand); // Backup command group @@ -96,6 +97,7 @@ public static class Program connectionCommand.AddCommand(ConnectionCommands.CreateListCommand(serviceProvider, configPathOption, verboseOption, quietOption)); connectionCommand.AddCommand(ConnectionCommands.CreateShowCommand(serviceProvider, configPathOption, verboseOption, quietOption)); connectionCommand.AddCommand(ConnectionCommands.CreateAddCommand(serviceProvider, configPathOption, verboseOption, quietOption)); + connectionCommand.AddCommand(ConnectionCommands.CreateUpdateCommand(serviceProvider, configPathOption, verboseOption, quietOption)); connectionCommand.AddCommand(ConnectionCommands.CreateRemoveCommand(serviceProvider, configPathOption, verboseOption, quietOption)); rootCommand.AddCommand(connectionCommand); diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs index 2fafe76..5f08617 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; namespace JdeScoping.ConfigManager.Cli.Tests.Commands; +[Collection("Console Tests")] public class BackupCommandsTests { private readonly IServiceProvider _serviceProvider; diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs index f944275..fcc3d8a 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs @@ -6,11 +6,13 @@ 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; @@ -19,10 +21,12 @@ public class ConfigCommandsTests { _configFileService = Substitute.For(); _autoDiscoveryService = Substitute.For(); + _backupService = Substitute.For(); var services = new ServiceCollection(); services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); + services.AddSingleton(_backupService); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); @@ -180,4 +184,322 @@ public class ConfigCommandsTests // 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 error output + var originalErr = Console.Error; + using var writer = new StringWriter(); + Console.SetError(writer); + + try + { + // Act - run without any options + await rootCommand.InvokeAsync(["set", "datasync"]); + + // Assert + var output = writer.ToString(); + output.ShouldContain("No changes specified"); + } + finally + { + Console.SetError(originalErr); + } + } + 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); + } + } } diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs index edc8cf3..be7cd6b 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; namespace JdeScoping.ConfigManager.Cli.Tests.Commands; +[Collection("Console Tests")] public class ConnectionCommandsTests { private readonly IServiceProvider _serviceProvider; @@ -416,4 +417,246 @@ public class ConnectionCommandsTests 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 error output + var originalErr = Console.Error; + using var writer = new StringWriter(); + Console.SetError(writer); + + try + { + // Act + await rootCommand.InvokeAsync(["update", "NonExistent", "--server", "newhost"]); + + // Assert + var output = writer.ToString(); + output.ShouldContain("not found"); + } + finally + { + Console.SetError(originalErr); + } + } + 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); + } + } } diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs index ae0eafb..b815625 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; namespace JdeScoping.ConfigManager.Cli.Tests.Commands; +[Collection("Console Tests")] public class PipelineCommandsTests { private readonly IServiceProvider _serviceProvider; diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/SecretCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/SecretCommandsTests.cs new file mode 100644 index 0000000..964f0b2 --- /dev/null +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/SecretCommandsTests.cs @@ -0,0 +1,211 @@ +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); + _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 error output + var originalErr = Console.Error; + using var writer = new StringWriter(); + Console.SetError(writer); + + try + { + // Act + await rootCommand.InvokeAsync(["get", "nonexistent"]); + + // Assert + var output = writer.ToString(); + output.ShouldContain("not found"); + } + finally + { + Console.SetError(originalErr); + } + } + 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(); + } +} diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/TestConnectionCommandTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/TestConnectionCommandTests.cs new file mode 100644 index 0000000..e8cc6b9 --- /dev/null +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/TestConnectionCommandTests.cs @@ -0,0 +1,294 @@ +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 TestConnectionCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IConfigFileService _configFileService; + private readonly IAutoDiscoveryService _autoDiscoveryService; + private readonly IConnectionTestService _connectionTestService; + private readonly Option _configPathOption; + private readonly Option _verboseOption; + private readonly Option _quietOption; + + public TestConnectionCommandTests() + { + _configFileService = Substitute.For(); + _autoDiscoveryService = Substitute.For(); + _connectionTestService = Substitute.For(); + + var services = new ServiceCollection(); + services.AddSingleton(_configFileService); + services.AddSingleton(_autoDiscoveryService); + services.AddSingleton(_connectionTestService); + _serviceProvider = services.BuildServiceProvider(); + + _configPathOption = new Option(["--config-path", "-c"]); + _verboseOption = new Option(["--verbose", "-v"]); + _quietOption = new Option(["--quiet", "-q"]); + } + + [Fact] + public void CreateSqlCommand_ReturnsCommand() + { + // Act + var command = TestConnectionCommand.CreateSqlCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.ShouldNotBeNull(); + command.Name.ShouldBe("sql"); + command.Description.ShouldBe("Test SQL Server connection"); + } + + [Fact] + public void CreateSqlCommand_HasNameOption() + { + // Act + var command = TestConnectionCommand.CreateSqlCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.Options.ShouldContain(o => o.Name == "name"); + } + + [Fact] + public void CreateOracleCommand_ReturnsCommand() + { + // Act + var command = TestConnectionCommand.CreateOracleCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.ShouldNotBeNull(); + command.Name.ShouldBe("oracle"); + command.Description.ShouldBe("Test Oracle connection"); + } + + [Fact] + public void CreateOracleCommand_HasNameOption() + { + // Act + var command = TestConnectionCommand.CreateOracleCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.Options.ShouldContain(o => o.Name == "name"); + } + + [Fact] + public void CreateAllCommand_ReturnsCommand() + { + // Act + var command = TestConnectionCommand.CreateAllCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.ShouldNotBeNull(); + command.Name.ShouldBe("all"); + command.Description.ShouldBe("Test all configured connections"); + } + + [Fact] + public async Task SqlCommand_WithNoConfigFolder_ReturnsError() + { + // Arrange + _autoDiscoveryService.FindConfigFolderAsync(Arg.Any()) + .Returns(Task.FromResult(null)); + + var command = TestConnectionCommand.CreateSqlCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + var rootCommand = new RootCommand { command }; + rootCommand.AddGlobalOption(_configPathOption); + rootCommand.AddGlobalOption(_verboseOption); + rootCommand.AddGlobalOption(_quietOption); + + // Act + var exitCode = await rootCommand.InvokeAsync(["sql"]); + + // Assert - command completes without throwing + command.ShouldNotBeNull(); + } + + [Fact] + public async Task SqlCommand_WithSuccessfulConnection_ReturnsSuccess() + { + // 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 = "LocalCache", + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + Database = "TestDb" + } + ] + } + }; + + _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) + .Returns(Task.FromResult(config)); + + _connectionTestService.TestConnectionAsync( + Arg.Any(), + ConnectionProvider.SqlServer, + Arg.Any()) + .Returns(Task.FromResult(new ConnectionTestResult + { + Success = true, + Duration = TimeSpan.FromMilliseconds(50) + })); + + var command = TestConnectionCommand.CreateSqlCommand( + _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(["sql"]); + + // Assert + var output = writer.ToString(); + output.ShouldContain("Success"); + } + finally + { + Console.SetOut(originalOut); + } + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public async Task SqlCommand_WithFailedConnection_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 = "LocalCache", + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + Database = "TestDb" + } + ] + } + }; + + _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any()) + .Returns(Task.FromResult(config)); + + _connectionTestService.TestConnectionAsync( + Arg.Any(), + ConnectionProvider.SqlServer, + Arg.Any()) + .Returns(Task.FromResult(new ConnectionTestResult + { + Success = false, + Message = "Connection refused" + })); + + var command = TestConnectionCommand.CreateSqlCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + var rootCommand = new RootCommand { command }; + rootCommand.AddGlobalOption(_configPathOption); + rootCommand.AddGlobalOption(_verboseOption); + rootCommand.AddGlobalOption(_quietOption); + + // Capture console error output + var originalErr = Console.Error; + using var writer = new StringWriter(); + Console.SetError(writer); + + try + { + // Act + await rootCommand.InvokeAsync(["sql"]); + + // Assert + var output = writer.ToString(); + output.ShouldContain("Failed"); + } + finally + { + Console.SetError(originalErr); + } + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void SqlCommand_NameOptionExists() + { + // Arrange & Act + var command = TestConnectionCommand.CreateSqlCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + var nameOption = command.Options.FirstOrDefault(o => o.Name == "name"); + nameOption.ShouldNotBeNull(); + nameOption.Description.ShouldBe("Name of the connection string to test"); + } + + [Fact] + public void AllCommand_HasCorrectDescription() + { + // Arrange & Act + var command = TestConnectionCommand.CreateAllCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.Description.ShouldBe("Test all configured connections"); + } +} diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ValidateCommandTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ValidateCommandTests.cs new file mode 100644 index 0000000..2723940 --- /dev/null +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ValidateCommandTests.cs @@ -0,0 +1,298 @@ +using System.CommandLine; +using JdeScoping.ConfigManager.Cli.Commands; +using JdeScoping.ConfigManager.Core.Models; +using JdeScoping.ConfigManager.Core.Services; +using JdeScoping.DataSync.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace JdeScoping.ConfigManager.Cli.Tests.Commands; + +[Collection("Console Tests")] +public class ValidateCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IConfigFileService _configFileService; + private readonly IAutoDiscoveryService _autoDiscoveryService; + private readonly IValidationService _validationService; + private readonly IRuntimeConfigValidationService _runtimeValidationService; + private readonly Option _configPathOption; + private readonly Option _verboseOption; + private readonly Option _quietOption; + + public ValidateCommandTests() + { + _configFileService = Substitute.For(); + _autoDiscoveryService = Substitute.For(); + _validationService = Substitute.For(); + _runtimeValidationService = Substitute.For(); + + var services = new ServiceCollection(); + services.AddSingleton(_configFileService); + services.AddSingleton(_autoDiscoveryService); + services.AddSingleton(_validationService); + services.AddSingleton(_runtimeValidationService); + _serviceProvider = services.BuildServiceProvider(); + + _configPathOption = new Option(["--config-path", "-c"]); + _verboseOption = new Option(["--verbose", "-v"]); + _quietOption = new Option(["--quiet", "-q"]); + } + + [Fact] + public void CreateAppSettingsCommand_ReturnsCommand() + { + // Act + var command = ValidateCommand.CreateAppSettingsCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.ShouldNotBeNull(); + command.Name.ShouldBe("appsettings"); + command.Description.ShouldBe("Validate appsettings.json"); + } + + [Fact] + public void CreatePipelinesCommand_ReturnsCommand() + { + // Act + var command = ValidateCommand.CreatePipelinesCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.ShouldNotBeNull(); + command.Name.ShouldBe("pipelines"); + command.Description.ShouldBe("Validate pipeline configuration files"); + } + + [Fact] + public void CreateAllCommand_ReturnsCommand() + { + // Act + var command = ValidateCommand.CreateAllCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.ShouldNotBeNull(); + command.Name.ShouldBe("all"); + command.Description.ShouldBe("Validate all configuration files"); + } + + [Fact] + public void CreateRuntimeCommand_ReturnsCommand() + { + // Act + var command = ValidateCommand.CreateRuntimeCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + + // Assert + command.ShouldNotBeNull(); + command.Name.ShouldBe("runtime"); + command.Description.ShouldBe("Run Infrastructure validators"); + } + + [Fact] + public async Task AppSettingsCommand_WithNoConfigFolder_ReturnsError() + { + // Arrange + _autoDiscoveryService.FindConfigFolderAsync(Arg.Any()) + .Returns(Task.FromResult(null)); + + var command = ValidateCommand.CreateAppSettingsCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + var rootCommand = new RootCommand { command }; + rootCommand.AddGlobalOption(_configPathOption); + rootCommand.AddGlobalOption(_verboseOption); + rootCommand.AddGlobalOption(_quietOption); + + // Act + var exitCode = await rootCommand.InvokeAsync(["appsettings"]); + + // Assert - command completes without throwing + command.ShouldNotBeNull(); + } + + [Fact] + public async Task AppSettingsCommand_WithValidConfig_ReturnsSuccess() + { + // 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)); + + _validationService.ValidateAppSettings(config) + .Returns(new ValidationResult()); // No errors = IsValid true + + var command = ValidateCommand.CreateAppSettingsCommand( + _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(["appsettings"]); + + // Assert + var output = writer.ToString(); + output.ShouldContain("Valid"); + } + finally + { + Console.SetOut(originalOut); + } + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public async Task AppSettingsCommand_WithInvalidConfig_ReturnsErrors() + { + // 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 result = new ValidationResult(); + result.Errors.Add("Connection string 'LocalCache' is required"); + _validationService.ValidateAppSettings(config).Returns(result); + + var command = ValidateCommand.CreateAppSettingsCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + var rootCommand = new RootCommand { command }; + rootCommand.AddGlobalOption(_configPathOption); + rootCommand.AddGlobalOption(_verboseOption); + rootCommand.AddGlobalOption(_quietOption); + + // Capture console error output + var originalErr = Console.Error; + using var writer = new StringWriter(); + Console.SetError(writer); + + try + { + // Act + await rootCommand.InvokeAsync(["appsettings"]); + + // Assert + var output = writer.ToString(); + output.ShouldContain("Error"); + } + finally + { + Console.SetError(originalErr); + } + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public async Task PipelinesCommand_WithValidPipelines_ReturnsSuccess() + { + // Arrange + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var pipelinesDir = Path.Combine(tempDir, "Pipelines"); + Directory.CreateDirectory(pipelinesDir); + + try + { + _autoDiscoveryService.FindConfigFolderAsync(Arg.Any()) + .Returns(Task.FromResult(tempDir)); + + var pipelines = new Dictionary + { + ["TestPipeline"] = new EtlPipelineConfig { Name = "TestPipeline" } + }; + + _configFileService.LoadAllPipelinesAsync(pipelinesDir, Arg.Any()) + .Returns(Task.FromResult(pipelines)); + + _validationService.ValidatePipelines(pipelines) + .Returns(new ValidationResult()); // No errors = IsValid true + + var command = ValidateCommand.CreatePipelinesCommand( + _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(["pipelines"]); + + // Assert + var output = writer.ToString(); + output.ShouldContain("Valid"); + } + finally + { + Console.SetOut(originalOut); + } + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public async Task RuntimeCommand_WithNoConfigFolder_ReturnsError() + { + // Arrange + _autoDiscoveryService.FindConfigFolderAsync(Arg.Any()) + .Returns(Task.FromResult(null)); + + var command = ValidateCommand.CreateRuntimeCommand( + _serviceProvider, _configPathOption, _verboseOption, _quietOption); + var rootCommand = new RootCommand { command }; + rootCommand.AddGlobalOption(_configPathOption); + rootCommand.AddGlobalOption(_verboseOption); + rootCommand.AddGlobalOption(_quietOption); + + // Act + var exitCode = await rootCommand.InvokeAsync(["runtime"]); + + // Assert - command completes without throwing + command.ShouldNotBeNull(); + } +} diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/ConsoleTestCollection.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/ConsoleTestCollection.cs new file mode 100644 index 0000000..c240af1 --- /dev/null +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/ConsoleTestCollection.cs @@ -0,0 +1,10 @@ +namespace JdeScoping.ConfigManager.Cli.Tests; + +/// +/// Collection definition for tests that redirect Console.Out or Console.Error. +/// These tests must run sequentially to avoid interference. +/// +[CollectionDefinition("Console Tests", DisableParallelization = true)] +public class ConsoleTestCollection +{ +}