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));
|
secretCommand.AddCommand(SecretCommands.CreateInitCommand(serviceProvider, configPathOption, verboseOption, quietOption));
|
||||||
rootCommand.AddCommand(secretCommand);
|
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);
|
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