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
+{
+}