feat(configmanager): add config set and connection update CLI commands
Add missing CLI commands to match UI capabilities: config set commands for all configuration sections (datasync, dataaccess, auth, ldap, search, excelexport) and connection update command. Also adds unit tests for SecretCommands, ValidateCommand, and TestConnectionCommand.
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration viewing command implementations.
|
||||
/// Configuration viewing and editing command implementations.
|
||||
/// </summary>
|
||||
public static class ConfigCommands
|
||||
{
|
||||
@@ -432,4 +433,601 @@ public static class ConfigCommands
|
||||
var autoDiscoveryService = serviceProvider.GetRequiredService<IAutoDiscoveryService>();
|
||||
return await autoDiscoveryService.FindConfigFolderAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the config set command with section subcommands.
|
||||
/// </summary>
|
||||
public static Command CreateSetCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> 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<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("datasync", "Modify DataSync configuration");
|
||||
|
||||
var enabledOption = new Option<bool?>("--enabled", "Enable or disable data synchronization");
|
||||
var checkIntervalOption = new Option<int?>("--check-interval", "Check interval in minutes");
|
||||
var maxParallelismOption = new Option<int?>("--max-parallelism", "Maximum degree of parallelism");
|
||||
var batchSizeOption = new Option<int?>("--batch-size", "Batch size for sync operations");
|
||||
var bulkCopyBatchSizeOption = new Option<int?>("--bulk-copy-batch-size", "Batch size for bulk copy operations");
|
||||
var lookbackMultiplierOption = new Option<double?>("--lookback-multiplier", "Lookback multiplier for delta calculations");
|
||||
var purgeRetentionDaysOption = new Option<int?>("--purge-retention-days", "Days to retain synced data before purging");
|
||||
var syncTimeoutOption = new Option<int?>("--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<int> 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<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("dataaccess", "Modify DataAccess configuration");
|
||||
|
||||
var defaultTimeoutOption = new Option<int?>("--default-timeout", "Default timeout in seconds for database queries");
|
||||
var lotUsageTimeoutOption = new Option<int?>("--lot-usage-timeout", "Timeout in seconds for lot usage queries");
|
||||
var misDataTimeoutOption = new Option<int?>("--mis-data-timeout", "Timeout in seconds for MIS data queries");
|
||||
var detailedLoggingOption = new Option<bool?>("--detailed-logging", "Enable or disable detailed query logging");
|
||||
var productionSchemaOption = new Option<string?>("--production-schema", "Schema name for production data");
|
||||
var archiveSchemaOption = new Option<string?>("--archive-schema", "Schema name for archive data");
|
||||
var stageSchemaOption = new Option<string?>("--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<int> 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<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("auth", "Modify Auth configuration");
|
||||
|
||||
var cookieNameOption = new Option<string?>("--cookie-name", "Name of the authentication cookie");
|
||||
var cookieExpirationOption = new Option<int?>("--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<int> 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<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("ldap", "Modify LDAP configuration");
|
||||
|
||||
var serverUrlsOption = new Option<string?>("--server-urls", "Comma-separated list of LDAP server URLs");
|
||||
var groupDnOption = new Option<string?>("--group-dn", "Distinguished name of the LDAP group for authorization");
|
||||
var searchBaseOption = new Option<string?>("--search-base", "Base distinguished name for LDAP searches");
|
||||
var connectionTimeoutOption = new Option<int?>("--connection-timeout", "Connection timeout in seconds");
|
||||
var useFakeAuthOption = new Option<bool?>("--use-fake-auth", "Use fake authentication instead of LDAP");
|
||||
var adminBypassUsersOption = new Option<string?>("--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<int> 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<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("search", "Modify Search configuration");
|
||||
|
||||
var maxResultRowsOption = new Option<int?>("--max-result-rows", "Maximum number of result rows returned by a search");
|
||||
var timeoutOption = new Option<int?>("--timeout", "Timeout in seconds for search operations");
|
||||
var maxConcurrentOption = new Option<int?>("--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<int> 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<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("excelexport", "Modify ExcelExport configuration");
|
||||
|
||||
var maxRowsPerSheetOption = new Option<int?>("--max-rows-per-sheet", "Maximum number of rows per Excel worksheet");
|
||||
var dateFormatOption = new Option<string?>("--date-format", "Default date format for Excel exports");
|
||||
var timezoneIdOption = new Option<string?>("--timezone-id", "Time zone identifier for date/time conversions");
|
||||
var debugWriteToFileOption = new Option<bool?>("--debug-write-to-file", "Write debug output to files");
|
||||
var debugOutputDirOption = new Option<string?>("--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<int> 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<int> ModifyConfigAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
string sectionName,
|
||||
Func<ConfigModel, bool> 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<IConfigFileService>();
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,84 @@ public static class ConnectionCommands
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the connection update command.
|
||||
/// </summary>
|
||||
public static Command CreateUpdateCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("update", "Update an existing connection");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The connection name to update");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
var providerOption = new Option<ConnectionProvider?>(
|
||||
aliases: ["--provider", "-p"],
|
||||
description: "Database provider (SqlServer, Oracle, Generic)");
|
||||
command.AddOption(providerOption);
|
||||
|
||||
var serverOption = new Option<string?>(
|
||||
aliases: ["--server", "-s"],
|
||||
description: "Server name or host");
|
||||
command.AddOption(serverOption);
|
||||
|
||||
var databaseOption = new Option<string?>(
|
||||
aliases: ["--database", "-d"],
|
||||
description: "Database name");
|
||||
command.AddOption(databaseOption);
|
||||
|
||||
var userOption = new Option<string?>(
|
||||
aliases: ["--user", "-u"],
|
||||
description: "User ID");
|
||||
command.AddOption(userOption);
|
||||
|
||||
var passwordOption = new Option<string?>(
|
||||
aliases: ["--password"],
|
||||
description: "Password (will be stored in config)");
|
||||
command.AddOption(passwordOption);
|
||||
|
||||
var portOption = new Option<int?>(
|
||||
aliases: ["--port"],
|
||||
description: "Port number");
|
||||
command.AddOption(portOption);
|
||||
|
||||
var serviceNameOption = new Option<string?>(
|
||||
aliases: ["--service-name"],
|
||||
description: "Oracle service name");
|
||||
command.AddOption(serviceNameOption);
|
||||
|
||||
var rawOption = new Option<string?>(
|
||||
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<int> ListConnectionsAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
@@ -438,6 +516,118 @@ public static class ConnectionCommands
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> 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<IConfigFileService>();
|
||||
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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
|
||||
|
||||
[Collection("Console Tests")]
|
||||
public class BackupCommandsTests
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
@@ -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<string?> _configPathOption;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly Option<bool> _quietOption;
|
||||
@@ -19,10 +21,12 @@ public class ConfigCommandsTests
|
||||
{
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
_backupService = Substitute.For<IBackupService>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddSingleton(_backupService);
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
_backupService.CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.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<ConfigModel>(), Arg.Any<CancellationToken>());
|
||||
await _backupService.Received(1).CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>());
|
||||
}
|
||||
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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
_backupService.CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.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<ConfigModel>(c => c.Search.MaxResultRows == 50000),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+243
@@ -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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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<CancellationToken>())
|
||||
.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<ConfigModel>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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<CancellationToken>())
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
|
||||
|
||||
[Collection("Console Tests")]
|
||||
public class PipelineCommandsTests
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
@@ -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<string?> _configPathOption;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly Option<bool> _quietOption;
|
||||
|
||||
public SecretCommandsTests()
|
||||
{
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
_secureStoreManager = Substitute.For<ISecureStoreManager>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddSingleton(_secureStoreManager);
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
_verboseOption = new Option<bool>(["--verbose", "-v"]);
|
||||
_quietOption = new Option<bool>(["--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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel
|
||||
{
|
||||
SecureStore = new SecureStoreSection
|
||||
{
|
||||
StorePath = "data/secrets.json",
|
||||
KeyFilePath = "data/secrets.key"
|
||||
}
|
||||
};
|
||||
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.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();
|
||||
}
|
||||
}
|
||||
+294
@@ -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<string?> _configPathOption;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly Option<bool> _quietOption;
|
||||
|
||||
public TestConnectionCommandTests()
|
||||
{
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
_connectionTestService = Substitute.For<IConnectionTestService>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddSingleton(_connectionTestService);
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
_verboseOption = new Option<bool>(["--verbose", "-v"]);
|
||||
_quietOption = new Option<bool>(["--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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
_connectionTestService.TestConnectionAsync(
|
||||
Arg.Any<string>(),
|
||||
ConnectionProvider.SqlServer,
|
||||
Arg.Any<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
_connectionTestService.TestConnectionAsync(
|
||||
Arg.Any<string>(),
|
||||
ConnectionProvider.SqlServer,
|
||||
Arg.Any<CancellationToken>())
|
||||
.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");
|
||||
}
|
||||
}
|
||||
@@ -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<string?> _configPathOption;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly Option<bool> _quietOption;
|
||||
|
||||
public ValidateCommandTests()
|
||||
{
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
_validationService = Substitute.For<IValidationService>();
|
||||
_runtimeValidationService = Substitute.For<IRuntimeConfigValidationService>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
services.AddSingleton(_validationService);
|
||||
services.AddSingleton(_runtimeValidationService);
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
_configPathOption = new Option<string?>(["--config-path", "-c"]);
|
||||
_verboseOption = new Option<bool>(["--verbose", "-v"]);
|
||||
_quietOption = new Option<bool>(["--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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var pipelines = new Dictionary<string, EtlPipelineConfig>
|
||||
{
|
||||
["TestPipeline"] = new EtlPipelineConfig { Name = "TestPipeline" }
|
||||
};
|
||||
|
||||
_configFileService.LoadAllPipelinesAsync(pipelinesDir, Arg.Any<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace JdeScoping.ConfigManager.Cli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Collection definition for tests that redirect Console.Out or Console.Error.
|
||||
/// These tests must run sequentially to avoid interference.
|
||||
/// </summary>
|
||||
[CollectionDefinition("Console Tests", DisableParallelization = true)]
|
||||
public class ConsoleTestCollection
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user