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,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