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:
Joseph Doherty
2026-01-28 10:40:25 -05:00
parent 1fc7792cd1
commit bad0102af1
10 changed files with 3158 additions and 0 deletions
@@ -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");
}
}
@@ -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);
}
}
}