feat(configmanager): add CLI feature parity with UI
Expand ConfigManager.Cli with pipeline, config, backup, and connection commands to match the feature set of ConfigManager.Ui, plus README documentation.
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Backup management command implementations.
|
||||
/// </summary>
|
||||
public static class BackupCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the backup create command.
|
||||
/// </summary>
|
||||
public static Command CreateCreateCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("create", "Create backup of appsettings.json");
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
|
||||
{
|
||||
var exitCode = await CreateBackupAsync(serviceProvider, configPath, verbose, quiet);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the backup list command.
|
||||
/// </summary>
|
||||
public static Command CreateListCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("list", "List available backups");
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
|
||||
{
|
||||
var exitCode = await ListBackupsAsync(serviceProvider, configPath, verbose, quiet);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the backup restore command.
|
||||
/// </summary>
|
||||
public static Command CreateRestoreCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("restore", "Restore a specific backup");
|
||||
|
||||
var timestampArgument = new Argument<string>("timestamp", "The backup timestamp (e.g., 2026-01-15_120000)");
|
||||
command.AddArgument(timestampArgument);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string timestamp) =>
|
||||
{
|
||||
var exitCode = await RestoreBackupAsync(serviceProvider, configPath, verbose, quiet, timestamp);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, timestampArgument);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the backup cleanup command.
|
||||
/// </summary>
|
||||
public static Command CreateCleanupCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("cleanup", "Remove old backups");
|
||||
|
||||
var keepOption = new Option<int>(
|
||||
aliases: ["--keep", "-k"],
|
||||
getDefaultValue: () => 10,
|
||||
description: "Number of backups to keep");
|
||||
command.AddOption(keepOption);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, int keep) =>
|
||||
{
|
||||
var exitCode = await CleanupBackupsAsync(serviceProvider, configPath, verbose, quiet, keep);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, keepOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static async Task<int> CreateBackupAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
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 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 backupPath = await backupService.CreateBackupAsync(appSettingsPath);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine("Backup created successfully");
|
||||
Console.WriteLine($"Path: {backupPath}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> ListBackupsAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
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 backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
{
|
||||
var backups = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== Backups ({backups.Count}) ===");
|
||||
}
|
||||
|
||||
if (backups.Count == 0)
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine("No backups found");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"{"Timestamp",-20} {"Size",-12} {"Path"}");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
}
|
||||
|
||||
foreach (var backup in backups)
|
||||
{
|
||||
var timestamp = backup.Timestamp.ToString("yyyy-MM-dd_HHmmss");
|
||||
var size = FormatFileSize(backup.Size);
|
||||
var path = verbose ? backup.Path : Path.GetFileName(backup.Path);
|
||||
|
||||
Console.WriteLine($"{timestamp,-20} {size,-12} {path}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> RestoreBackupAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
string timestamp)
|
||||
{
|
||||
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 backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
{
|
||||
var backups = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var backup = backups.FirstOrDefault(b => b.Path.Contains(timestamp));
|
||||
|
||||
if (backup == null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Backup with timestamp '{timestamp}' not found");
|
||||
|
||||
if (backups.Count > 0)
|
||||
{
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Available backups:");
|
||||
foreach (var b in backups.Take(5))
|
||||
{
|
||||
Console.Error.WriteLine($" {b.Timestamp:yyyy-MM-dd_HHmmss}");
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create a backup of current config before restoring
|
||||
if (File.Exists(appSettingsPath))
|
||||
{
|
||||
var currentBackup = await backupService.CreateBackupAsync(appSettingsPath);
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"Current config backed up to: {Path.GetFileName(currentBackup)}");
|
||||
}
|
||||
}
|
||||
|
||||
await backupService.RestoreBackupAsync(backup.Path, appSettingsPath);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine("Backup restored successfully");
|
||||
Console.WriteLine($"Restored from: {Path.GetFileName(backup.Path)}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> CleanupBackupsAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
int keep)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (keep < 1)
|
||||
{
|
||||
Console.Error.WriteLine("Error: --keep must be at least 1");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var backupService = serviceProvider.GetRequiredService<IBackupService>();
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
|
||||
try
|
||||
{
|
||||
var backupsBefore = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var countBefore = backupsBefore.Count;
|
||||
|
||||
await backupService.CleanupOldBackupsAsync(appSettingsPath, keep);
|
||||
|
||||
var backupsAfter = await backupService.GetBackupsAsync(appSettingsPath);
|
||||
var deleted = countBefore - backupsAfter.Count;
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
if (deleted > 0)
|
||||
{
|
||||
Console.WriteLine($"Deleted {deleted} old backup(s)");
|
||||
Console.WriteLine($"Remaining: {backupsAfter.Count} backup(s)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("No backups to delete");
|
||||
Console.WriteLine($"Current count: {countBefore} (keeping {keep})");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatFileSize(long bytes)
|
||||
{
|
||||
string[] sizes = ["B", "KB", "MB", "GB"];
|
||||
var order = 0;
|
||||
double size = bytes;
|
||||
|
||||
while (size >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
size /= 1024;
|
||||
}
|
||||
|
||||
return $"{size:0.##} {sizes[order]}";
|
||||
}
|
||||
|
||||
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configPath))
|
||||
{
|
||||
if (Directory.Exists(configPath))
|
||||
return configPath;
|
||||
return null;
|
||||
}
|
||||
|
||||
var autoDiscoveryService = serviceProvider.GetRequiredService<IAutoDiscoveryService>();
|
||||
return await autoDiscoveryService.FindConfigFolderAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration viewing command implementations.
|
||||
/// </summary>
|
||||
public static class ConfigCommands
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates the config show command with section subcommands.
|
||||
/// </summary>
|
||||
public static Command CreateShowCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("show", "View configuration settings");
|
||||
|
||||
var jsonOption = new Option<bool>(
|
||||
aliases: ["--json", "-j"],
|
||||
description: "Output in JSON format");
|
||||
|
||||
command.AddOption(jsonOption);
|
||||
|
||||
// Add section subcommands
|
||||
command.AddCommand(CreateSectionCommand("datasync", "Show DataSync configuration",
|
||||
serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
|
||||
config => config.DataSync, FormatDataSync));
|
||||
|
||||
command.AddCommand(CreateSectionCommand("dataaccess", "Show DataAccess configuration",
|
||||
serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
|
||||
config => config.DataAccess, FormatDataAccess));
|
||||
|
||||
command.AddCommand(CreateSectionCommand("auth", "Show Auth configuration",
|
||||
serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
|
||||
config => config.Auth, FormatAuth));
|
||||
|
||||
command.AddCommand(CreateSectionCommand("ldap", "Show LDAP configuration",
|
||||
serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
|
||||
config => config.Ldap, FormatLdap));
|
||||
|
||||
command.AddCommand(CreateSectionCommand("search", "Show Search configuration",
|
||||
serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
|
||||
config => config.Search, FormatSearch));
|
||||
|
||||
command.AddCommand(CreateSectionCommand("excelexport", "Show ExcelExport configuration",
|
||||
serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
|
||||
config => config.ExcelExport, FormatExcelExport));
|
||||
|
||||
command.AddCommand(CreateSectionCommand("securestore", "Show SecureStore configuration",
|
||||
serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
|
||||
config => config.SecureStore, FormatSecureStore));
|
||||
|
||||
command.AddCommand(CreateConnectionsCommand(serviceProvider, configPathOption, verboseOption, quietOption, jsonOption));
|
||||
command.AddCommand(CreateAllCommand(serviceProvider, configPathOption, verboseOption, quietOption, jsonOption));
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command CreateSectionCommand<T>(
|
||||
string name,
|
||||
string description,
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption,
|
||||
Option<bool> jsonOption,
|
||||
Func<Core.Models.ConfigModel, T> selector,
|
||||
Action<T, bool> formatter)
|
||||
{
|
||||
var command = new Command(name, description);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, bool json) =>
|
||||
{
|
||||
var exitCode = await ShowSectionAsync(serviceProvider, configPath, verbose, quiet, json, name, selector, formatter);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, jsonOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command CreateConnectionsCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption,
|
||||
Option<bool> jsonOption)
|
||||
{
|
||||
var command = new Command("connections", "Show connection names (no secrets)");
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, bool json) =>
|
||||
{
|
||||
var exitCode = await ShowConnectionsAsync(serviceProvider, configPath, verbose, quiet, json);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, jsonOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command CreateAllCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption,
|
||||
Option<bool> jsonOption)
|
||||
{
|
||||
var command = new Command("all", "Show all configuration sections");
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, bool json) =>
|
||||
{
|
||||
var exitCode = await ShowAllAsync(serviceProvider, configPath, verbose, quiet, json);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, jsonOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static async Task<int> ShowSectionAsync<T>(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
bool json,
|
||||
string sectionName,
|
||||
Func<Core.Models.ConfigModel, T> selector,
|
||||
Action<T, bool> formatter)
|
||||
{
|
||||
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 section = selector(config);
|
||||
|
||||
if (json)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(section, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== {sectionName} ===");
|
||||
}
|
||||
formatter(section, verbose);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> ShowConnectionsAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
bool json)
|
||||
{
|
||||
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);
|
||||
|
||||
if (json)
|
||||
{
|
||||
// Output only names and providers, no secrets
|
||||
var safeConnections = config.ConnectionStrings.Entries.Select(e => new
|
||||
{
|
||||
e.Name,
|
||||
Provider = e.Provider.ToString()
|
||||
});
|
||||
Console.WriteLine(JsonSerializer.Serialize(safeConnections, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
|
||||
Console.WriteLine($"{"Name",-25} {"Provider",-15}");
|
||||
Console.WriteLine(new string('-', 40));
|
||||
}
|
||||
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
{
|
||||
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15}");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> ShowAllAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
bool json)
|
||||
{
|
||||
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);
|
||||
|
||||
if (json)
|
||||
{
|
||||
// Sanitize passwords before outputting
|
||||
var sanitizedConfig = new
|
||||
{
|
||||
config.DataSync,
|
||||
config.DataAccess,
|
||||
config.Auth,
|
||||
config.Ldap,
|
||||
config.Search,
|
||||
config.ExcelExport,
|
||||
config.SecureStore,
|
||||
config.Pipelines,
|
||||
Connections = config.ConnectionStrings.Entries.Select(e => new
|
||||
{
|
||||
e.Name,
|
||||
Provider = e.Provider.ToString()
|
||||
})
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(sanitizedConfig, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("=== DataSync ===");
|
||||
FormatDataSync(config.DataSync, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== DataAccess ===");
|
||||
FormatDataAccess(config.DataAccess, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== Auth ===");
|
||||
FormatAuth(config.Auth, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== LDAP ===");
|
||||
FormatLdap(config.Ldap, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== Search ===");
|
||||
FormatSearch(config.Search, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== ExcelExport ===");
|
||||
FormatExcelExport(config.ExcelExport, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== SecureStore ===");
|
||||
FormatSecureStore(config.SecureStore, verbose);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
|
||||
Console.WriteLine($"{"Name",-25} {"Provider",-15}");
|
||||
Console.WriteLine(new string('-', 40));
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
{
|
||||
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15}");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FormatDataSync(Core.Models.DataSyncSection section, bool verbose)
|
||||
{
|
||||
Console.WriteLine($"Enabled: {section.Enabled}");
|
||||
Console.WriteLine($"CheckInterval: {section.CheckInterval}");
|
||||
Console.WriteLine($"MaxDegreeOfParallelism: {section.MaxDegreeOfParallelism}");
|
||||
Console.WriteLine($"BatchSize: {section.BatchSize}");
|
||||
Console.WriteLine($"BulkCopyBatchSize: {section.BulkCopyBatchSize}");
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"LookbackMultiplier: {section.LookbackMultiplier}");
|
||||
Console.WriteLine($"PurgeRetentionDays: {section.PurgeRetentionDays}");
|
||||
Console.WriteLine($"SyncTimeoutSeconds: {section.SyncTimeoutSeconds}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void FormatDataAccess(Core.Models.DataAccessSection section, bool verbose)
|
||||
{
|
||||
Console.WriteLine($"DefaultTimeoutSeconds: {section.DefaultTimeoutSeconds}");
|
||||
Console.WriteLine($"LotUsageTimeoutSeconds: {section.LotUsageTimeoutSeconds}");
|
||||
Console.WriteLine($"MisDataTimeoutSeconds: {section.MisDataTimeoutSeconds}");
|
||||
Console.WriteLine($"EnableDetailedLogging: {section.EnableDetailedLogging}");
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"ProductionSchema: {section.ProductionSchema}");
|
||||
Console.WriteLine($"ArchiveSchema: {section.ArchiveSchema}");
|
||||
Console.WriteLine($"StageSchema: {section.StageSchema}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void FormatAuth(Core.Models.AuthSection section, bool verbose)
|
||||
{
|
||||
Console.WriteLine($"CookieName: {section.CookieName}");
|
||||
Console.WriteLine($"CookieExpirationMinutes: {section.CookieExpirationMinutes}");
|
||||
}
|
||||
|
||||
private static void FormatLdap(Core.Models.LdapSection section, bool verbose)
|
||||
{
|
||||
Console.WriteLine($"UseFakeAuth: {section.UseFakeAuth}");
|
||||
Console.WriteLine($"ConnectionTimeoutSeconds: {section.ConnectionTimeoutSeconds}");
|
||||
Console.WriteLine($"ServerUrls: {(section.ServerUrls.Length > 0 ? string.Join(", ", section.ServerUrls) : "(none)")}");
|
||||
Console.WriteLine($"GroupDn: {(string.IsNullOrEmpty(section.GroupDn) ? "(not set)" : section.GroupDn)}");
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"SearchBase: {(string.IsNullOrEmpty(section.SearchBase) ? "(not set)" : section.SearchBase)}");
|
||||
Console.WriteLine($"AdminBypassUsers: {(section.AdminBypassUsers.Length > 0 ? string.Join(", ", section.AdminBypassUsers) : "(none)")}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void FormatSearch(Core.Models.SearchSection section, bool verbose)
|
||||
{
|
||||
Console.WriteLine($"MaxResultRows: {section.MaxResultRows}");
|
||||
Console.WriteLine($"TimeoutSeconds: {section.TimeoutSeconds}");
|
||||
Console.WriteLine($"MaxConcurrentSearches: {section.MaxConcurrentSearches}");
|
||||
}
|
||||
|
||||
private static void FormatExcelExport(Core.Models.ExcelExportSection section, bool verbose)
|
||||
{
|
||||
Console.WriteLine($"MaxRowsPerSheet: {section.MaxRowsPerSheet}");
|
||||
Console.WriteLine($"DefaultDateFormat: {section.DefaultDateFormat}");
|
||||
Console.WriteLine($"TimezoneId: {section.TimezoneId}");
|
||||
Console.WriteLine($"TimezoneAbbreviation: {section.TimezoneAbbreviation}");
|
||||
Console.WriteLine($"DebugWriteToFile: {section.DebugWriteToFile}");
|
||||
|
||||
if (verbose && section.DebugWriteToFile)
|
||||
{
|
||||
Console.WriteLine($"DebugOutputDirectory: {section.DebugOutputDirectory}");
|
||||
}
|
||||
|
||||
// Note: passwords are not shown
|
||||
Console.WriteLine("CriteriaSheetPassword: ********");
|
||||
Console.WriteLine("DataSheetPassword: ********");
|
||||
}
|
||||
|
||||
private static void FormatSecureStore(Core.Models.SecureStoreSection section, bool verbose)
|
||||
{
|
||||
Console.WriteLine($"StorePath: {section.StorePath}");
|
||||
Console.WriteLine($"KeyFilePath: {section.KeyFilePath}");
|
||||
Console.WriteLine($"AutoCreateStore: {section.AutoCreateStore}");
|
||||
Console.WriteLine($"RequiredKeys: {(section.RequiredKeys.Count > 0 ? string.Join(", ", section.RequiredKeys) : "(none)")}");
|
||||
}
|
||||
|
||||
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configPath))
|
||||
{
|
||||
if (Directory.Exists(configPath))
|
||||
return configPath;
|
||||
return null;
|
||||
}
|
||||
|
||||
var autoDiscoveryService = serviceProvider.GetRequiredService<IAutoDiscoveryService>();
|
||||
return await autoDiscoveryService.FindConfigFolderAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Connection string management command implementations.
|
||||
/// </summary>
|
||||
public static class ConnectionCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the connection list command.
|
||||
/// </summary>
|
||||
public static Command CreateListCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("list", "List all connection names and providers");
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
|
||||
{
|
||||
var exitCode = await ListConnectionsAsync(serviceProvider, configPath, verbose, quiet);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the connection show command.
|
||||
/// </summary>
|
||||
public static Command CreateShowCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("show", "Show connection details (password masked)");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The connection name");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
|
||||
{
|
||||
var exitCode = await ShowConnectionAsync(serviceProvider, configPath, verbose, quiet, name);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, nameArgument);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the connection add command.
|
||||
/// </summary>
|
||||
public static Command CreateAddCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("add", "Add new connection");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The connection name");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
var providerOption = new Option<ConnectionProvider>(
|
||||
aliases: ["--provider", "-p"],
|
||||
getDefaultValue: () => ConnectionProvider.SqlServer,
|
||||
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 AddConnectionAsync(
|
||||
serviceProvider, configPath, verbose, quiet,
|
||||
name, provider, server, database, user, password, port, serviceName, raw);
|
||||
Environment.ExitCode = exitCode;
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the connection remove command.
|
||||
/// </summary>
|
||||
public static Command CreateRemoveCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("remove", "Remove a connection");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The connection name to remove");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
|
||||
{
|
||||
var exitCode = await RemoveConnectionAsync(serviceProvider, configPath, verbose, quiet, name);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, nameArgument);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static async Task<int> ListConnectionsAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
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);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
|
||||
Console.WriteLine($"{"Name",-25} {"Provider",-15} {"Server/Host",-30}");
|
||||
Console.WriteLine(new string('-', 70));
|
||||
}
|
||||
|
||||
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
|
||||
{
|
||||
var serverOrHost = entry.Provider switch
|
||||
{
|
||||
ConnectionProvider.SqlServer => entry.Server ?? "-",
|
||||
ConnectionProvider.Oracle => entry.Host ?? "-",
|
||||
ConnectionProvider.Generic => "(raw)",
|
||||
_ => "-"
|
||||
};
|
||||
|
||||
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15} {serverOrHost,-30}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> ShowConnectionAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
string name)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== Connection: {entry.Name} ===");
|
||||
}
|
||||
|
||||
Console.WriteLine($"Name: {entry.Name}");
|
||||
Console.WriteLine($"Provider: {entry.Provider}");
|
||||
|
||||
switch (entry.Provider)
|
||||
{
|
||||
case ConnectionProvider.SqlServer:
|
||||
Console.WriteLine($"Server: {entry.Server ?? "(not set)"}");
|
||||
if (entry.SqlServerPort.HasValue)
|
||||
Console.WriteLine($"Port: {entry.SqlServerPort.Value}");
|
||||
Console.WriteLine($"Database: {entry.Database ?? "(not set)"}");
|
||||
Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}");
|
||||
Console.WriteLine($"Password: {MaskPassword(entry.Password)}");
|
||||
Console.WriteLine($"Encrypt: {entry.Encrypt}");
|
||||
Console.WriteLine($"TrustServerCertificate: {entry.TrustServerCertificate}");
|
||||
Console.WriteLine($"ConnectionTimeout: {entry.ConnectionTimeout}");
|
||||
if (!string.IsNullOrEmpty(entry.ApplicationName))
|
||||
Console.WriteLine($"ApplicationName: {entry.ApplicationName}");
|
||||
break;
|
||||
|
||||
case ConnectionProvider.Oracle:
|
||||
Console.WriteLine($"Host: {entry.Host ?? "(not set)"}");
|
||||
Console.WriteLine($"Port: {entry.Port}");
|
||||
Console.WriteLine($"ServiceName: {entry.ServiceName ?? "(not set)"}");
|
||||
Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}");
|
||||
Console.WriteLine($"Password: {MaskPassword(entry.Password)}");
|
||||
break;
|
||||
|
||||
case ConnectionProvider.Generic:
|
||||
Console.WriteLine($"RawConnectionString: {MaskConnectionString(entry.RawConnectionString)}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Generated connection string (masked):");
|
||||
Console.WriteLine($" {MaskConnectionString(entry.GenerateConnectionString())}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> AddConnectionAsync(
|
||||
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);
|
||||
|
||||
// Check if connection already exists
|
||||
if (config.ConnectionStrings.Entries.Any(e =>
|
||||
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Connection '{name}' already exists");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate required fields based on provider
|
||||
if (provider == ConnectionProvider.Generic && string.IsNullOrEmpty(raw))
|
||||
{
|
||||
Console.Error.WriteLine("Error: --raw is required for Generic provider");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var entry = new ConnectionStringEntry
|
||||
{
|
||||
Name = name,
|
||||
Provider = provider,
|
||||
Server = server,
|
||||
Database = database,
|
||||
UserId = user,
|
||||
Password = password,
|
||||
Host = server, // For Oracle, use server as host
|
||||
ServiceName = serviceName,
|
||||
RawConnectionString = raw
|
||||
};
|
||||
|
||||
if (port.HasValue)
|
||||
{
|
||||
if (provider == ConnectionProvider.SqlServer)
|
||||
entry.SqlServerPort = port;
|
||||
else if (provider == ConnectionProvider.Oracle)
|
||||
entry.Port = port.Value;
|
||||
}
|
||||
|
||||
config.ConnectionStrings.Entries.Add(entry);
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"Connection '{name}' added successfully");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> RemoveConnectionAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
string name)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
config.ConnectionStrings.Entries.Remove(entry);
|
||||
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"Connection '{name}' removed successfully");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static string MaskPassword(string? password)
|
||||
{
|
||||
if (string.IsNullOrEmpty(password))
|
||||
return "(not set)";
|
||||
return "********";
|
||||
}
|
||||
|
||||
private static string MaskConnectionString(string? connectionString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
return "(not set)";
|
||||
|
||||
// Mask password in connection string
|
||||
var result = connectionString;
|
||||
|
||||
// Common patterns for password in connection strings
|
||||
var patterns = new[]
|
||||
{
|
||||
"Password=",
|
||||
"Pwd=",
|
||||
"password="
|
||||
};
|
||||
|
||||
foreach (var pattern in patterns)
|
||||
{
|
||||
var idx = result.IndexOf(pattern, StringComparison.OrdinalIgnoreCase);
|
||||
if (idx >= 0)
|
||||
{
|
||||
var start = idx + pattern.Length;
|
||||
var end = result.IndexOf(';', start);
|
||||
if (end < 0) end = result.Length;
|
||||
|
||||
result = string.Concat(result.AsSpan(0, start), "********", result.AsSpan(end));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configPath))
|
||||
{
|
||||
if (Directory.Exists(configPath))
|
||||
return configPath;
|
||||
return null;
|
||||
}
|
||||
|
||||
var autoDiscoveryService = serviceProvider.GetRequiredService<IAutoDiscoveryService>();
|
||||
return await autoDiscoveryService.FindConfigFolderAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline management command implementations.
|
||||
/// </summary>
|
||||
public static class PipelineCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the pipeline list command.
|
||||
/// </summary>
|
||||
public static Command CreateListCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("list", "List all pipelines with status");
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
|
||||
{
|
||||
var exitCode = await ListPipelinesAsync(serviceProvider, configPath, verbose, quiet);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the pipeline show command.
|
||||
/// </summary>
|
||||
public static Command CreateShowCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("show", "Show detailed pipeline configuration");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The pipeline name");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
|
||||
{
|
||||
var exitCode = await ShowPipelineAsync(serviceProvider, configPath, verbose, quiet, name);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, nameArgument);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the pipeline create command.
|
||||
/// </summary>
|
||||
public static Command CreateCreateCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("create", "Create new pipeline with defaults");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The pipeline name");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
var enabledOption = new Option<bool>(
|
||||
aliases: ["--enabled", "-e"],
|
||||
getDefaultValue: () => true,
|
||||
description: "Whether the pipeline is enabled");
|
||||
command.AddOption(enabledOption);
|
||||
|
||||
var manualOnlyOption = new Option<bool>(
|
||||
aliases: ["--manual-only", "-m"],
|
||||
description: "Pipeline can only be triggered manually");
|
||||
command.AddOption(manualOnlyOption);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name, bool enabled, bool manualOnly) =>
|
||||
{
|
||||
var exitCode = await CreatePipelineAsync(serviceProvider, configPath, verbose, quiet, name, enabled, manualOnly);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, nameArgument, enabledOption, manualOnlyOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the pipeline delete command.
|
||||
/// </summary>
|
||||
public static Command CreateDeleteCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("delete", "Delete a pipeline");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The pipeline name to delete");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
var forceOption = new Option<bool>(
|
||||
aliases: ["--force", "-f"],
|
||||
description: "Skip confirmation prompt");
|
||||
command.AddOption(forceOption);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name, bool force) =>
|
||||
{
|
||||
var exitCode = await DeletePipelineAsync(serviceProvider, configPath, verbose, quiet, name, force);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, nameArgument, forceOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the pipeline enable command.
|
||||
/// </summary>
|
||||
public static Command CreateEnableCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("enable", "Enable a pipeline");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The pipeline name to enable");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
|
||||
{
|
||||
var exitCode = await SetPipelineEnabledAsync(serviceProvider, configPath, verbose, quiet, name, true);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, nameArgument);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the pipeline disable command.
|
||||
/// </summary>
|
||||
public static Command CreateDisableCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<string?> configPathOption,
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> quietOption)
|
||||
{
|
||||
var command = new Command("disable", "Disable a pipeline");
|
||||
|
||||
var nameArgument = new Argument<string>("name", "The pipeline name to disable");
|
||||
command.AddArgument(nameArgument);
|
||||
|
||||
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
|
||||
{
|
||||
var exitCode = await SetPipelineEnabledAsync(serviceProvider, configPath, verbose, quiet, name, false);
|
||||
Environment.ExitCode = exitCode;
|
||||
}, configPathOption, verboseOption, quietOption, nameArgument);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static async Task<int> ListPipelinesAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet)
|
||||
{
|
||||
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 pipelinesDir = Path.Combine(folderPath, "Pipelines");
|
||||
|
||||
try
|
||||
{
|
||||
var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== Pipelines ({pipelines.Count}) ===");
|
||||
Console.WriteLine($"{"Name",-20} {"Enabled",-8} {"Mass",-8} {"Daily",-8} {"Hourly",-8}");
|
||||
Console.WriteLine(new string('-', 52));
|
||||
}
|
||||
|
||||
foreach (var kvp in pipelines.OrderBy(p => p.Key))
|
||||
{
|
||||
var p = kvp.Value;
|
||||
var mass = FormatInterval(p.MassSyncIntervalMinutes);
|
||||
var daily = FormatInterval(p.DailySyncIntervalMinutes);
|
||||
var hourly = FormatInterval(p.HourlySyncIntervalMinutes);
|
||||
var enabled = p.IsEnabled ? "Yes" : "No";
|
||||
|
||||
Console.WriteLine($"{p.Name,-20} {enabled,-8} {mass,-8} {daily,-8} {hourly,-8}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> ShowPipelineAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
string name)
|
||||
{
|
||||
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 pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
if (!File.Exists(pipelinePath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"=== Pipeline: {pipeline.Name} ===");
|
||||
}
|
||||
|
||||
Console.WriteLine($"Name: {pipeline.Name}");
|
||||
Console.WriteLine($"Enabled: {pipeline.IsEnabled}");
|
||||
Console.WriteLine($"Manual Only: {pipeline.IsManualOnly}");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("Sync Intervals:");
|
||||
Console.WriteLine($" Mass: {FormatInterval(pipeline.MassSyncIntervalMinutes)}");
|
||||
Console.WriteLine($" Daily: {FormatInterval(pipeline.DailySyncIntervalMinutes)}");
|
||||
Console.WriteLine($" Hourly: {FormatInterval(pipeline.HourlySyncIntervalMinutes)}");
|
||||
|
||||
if (pipeline.Source != null)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Source:");
|
||||
Console.WriteLine($" Connection: {pipeline.Source.Connection}");
|
||||
if (!string.IsNullOrEmpty(pipeline.Source.Query))
|
||||
Console.WriteLine($" Query: {(pipeline.Source.Query.Length > 50 ? pipeline.Source.Query[..50] + "..." : pipeline.Source.Query)}");
|
||||
}
|
||||
|
||||
if (pipeline.Destination != null)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Destination:");
|
||||
Console.WriteLine($" Table: {pipeline.Destination.Table}");
|
||||
if (pipeline.Destination.MatchColumns.Count > 0)
|
||||
Console.WriteLine($" Match Columns: {string.Join(", ", pipeline.Destination.MatchColumns)}");
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
if (pipeline.PreScripts.Count > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Pre-Scripts: {pipeline.PreScripts.Count}");
|
||||
}
|
||||
|
||||
if (pipeline.Transforms.Count > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Transforms: {pipeline.Transforms.Count}");
|
||||
}
|
||||
|
||||
if (pipeline.PostScripts.Count > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Post-Scripts: {pipeline.PostScripts.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> CreatePipelineAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
string name,
|
||||
bool enabled,
|
||||
bool manualOnly)
|
||||
{
|
||||
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 pipelinesDir = Path.Combine(folderPath, "Pipelines");
|
||||
var pipelinePath = Path.Combine(pipelinesDir, $"pipeline.{name}.json");
|
||||
|
||||
if (File.Exists(pipelinePath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Pipeline '{name}' already exists");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Create Pipelines directory if it doesn't exist
|
||||
if (!Directory.Exists(pipelinesDir))
|
||||
{
|
||||
Directory.CreateDirectory(pipelinesDir);
|
||||
}
|
||||
|
||||
var pipeline = new EtlPipelineConfig
|
||||
{
|
||||
Name = name,
|
||||
IsEnabled = enabled,
|
||||
IsManualOnly = manualOnly,
|
||||
Source = new SourceElement { Connection = "jde" },
|
||||
Destination = new DestinationElement { Table = name }
|
||||
};
|
||||
|
||||
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"Pipeline '{name}' created successfully");
|
||||
Console.WriteLine($"File: {pipelinePath}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> DeletePipelineAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
string name,
|
||||
bool force)
|
||||
{
|
||||
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 pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
if (!File.Exists(pipelinePath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!force)
|
||||
{
|
||||
Console.Write($"Delete pipeline '{name}'? [y/N] ");
|
||||
var response = Console.ReadLine();
|
||||
if (!string.Equals(response, "y", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(response, "yes", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!quiet)
|
||||
Console.WriteLine("Cancelled");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await configFileService.DeletePipelineFileAsync(pipelinePath);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
Console.WriteLine($"Pipeline '{name}' deleted successfully");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> SetPipelineEnabledAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
string? configPath,
|
||||
bool verbose,
|
||||
bool quiet,
|
||||
string name,
|
||||
bool enabled)
|
||||
{
|
||||
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 pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
|
||||
|
||||
if (!File.Exists(pipelinePath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
|
||||
pipeline.IsEnabled = enabled;
|
||||
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
var action = enabled ? "enabled" : "disabled";
|
||||
Console.WriteLine($"Pipeline '{name}' {action} successfully");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatInterval(int? minutes)
|
||||
{
|
||||
if (!minutes.HasValue)
|
||||
return "-";
|
||||
|
||||
if (minutes.Value >= 10080) // 7 days
|
||||
return $"{minutes.Value / 10080}w";
|
||||
if (minutes.Value >= 1440) // 1 day
|
||||
return $"{minutes.Value / 1440}d";
|
||||
if (minutes.Value >= 60)
|
||||
return $"{minutes.Value / 60}h";
|
||||
return $"{minutes.Value}m";
|
||||
}
|
||||
|
||||
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configPath))
|
||||
{
|
||||
if (Directory.Exists(configPath))
|
||||
return configPath;
|
||||
return null;
|
||||
}
|
||||
|
||||
var autoDiscoveryService = serviceProvider.GetRequiredService<IAutoDiscoveryService>();
|
||||
return await autoDiscoveryService.FindConfigFolderAsync();
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,37 @@ public static class Program
|
||||
secretCommand.AddCommand(SecretCommands.CreateInitCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
rootCommand.AddCommand(secretCommand);
|
||||
|
||||
// Pipeline command group
|
||||
var pipelineCommand = new Command("pipeline", "Manage ETL pipelines");
|
||||
pipelineCommand.AddCommand(PipelineCommands.CreateListCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
pipelineCommand.AddCommand(PipelineCommands.CreateShowCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
pipelineCommand.AddCommand(PipelineCommands.CreateCreateCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
pipelineCommand.AddCommand(PipelineCommands.CreateDeleteCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
pipelineCommand.AddCommand(PipelineCommands.CreateEnableCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
pipelineCommand.AddCommand(PipelineCommands.CreateDisableCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
rootCommand.AddCommand(pipelineCommand);
|
||||
|
||||
// Config command group
|
||||
var configCommand = new Command("config", "View configuration settings");
|
||||
configCommand.AddCommand(ConfigCommands.CreateShowCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
rootCommand.AddCommand(configCommand);
|
||||
|
||||
// Backup command group
|
||||
var backupCommand = new Command("backup", "Manage configuration backups");
|
||||
backupCommand.AddCommand(BackupCommands.CreateCreateCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
backupCommand.AddCommand(BackupCommands.CreateListCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
backupCommand.AddCommand(BackupCommands.CreateRestoreCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
backupCommand.AddCommand(BackupCommands.CreateCleanupCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
rootCommand.AddCommand(backupCommand);
|
||||
|
||||
// Connection command group
|
||||
var connectionCommand = new Command("connection", "Manage connection strings");
|
||||
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.CreateRemoveCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||
rootCommand.AddCommand(connectionCommand);
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
# JdeScoping ConfigManager CLI
|
||||
|
||||
Command-line tool for managing JDE Scoping Tool configuration files, ETL pipelines, connection strings, secrets, and backups.
|
||||
|
||||
## Installation
|
||||
|
||||
Build the tool from the solution:
|
||||
|
||||
```bash
|
||||
dotnet build NEW/src/Utils/JdeScoping.ConfigManager.Cli/JdeScoping.ConfigManager.Cli.csproj
|
||||
```
|
||||
|
||||
The executable is located at:
|
||||
- **Windows:** `bin/Debug/net10.0/jdescoping-config.exe`
|
||||
- **macOS/Linux:** `bin/Debug/net10.0/jdescoping-config`
|
||||
|
||||
## Global Options
|
||||
|
||||
These options apply to all commands:
|
||||
|
||||
| Option | Alias | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--config-path` | `-c` | Path to configuration folder (overrides auto-discovery) |
|
||||
| `--verbose` | `-v` | Enable verbose output |
|
||||
| `--quiet` | `-q` | Suppress non-error output |
|
||||
|
||||
## Configuration Discovery
|
||||
|
||||
If `--config-path` is not specified, the tool searches for configuration in this order:
|
||||
|
||||
1. `JDESCOPING_CONFIG_PATH` environment variable
|
||||
2. Same directory as the executable
|
||||
3. `../JdeScoping.Host/` relative to executable
|
||||
4. User config directory (`~/.jdescoping` on Unix, `%LOCALAPPDATA%\JdeScoping` on Windows)
|
||||
|
||||
## Commands Reference
|
||||
|
||||
### Validation Commands
|
||||
|
||||
Validate configuration files for errors and warnings.
|
||||
|
||||
```bash
|
||||
# Validate appsettings.json
|
||||
jdescoping-config validate appsettings
|
||||
|
||||
# Validate all pipeline files
|
||||
jdescoping-config validate pipelines
|
||||
|
||||
# Validate all configuration files
|
||||
jdescoping-config validate all
|
||||
|
||||
# Run runtime infrastructure validators
|
||||
jdescoping-config validate runtime
|
||||
```
|
||||
|
||||
### Connection Testing Commands
|
||||
|
||||
Test database connections defined in configuration.
|
||||
|
||||
```bash
|
||||
# Test SQL Server connection
|
||||
jdescoping-config test-connection sql
|
||||
|
||||
# Test a specific named connection
|
||||
jdescoping-config test-connection sql --name SqlServerCache
|
||||
|
||||
# Test Oracle connection
|
||||
jdescoping-config test-connection oracle
|
||||
|
||||
# Test all configured connections
|
||||
jdescoping-config test-connection all
|
||||
```
|
||||
|
||||
### Secret Management Commands
|
||||
|
||||
Manage encrypted secrets in SecureStore.
|
||||
|
||||
```bash
|
||||
# Initialize a new SecureStore
|
||||
jdescoping-config secret init
|
||||
|
||||
# List all secret keys
|
||||
jdescoping-config secret list
|
||||
|
||||
# Get a secret value
|
||||
jdescoping-config secret get <key>
|
||||
|
||||
# Set or update a secret
|
||||
jdescoping-config secret set <key> <value>
|
||||
|
||||
# Remove a secret
|
||||
jdescoping-config secret remove <key>
|
||||
```
|
||||
|
||||
### Pipeline Management Commands
|
||||
|
||||
Manage ETL pipeline configurations.
|
||||
|
||||
```bash
|
||||
# List all pipelines with status
|
||||
jdescoping-config pipeline list
|
||||
|
||||
# Example output:
|
||||
# === Pipelines (3) ===
|
||||
# Name Enabled Mass Daily Hourly
|
||||
# -------------------- -------- -------- -------- --------
|
||||
# WorkOrder_Curr Yes 1w 1d 1h
|
||||
# LotUsage_Curr Yes 1w 1d -
|
||||
# Item No 1w - -
|
||||
|
||||
# Show detailed pipeline configuration
|
||||
jdescoping-config pipeline show <name>
|
||||
|
||||
# Create a new pipeline with defaults
|
||||
jdescoping-config pipeline create <name>
|
||||
jdescoping-config pipeline create <name> --enabled false
|
||||
jdescoping-config pipeline create <name> --manual-only
|
||||
|
||||
# Delete a pipeline
|
||||
jdescoping-config pipeline delete <name>
|
||||
jdescoping-config pipeline delete <name> --force # Skip confirmation
|
||||
|
||||
# Enable/disable a pipeline
|
||||
jdescoping-config pipeline enable <name>
|
||||
jdescoping-config pipeline disable <name>
|
||||
```
|
||||
|
||||
### Configuration Viewing Commands
|
||||
|
||||
View configuration settings (read-only).
|
||||
|
||||
```bash
|
||||
# Show specific sections
|
||||
jdescoping-config config show datasync
|
||||
jdescoping-config config show dataaccess
|
||||
jdescoping-config config show auth
|
||||
jdescoping-config config show ldap
|
||||
jdescoping-config config show search
|
||||
jdescoping-config config show excelexport
|
||||
jdescoping-config config show securestore
|
||||
|
||||
# Show connection names (no secrets)
|
||||
jdescoping-config config show connections
|
||||
|
||||
# Show all configuration sections
|
||||
jdescoping-config config show all
|
||||
|
||||
# Output as JSON
|
||||
jdescoping-config config show datasync --json
|
||||
jdescoping-config config show all --json
|
||||
```
|
||||
|
||||
### Backup Management Commands
|
||||
|
||||
Manage configuration file backups.
|
||||
|
||||
```bash
|
||||
# Create a backup of appsettings.json
|
||||
jdescoping-config backup create
|
||||
|
||||
# List available backups
|
||||
jdescoping-config backup list
|
||||
|
||||
# Example output:
|
||||
# === Backups (3) ===
|
||||
# Timestamp Size Path
|
||||
# -------------------- ------------ --------------------------------
|
||||
# 2026-01-15_120000 1.2 KB appsettings.2026-01-15_120000.bak
|
||||
# 2026-01-14_090000 1.1 KB appsettings.2026-01-14_090000.bak
|
||||
|
||||
# Restore a specific backup
|
||||
jdescoping-config backup restore <timestamp>
|
||||
jdescoping-config backup restore 2026-01-15_120000
|
||||
|
||||
# Remove old backups (keep most recent N)
|
||||
jdescoping-config backup cleanup
|
||||
jdescoping-config backup cleanup --keep 5
|
||||
```
|
||||
|
||||
### Connection String Management Commands
|
||||
|
||||
Manage database connection strings.
|
||||
|
||||
```bash
|
||||
# List all connections
|
||||
jdescoping-config connection list
|
||||
|
||||
# Example output:
|
||||
# === Connections (3) ===
|
||||
# Name Provider Server/Host
|
||||
# ------------------------- --------------- ------------------------------
|
||||
# SqlServerCache SqlServer localhost
|
||||
# JdeOracle Oracle jde-prod-server
|
||||
# CmsSybase Generic (raw)
|
||||
|
||||
# Show connection details (password masked)
|
||||
jdescoping-config connection show <name>
|
||||
jdescoping-config connection show SqlServerCache
|
||||
|
||||
# Add a new SQL Server connection
|
||||
jdescoping-config connection add MyConnection \
|
||||
--provider SqlServer \
|
||||
--server localhost \
|
||||
--database MyDatabase \
|
||||
--user sa \
|
||||
--password secret123 \
|
||||
--port 1434
|
||||
|
||||
# Add a new Oracle connection
|
||||
jdescoping-config connection add JdeOracle \
|
||||
--provider Oracle \
|
||||
--server oracle-host \
|
||||
--service-name JDEPRD \
|
||||
--port 1521 \
|
||||
--user jdeuser
|
||||
|
||||
# Add a Generic connection (raw connection string)
|
||||
jdescoping-config connection add CustomDb \
|
||||
--provider Generic \
|
||||
--raw "Driver={...};Server=...;Database=..."
|
||||
|
||||
# Remove a connection
|
||||
jdescoping-config connection remove <name>
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 0 | Success |
|
||||
| 1 | Error (validation failure, missing file, operation error) |
|
||||
|
||||
## Examples
|
||||
|
||||
### Validate and test before deployment
|
||||
|
||||
```bash
|
||||
# Validate all configuration
|
||||
jdescoping-config validate all
|
||||
|
||||
# Test all database connections
|
||||
jdescoping-config test-connection all
|
||||
|
||||
# Create a backup before making changes
|
||||
jdescoping-config backup create
|
||||
```
|
||||
|
||||
### Quick status check
|
||||
|
||||
```bash
|
||||
# View pipelines
|
||||
jdescoping-config pipeline list --quiet
|
||||
|
||||
# View connections
|
||||
jdescoping-config connection list --quiet
|
||||
```
|
||||
|
||||
### Scripted configuration
|
||||
|
||||
```bash
|
||||
# Disable a pipeline for maintenance
|
||||
jdescoping-config pipeline disable WorkOrder_Curr
|
||||
|
||||
# Re-enable after maintenance
|
||||
jdescoping-config pipeline enable WorkOrder_Curr
|
||||
```
|
||||
|
||||
### Export configuration as JSON
|
||||
|
||||
```bash
|
||||
# Export all config to JSON for review
|
||||
jdescoping-config config show all --json > config-snapshot.json
|
||||
```
|
||||
@@ -0,0 +1,298 @@
|
||||
using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Cli.Commands;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
|
||||
|
||||
public class BackupCommandsTests
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IBackupService _backupService;
|
||||
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
||||
private readonly Option<string?> _configPathOption;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly Option<bool> _quietOption;
|
||||
|
||||
public BackupCommandsTests()
|
||||
{
|
||||
_backupService = Substitute.For<IBackupService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_backupService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
_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 CreateCreateCommand_ReturnsCommand()
|
||||
{
|
||||
// Act
|
||||
var command = BackupCommands.CreateCreateCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("create");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateListCommand_ReturnsCommand()
|
||||
{
|
||||
// Act
|
||||
var command = BackupCommands.CreateListCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("list");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRestoreCommand_ReturnsCommandWithTimestampArgument()
|
||||
{
|
||||
// Act
|
||||
var command = BackupCommands.CreateRestoreCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("restore");
|
||||
command.Arguments.ShouldContain(a => a.Name == "timestamp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCleanupCommand_ReturnsCommandWithKeepOption()
|
||||
{
|
||||
// Act
|
||||
var command = BackupCommands.CreateCleanupCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("cleanup");
|
||||
command.Options.ShouldContain(o => o.Name == "keep");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCommand_WithNoConfigFolder_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(null));
|
||||
|
||||
var command = BackupCommands.CreateCreateCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Act
|
||||
var exitCode = await rootCommand.InvokeAsync(["create"]);
|
||||
|
||||
// Assert - command completes without throwing
|
||||
command.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCommand_WithValidConfig_CreatesBackup()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
var backupPath = Path.Combine(tempDir, "appsettings.2026-01-15_120000.bak");
|
||||
|
||||
try
|
||||
{
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
_backupService.CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(backupPath));
|
||||
|
||||
var command = BackupCommands.CreateCreateCommand(
|
||||
_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(["create"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("Backup created successfully");
|
||||
await _backupService.Received(1).CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListCommand_HasCorrectDescription()
|
||||
{
|
||||
// Arrange & Act
|
||||
var command = BackupCommands.CreateListCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.Description.ShouldBe("List available backups");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCommand_WithNoBackups_ShowsEmptyMessage()
|
||||
{
|
||||
// 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));
|
||||
|
||||
_backupService.GetBackupsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<BackupInfo>>(new List<BackupInfo>()));
|
||||
|
||||
var command = BackupCommands.CreateListCommand(
|
||||
_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(["list"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("No backups found");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupCommand_CallsCleanupService()
|
||||
{
|
||||
// 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));
|
||||
|
||||
_backupService.GetBackupsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<BackupInfo>>(new List<BackupInfo>()));
|
||||
|
||||
var command = BackupCommands.CreateCleanupCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Act
|
||||
await rootCommand.InvokeAsync(["cleanup", "--keep", "5"]);
|
||||
|
||||
// Assert
|
||||
await _backupService.Received(1).CleanupOldBackupsAsync(appSettingsPath, 5, Arg.Any<CancellationToken>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RestoreCommand_WithNonExistentBackup_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));
|
||||
|
||||
_backupService.GetBackupsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<BackupInfo>>(new List<BackupInfo>()));
|
||||
|
||||
var command = BackupCommands.CreateRestoreCommand(
|
||||
_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(["restore", "2026-01-99_999999"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("not found");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
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;
|
||||
|
||||
public class ConfigCommandsTests
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IConfigFileService _configFileService;
|
||||
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
||||
private readonly Option<string?> _configPathOption;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly Option<bool> _quietOption;
|
||||
|
||||
public ConfigCommandsTests()
|
||||
{
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
_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 CreateShowCommand_ReturnsCommandWithSubcommands()
|
||||
{
|
||||
// Act
|
||||
var command = ConfigCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("show");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "datasync");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "dataaccess");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "auth");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "ldap");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "search");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "excelexport");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "securestore");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "connections");
|
||||
command.Subcommands.ShouldContain(c => c.Name == "all");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateShowCommand_HasJsonOption()
|
||||
{
|
||||
// Act
|
||||
var command = ConfigCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.Options.ShouldContain(o => o.Name == "json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowDataSyncCommand_WithNoConfigFolder_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(null));
|
||||
|
||||
var command = ConfigCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Act
|
||||
var exitCode = await rootCommand.InvokeAsync(["show", "datasync"]);
|
||||
|
||||
// Assert - command completes without throwing
|
||||
command.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataSyncSubcommand_Exists()
|
||||
{
|
||||
// Arrange
|
||||
var command = ConfigCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Act
|
||||
var datasyncSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "datasync");
|
||||
|
||||
// Assert
|
||||
datasyncSubcommand.ShouldNotBeNull();
|
||||
datasyncSubcommand.Description.ShouldBe("Show DataSync configuration");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowConnectionsCommand_MasksNoPasswords()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
|
||||
try
|
||||
{
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel
|
||||
{
|
||||
ConnectionStrings = new ConnectionStringsSection
|
||||
{
|
||||
Entries =
|
||||
[
|
||||
new ConnectionStringEntry
|
||||
{
|
||||
Name = "TestConnection",
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "localhost",
|
||||
Password = "secret123"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
var command = ConfigCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console output
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await rootCommand.InvokeAsync(["show", "connections"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("TestConnection");
|
||||
output.ShouldNotContain("secret123"); // Password should not be visible
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllSubcommand_HasJsonOption()
|
||||
{
|
||||
// Arrange
|
||||
var command = ConfigCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Act
|
||||
var allSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "all");
|
||||
|
||||
// Assert
|
||||
allSubcommand.ShouldNotBeNull();
|
||||
// The json option is on the parent show command
|
||||
command.Options.ShouldContain(o => o.Name == "json");
|
||||
}
|
||||
}
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
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;
|
||||
|
||||
public class ConnectionCommandsTests
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IConfigFileService _configFileService;
|
||||
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
||||
private readonly Option<string?> _configPathOption;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly Option<bool> _quietOption;
|
||||
|
||||
public ConnectionCommandsTests()
|
||||
{
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
_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 = ConnectionCommands.CreateListCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("list");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateShowCommand_ReturnsCommandWithNameArgument()
|
||||
{
|
||||
// Act
|
||||
var command = ConnectionCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("show");
|
||||
command.Arguments.ShouldContain(a => a.Name == "name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateAddCommand_ReturnsCommandWithOptions()
|
||||
{
|
||||
// Act
|
||||
var command = ConnectionCommands.CreateAddCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("add");
|
||||
command.Arguments.ShouldContain(a => a.Name == "name");
|
||||
command.Options.ShouldContain(o => o.Name == "provider");
|
||||
command.Options.ShouldContain(o => o.Name == "server");
|
||||
command.Options.ShouldContain(o => o.Name == "database");
|
||||
command.Options.ShouldContain(o => o.Name == "user");
|
||||
command.Options.ShouldContain(o => o.Name == "password");
|
||||
command.Options.ShouldContain(o => o.Name == "port");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRemoveCommand_ReturnsCommandWithNameArgument()
|
||||
{
|
||||
// Act
|
||||
var command = ConnectionCommands.CreateRemoveCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("remove");
|
||||
command.Arguments.ShouldContain(a => a.Name == "name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCommand_WithNoConfigFolder_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(null));
|
||||
|
||||
var command = ConnectionCommands.CreateListCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Act
|
||||
var exitCode = await rootCommand.InvokeAsync(["list"]);
|
||||
|
||||
// Assert - command completes without throwing
|
||||
command.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListCommand_HasCorrectDescription()
|
||||
{
|
||||
// Arrange & Act
|
||||
var command = ConnectionCommands.CreateListCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.Description.ShouldBe("List all connection names and providers");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowCommand_WithValidConnection_ShowsDetailsWithMaskedPassword()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
|
||||
try
|
||||
{
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel
|
||||
{
|
||||
ConnectionStrings = new ConnectionStringsSection
|
||||
{
|
||||
Entries =
|
||||
[
|
||||
new ConnectionStringEntry
|
||||
{
|
||||
Name = "TestConnection",
|
||||
Provider = ConnectionProvider.SqlServer,
|
||||
Server = "localhost",
|
||||
Database = "TestDb",
|
||||
UserId = "sa",
|
||||
Password = "SuperSecretPassword123!"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
var command = ConnectionCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console output
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await rootCommand.InvokeAsync(["show", "TestConnection"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("TestConnection");
|
||||
output.ShouldContain("SqlServer");
|
||||
output.ShouldContain("localhost");
|
||||
output.ShouldContain("TestDb");
|
||||
output.ShouldContain("********"); // Password should be masked
|
||||
output.ShouldNotContain("SuperSecretPassword123!"); // Actual password should not appear
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowCommand_WithNonExistentConnection_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
|
||||
try
|
||||
{
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
var command = ConnectionCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await rootCommand.InvokeAsync(["show", "NonExistent"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("not found");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddCommand_WithNewConnection_AddsConnection()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
|
||||
try
|
||||
{
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
var command = ConnectionCommands.CreateAddCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console output
|
||||
var originalOut = Console.Out;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await rootCommand.InvokeAsync(["add", "NewConnection", "--server", "localhost", "--database", "TestDb"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("added successfully");
|
||||
await _configFileService.Received(1).SaveAppSettingsAsync(appSettingsPath, Arg.Any<ConfigModel>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddCommand_WithExistingConnection_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
|
||||
try
|
||||
{
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel
|
||||
{
|
||||
ConnectionStrings = new ConnectionStringsSection
|
||||
{
|
||||
Entries =
|
||||
[
|
||||
new ConnectionStringEntry { Name = "ExistingConnection" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
var command = ConnectionCommands.CreateAddCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await rootCommand.InvokeAsync(["add", "ExistingConnection", "--server", "localhost"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("already exists");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveCommand_HasCorrectDescription()
|
||||
{
|
||||
// Arrange & Act
|
||||
var command = ConnectionCommands.CreateRemoveCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.Description.ShouldBe("Remove a connection");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveCommand_WithNonExistentConnection_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
|
||||
File.WriteAllText(appSettingsPath, "{}");
|
||||
|
||||
try
|
||||
{
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(tempDir));
|
||||
|
||||
var config = new ConfigModel();
|
||||
|
||||
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(config));
|
||||
|
||||
var command = ConnectionCommands.CreateRemoveCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
var rootCommand = new RootCommand { command };
|
||||
rootCommand.AddGlobalOption(_configPathOption);
|
||||
rootCommand.AddGlobalOption(_verboseOption);
|
||||
rootCommand.AddGlobalOption(_quietOption);
|
||||
|
||||
// Capture console error output
|
||||
var originalErr = Console.Error;
|
||||
using var writer = new StringWriter();
|
||||
Console.SetError(writer);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await rootCommand.InvokeAsync(["remove", "NonExistent"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("not found");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetError(originalErr);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
using System.CommandLine;
|
||||
using JdeScoping.ConfigManager.Cli.Commands;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
|
||||
|
||||
public class PipelineCommandsTests
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IConfigFileService _configFileService;
|
||||
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
||||
private readonly Option<string?> _configPathOption;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
private readonly Option<bool> _quietOption;
|
||||
|
||||
public PipelineCommandsTests()
|
||||
{
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_configFileService);
|
||||
services.AddSingleton(_autoDiscoveryService);
|
||||
_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 = PipelineCommands.CreateListCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("list");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateShowCommand_ReturnsCommandWithNameArgument()
|
||||
{
|
||||
// Act
|
||||
var command = PipelineCommands.CreateShowCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("show");
|
||||
command.Arguments.ShouldContain(a => a.Name == "name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCreateCommand_ReturnsCommandWithOptions()
|
||||
{
|
||||
// Act
|
||||
var command = PipelineCommands.CreateCreateCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("create");
|
||||
command.Arguments.ShouldContain(a => a.Name == "name");
|
||||
command.Options.ShouldContain(o => o.Name == "enabled");
|
||||
command.Options.ShouldContain(o => o.Name == "manual-only");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateDeleteCommand_ReturnsCommandWithForceOption()
|
||||
{
|
||||
// Act
|
||||
var command = PipelineCommands.CreateDeleteCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("delete");
|
||||
command.Arguments.ShouldContain(a => a.Name == "name");
|
||||
command.Options.ShouldContain(o => o.Name == "force");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEnableCommand_ReturnsCommand()
|
||||
{
|
||||
// Act
|
||||
var command = PipelineCommands.CreateEnableCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("enable");
|
||||
command.Arguments.ShouldContain(a => a.Name == "name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateDisableCommand_ReturnsCommand()
|
||||
{
|
||||
// Act
|
||||
var command = PipelineCommands.CreateDisableCommand(
|
||||
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
|
||||
|
||||
// Assert
|
||||
command.ShouldNotBeNull();
|
||||
command.Name.ShouldBe("disable");
|
||||
command.Arguments.ShouldContain(a => a.Name == "name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCommand_WithNoConfigFolder_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<string?>(null));
|
||||
|
||||
var command = PipelineCommands.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 - exit code is set via Environment.ExitCode
|
||||
// The command should complete without throwing
|
||||
command.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCommand_WithValidPipelines_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
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>
|
||||
{
|
||||
["WorkOrder_Curr"] = new EtlPipelineConfig
|
||||
{
|
||||
Name = "WorkOrder_Curr",
|
||||
IsEnabled = true,
|
||||
MassSyncIntervalMinutes = 10080,
|
||||
DailySyncIntervalMinutes = 1440
|
||||
}
|
||||
};
|
||||
|
||||
_configFileService.LoadAllPipelinesAsync(pipelinesDir, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(pipelines));
|
||||
|
||||
var command = PipelineCommands.CreateListCommand(
|
||||
_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(["list"]);
|
||||
|
||||
// Assert
|
||||
var output = writer.ToString();
|
||||
output.ShouldContain("WorkOrder_Curr");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user