feat(configmanager): add config set and connection update CLI commands

Add missing CLI commands to match UI capabilities: config set commands for
all configuration sections (datasync, dataaccess, auth, ldap, search,
excelexport) and connection update command. Also adds unit tests for
SecretCommands, ValidateCommand, and TestConnectionCommand.
This commit is contained in:
Joseph Doherty
2026-01-28 15:10:22 -05:00
parent bad0102af1
commit 61694ca50b
11 changed files with 2172 additions and 2 deletions
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class BackupCommandsTests
{
private readonly IServiceProvider _serviceProvider;
@@ -6,11 +6,13 @@ using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class ConfigCommandsTests
{
private readonly IServiceProvider _serviceProvider;
private readonly IConfigFileService _configFileService;
private readonly IAutoDiscoveryService _autoDiscoveryService;
private readonly IBackupService _backupService;
private readonly Option<string?> _configPathOption;
private readonly Option<bool> _verboseOption;
private readonly Option<bool> _quietOption;
@@ -19,10 +21,12 @@ public class ConfigCommandsTests
{
_configFileService = Substitute.For<IConfigFileService>();
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
_backupService = Substitute.For<IBackupService>();
var services = new ServiceCollection();
services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService);
services.AddSingleton(_backupService);
_serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]);
@@ -180,4 +184,322 @@ public class ConfigCommandsTests
// The json option is on the parent show command
command.Options.ShouldContain(o => o.Name == "json");
}
// ===== Config Set Command Tests =====
[Fact]
public void CreateSetCommand_ReturnsCommandWithSubcommands()
{
// Act
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("set");
command.Subcommands.ShouldContain(c => c.Name == "datasync");
command.Subcommands.ShouldContain(c => c.Name == "dataaccess");
command.Subcommands.ShouldContain(c => c.Name == "auth");
command.Subcommands.ShouldContain(c => c.Name == "ldap");
command.Subcommands.ShouldContain(c => c.Name == "search");
command.Subcommands.ShouldContain(c => c.Name == "excelexport");
}
[Fact]
public void SetDataSyncCommand_HasExpectedOptions()
{
// Arrange
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Act
var datasyncSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "datasync");
// Assert
datasyncSubcommand.ShouldNotBeNull();
datasyncSubcommand.Options.ShouldContain(o => o.Name == "enabled");
datasyncSubcommand.Options.ShouldContain(o => o.Name == "check-interval");
datasyncSubcommand.Options.ShouldContain(o => o.Name == "max-parallelism");
datasyncSubcommand.Options.ShouldContain(o => o.Name == "batch-size");
datasyncSubcommand.Options.ShouldContain(o => o.Name == "bulk-copy-batch-size");
datasyncSubcommand.Options.ShouldContain(o => o.Name == "lookback-multiplier");
datasyncSubcommand.Options.ShouldContain(o => o.Name == "purge-retention-days");
datasyncSubcommand.Options.ShouldContain(o => o.Name == "sync-timeout");
}
[Fact]
public void SetDataAccessCommand_HasExpectedOptions()
{
// Arrange
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Act
var dataAccessSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "dataaccess");
// Assert
dataAccessSubcommand.ShouldNotBeNull();
dataAccessSubcommand.Options.ShouldContain(o => o.Name == "default-timeout");
dataAccessSubcommand.Options.ShouldContain(o => o.Name == "lot-usage-timeout");
dataAccessSubcommand.Options.ShouldContain(o => o.Name == "mis-data-timeout");
dataAccessSubcommand.Options.ShouldContain(o => o.Name == "detailed-logging");
dataAccessSubcommand.Options.ShouldContain(o => o.Name == "production-schema");
dataAccessSubcommand.Options.ShouldContain(o => o.Name == "archive-schema");
dataAccessSubcommand.Options.ShouldContain(o => o.Name == "stage-schema");
}
[Fact]
public void SetAuthCommand_HasExpectedOptions()
{
// Arrange
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Act
var authSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "auth");
// Assert
authSubcommand.ShouldNotBeNull();
authSubcommand.Options.ShouldContain(o => o.Name == "cookie-name");
authSubcommand.Options.ShouldContain(o => o.Name == "cookie-expiration");
}
[Fact]
public void SetLdapCommand_HasExpectedOptions()
{
// Arrange
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Act
var ldapSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "ldap");
// Assert
ldapSubcommand.ShouldNotBeNull();
ldapSubcommand.Options.ShouldContain(o => o.Name == "server-urls");
ldapSubcommand.Options.ShouldContain(o => o.Name == "group-dn");
ldapSubcommand.Options.ShouldContain(o => o.Name == "search-base");
ldapSubcommand.Options.ShouldContain(o => o.Name == "connection-timeout");
ldapSubcommand.Options.ShouldContain(o => o.Name == "use-fake-auth");
ldapSubcommand.Options.ShouldContain(o => o.Name == "admin-bypass-users");
}
[Fact]
public void SetSearchCommand_HasExpectedOptions()
{
// Arrange
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Act
var searchSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "search");
// Assert
searchSubcommand.ShouldNotBeNull();
searchSubcommand.Options.ShouldContain(o => o.Name == "max-result-rows");
searchSubcommand.Options.ShouldContain(o => o.Name == "timeout");
searchSubcommand.Options.ShouldContain(o => o.Name == "max-concurrent");
}
[Fact]
public void SetExcelExportCommand_HasExpectedOptions()
{
// Arrange
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Act
var excelExportSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "excelexport");
// Assert
excelExportSubcommand.ShouldNotBeNull();
excelExportSubcommand.Options.ShouldContain(o => o.Name == "max-rows-per-sheet");
excelExportSubcommand.Options.ShouldContain(o => o.Name == "date-format");
excelExportSubcommand.Options.ShouldContain(o => o.Name == "timezone-id");
excelExportSubcommand.Options.ShouldContain(o => o.Name == "debug-write-to-file");
excelExportSubcommand.Options.ShouldContain(o => o.Name == "debug-output-dir");
}
[Fact]
public async Task SetDataSyncCommand_WithNoConfigFolder_ReturnsError()
{
// Arrange
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(null));
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
var exitCode = await rootCommand.InvokeAsync(["set", "datasync", "--enabled", "true"]);
// Assert - command completes without throwing
command.ShouldNotBeNull();
}
[Fact]
public async Task SetDataSyncCommand_WithNoChanges_ReturnsError()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel();
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console error output
var originalErr = Console.Error;
using var writer = new StringWriter();
Console.SetError(writer);
try
{
// Act - run without any options
await rootCommand.InvokeAsync(["set", "datasync"]);
// Assert
var output = writer.ToString();
output.ShouldContain("No changes specified");
}
finally
{
Console.SetError(originalErr);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task SetDataSyncCommand_WithValidChange_UpdatesAndSaves()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel();
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
_backupService.CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(Path.Combine(tempDir, "backup.json")));
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["set", "datasync", "--enabled", "false"]);
// Assert
var output = writer.ToString();
output.ShouldContain("updated successfully");
await _configFileService.Received(1).SaveAppSettingsAsync(appSettingsPath, Arg.Any<ConfigModel>(), Arg.Any<CancellationToken>());
await _backupService.Received(1).CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>());
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task SetSearchCommand_WithValidChange_UpdatesAndSaves()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel();
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
_backupService.CreateBackupAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(Path.Combine(tempDir, "backup.json")));
var command = ConfigCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["set", "search", "--max-result-rows", "50000"]);
// Assert
var output = writer.ToString();
output.ShouldContain("updated successfully");
await _configFileService.Received(1).SaveAppSettingsAsync(
appSettingsPath,
Arg.Is<ConfigModel>(c => c.Search.MaxResultRows == 50000),
Arg.Any<CancellationToken>());
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
}
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class ConnectionCommandsTests
{
private readonly IServiceProvider _serviceProvider;
@@ -416,4 +417,246 @@ public class ConnectionCommandsTests
Directory.Delete(tempDir, true);
}
}
// ===== Connection Update Command Tests =====
[Fact]
public void CreateUpdateCommand_ReturnsCommandWithOptions()
{
// Act
var command = ConnectionCommands.CreateUpdateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("update");
command.Arguments.ShouldContain(a => a.Name == "name");
command.Options.ShouldContain(o => o.Name == "provider");
command.Options.ShouldContain(o => o.Name == "server");
command.Options.ShouldContain(o => o.Name == "database");
command.Options.ShouldContain(o => o.Name == "user");
command.Options.ShouldContain(o => o.Name == "password");
command.Options.ShouldContain(o => o.Name == "port");
command.Options.ShouldContain(o => o.Name == "service-name");
command.Options.ShouldContain(o => o.Name == "raw");
}
[Fact]
public void UpdateCommand_HasCorrectDescription()
{
// Arrange & Act
var command = ConnectionCommands.CreateUpdateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.Description.ShouldBe("Update an existing connection");
}
[Fact]
public async Task UpdateCommand_WithNonExistentConnection_ReturnsError()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel();
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
var command = ConnectionCommands.CreateUpdateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console error output
var originalErr = Console.Error;
using var writer = new StringWriter();
Console.SetError(writer);
try
{
// Act
await rootCommand.InvokeAsync(["update", "NonExistent", "--server", "newhost"]);
// Assert
var output = writer.ToString();
output.ShouldContain("not found");
}
finally
{
Console.SetError(originalErr);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public void UpdateCommand_AllOptionsExist()
{
// Arrange & Act
var command = ConnectionCommands.CreateUpdateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert - Verify all options exist
command.Options.ShouldContain(o => o.Name == "provider");
command.Options.ShouldContain(o => o.Name == "server");
command.Options.ShouldContain(o => o.Name == "database");
command.Options.ShouldContain(o => o.Name == "user");
command.Options.ShouldContain(o => o.Name == "password");
command.Options.ShouldContain(o => o.Name == "port");
command.Options.ShouldContain(o => o.Name == "service-name");
command.Options.ShouldContain(o => o.Name == "raw");
}
[Fact]
public async Task UpdateCommand_WithValidChange_UpdatesAndSaves()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel
{
ConnectionStrings = new ConnectionStringsSection
{
Entries =
[
new ConnectionStringEntry
{
Name = "TestConnection",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "OldDb"
}
]
}
};
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
var command = ConnectionCommands.CreateUpdateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["update", "TestConnection", "--database", "NewDb"]);
// Assert
var output = writer.ToString();
output.ShouldContain("updated successfully");
await _configFileService.Received(1).SaveAppSettingsAsync(appSettingsPath, Arg.Any<ConfigModel>(), Arg.Any<CancellationToken>());
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task UpdateCommand_WithMultipleChanges_UpdatesAllProperties()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel
{
ConnectionStrings = new ConnectionStringsSection
{
Entries =
[
new ConnectionStringEntry
{
Name = "TestConnection",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "OldDb",
UserId = "olduser"
}
]
}
};
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
var command = ConnectionCommands.CreateUpdateCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["update", "TestConnection", "--server", "newhost", "--database", "NewDb", "--user", "newuser"]);
// Assert
var output = writer.ToString();
output.ShouldContain("updated successfully");
// Verify the config was updated
config.ConnectionStrings.Entries[0].Server.ShouldBe("newhost");
config.ConnectionStrings.Entries[0].Database.ShouldBe("NewDb");
config.ConnectionStrings.Entries[0].UserId.ShouldBe("newuser");
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
}
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class PipelineCommandsTests
{
private readonly IServiceProvider _serviceProvider;
@@ -0,0 +1,211 @@
using System.CommandLine;
using JdeScoping.ConfigManager.Cli.Commands;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class SecretCommandsTests
{
private readonly IServiceProvider _serviceProvider;
private readonly IConfigFileService _configFileService;
private readonly IAutoDiscoveryService _autoDiscoveryService;
private readonly ISecureStoreManager _secureStoreManager;
private readonly Option<string?> _configPathOption;
private readonly Option<bool> _verboseOption;
private readonly Option<bool> _quietOption;
public SecretCommandsTests()
{
_configFileService = Substitute.For<IConfigFileService>();
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
_secureStoreManager = Substitute.For<ISecureStoreManager>();
var services = new ServiceCollection();
services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService);
services.AddSingleton(_secureStoreManager);
_serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]);
_verboseOption = new Option<bool>(["--verbose", "-v"]);
_quietOption = new Option<bool>(["--quiet", "-q"]);
}
[Fact]
public void CreateListCommand_ReturnsCommand()
{
// Act
var command = SecretCommands.CreateListCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("list");
command.Description.ShouldBe("List all secret keys");
}
[Fact]
public void CreateGetCommand_ReturnsCommandWithKeyArgument()
{
// Act
var command = SecretCommands.CreateGetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("get");
command.Description.ShouldBe("Get a secret value");
command.Arguments.ShouldContain(a => a.Name == "key");
}
[Fact]
public void CreateSetCommand_ReturnsCommandWithKeyAndValueArguments()
{
// Act
var command = SecretCommands.CreateSetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("set");
command.Description.ShouldBe("Set or update a secret");
command.Arguments.ShouldContain(a => a.Name == "key");
command.Arguments.ShouldContain(a => a.Name == "value");
}
[Fact]
public void CreateRemoveCommand_ReturnsCommandWithKeyArgument()
{
// Act
var command = SecretCommands.CreateRemoveCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("remove");
command.Description.ShouldBe("Remove a secret");
command.Arguments.ShouldContain(a => a.Name == "key");
}
[Fact]
public void CreateInitCommand_ReturnsCommandWithOptions()
{
// Act
var command = SecretCommands.CreateInitCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("init");
command.Description.ShouldBe("Initialize a new SecureStore");
command.Options.ShouldContain(o => o.Name == "store");
command.Options.ShouldContain(o => o.Name == "key");
}
[Fact]
public async Task ListCommand_WithNoConfigFolder_ReturnsError()
{
// Arrange
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(null));
var command = SecretCommands.CreateListCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
var exitCode = await rootCommand.InvokeAsync(["list"]);
// Assert - command completes without throwing
command.ShouldNotBeNull();
}
[Fact]
public async Task GetCommand_WithMissingKey_ReturnsError()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
var storePath = Path.Combine(tempDir, "data", "secrets.json");
var keyPath = Path.Combine(tempDir, "data", "secrets.key");
Directory.CreateDirectory(Path.Combine(tempDir, "data"));
File.WriteAllText(storePath, "{}");
File.WriteAllText(keyPath, "test-key");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel
{
SecureStore = new SecureStoreSection
{
StorePath = "data/secrets.json",
KeyFilePath = "data/secrets.key"
}
};
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
_secureStoreManager.When(m => m.GetSecret("nonexistent"))
.Throw(new KeyNotFoundException("Secret 'nonexistent' not found"));
var command = SecretCommands.CreateGetCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console error output
var originalErr = Console.Error;
using var writer = new StringWriter();
Console.SetError(writer);
try
{
// Act
await rootCommand.InvokeAsync(["get", "nonexistent"]);
// Assert
var output = writer.ToString();
output.ShouldContain("not found");
}
finally
{
Console.SetError(originalErr);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public void InitCommand_HasCorrectOptions()
{
// Arrange & Act
var command = SecretCommands.CreateInitCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
var storeOption = command.Options.FirstOrDefault(o => o.Name == "store");
var keyOption = command.Options.FirstOrDefault(o => o.Name == "key");
storeOption.ShouldNotBeNull();
keyOption.ShouldNotBeNull();
}
}
@@ -0,0 +1,294 @@
using System.CommandLine;
using JdeScoping.ConfigManager.Cli.Commands;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class TestConnectionCommandTests
{
private readonly IServiceProvider _serviceProvider;
private readonly IConfigFileService _configFileService;
private readonly IAutoDiscoveryService _autoDiscoveryService;
private readonly IConnectionTestService _connectionTestService;
private readonly Option<string?> _configPathOption;
private readonly Option<bool> _verboseOption;
private readonly Option<bool> _quietOption;
public TestConnectionCommandTests()
{
_configFileService = Substitute.For<IConfigFileService>();
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
_connectionTestService = Substitute.For<IConnectionTestService>();
var services = new ServiceCollection();
services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService);
services.AddSingleton(_connectionTestService);
_serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]);
_verboseOption = new Option<bool>(["--verbose", "-v"]);
_quietOption = new Option<bool>(["--quiet", "-q"]);
}
[Fact]
public void CreateSqlCommand_ReturnsCommand()
{
// Act
var command = TestConnectionCommand.CreateSqlCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("sql");
command.Description.ShouldBe("Test SQL Server connection");
}
[Fact]
public void CreateSqlCommand_HasNameOption()
{
// Act
var command = TestConnectionCommand.CreateSqlCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.Options.ShouldContain(o => o.Name == "name");
}
[Fact]
public void CreateOracleCommand_ReturnsCommand()
{
// Act
var command = TestConnectionCommand.CreateOracleCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("oracle");
command.Description.ShouldBe("Test Oracle connection");
}
[Fact]
public void CreateOracleCommand_HasNameOption()
{
// Act
var command = TestConnectionCommand.CreateOracleCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.Options.ShouldContain(o => o.Name == "name");
}
[Fact]
public void CreateAllCommand_ReturnsCommand()
{
// Act
var command = TestConnectionCommand.CreateAllCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("all");
command.Description.ShouldBe("Test all configured connections");
}
[Fact]
public async Task SqlCommand_WithNoConfigFolder_ReturnsError()
{
// Arrange
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(null));
var command = TestConnectionCommand.CreateSqlCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
var exitCode = await rootCommand.InvokeAsync(["sql"]);
// Assert - command completes without throwing
command.ShouldNotBeNull();
}
[Fact]
public async Task SqlCommand_WithSuccessfulConnection_ReturnsSuccess()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel
{
ConnectionStrings = new ConnectionStringsSection
{
Entries =
[
new ConnectionStringEntry
{
Name = "LocalCache",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
]
}
};
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
_connectionTestService.TestConnectionAsync(
Arg.Any<string>(),
ConnectionProvider.SqlServer,
Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new ConnectionTestResult
{
Success = true,
Duration = TimeSpan.FromMilliseconds(50)
}));
var command = TestConnectionCommand.CreateSqlCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["sql"]);
// Assert
var output = writer.ToString();
output.ShouldContain("Success");
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task SqlCommand_WithFailedConnection_ReturnsError()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel
{
ConnectionStrings = new ConnectionStringsSection
{
Entries =
[
new ConnectionStringEntry
{
Name = "LocalCache",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
]
}
};
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
_connectionTestService.TestConnectionAsync(
Arg.Any<string>(),
ConnectionProvider.SqlServer,
Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new ConnectionTestResult
{
Success = false,
Message = "Connection refused"
}));
var command = TestConnectionCommand.CreateSqlCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console error output
var originalErr = Console.Error;
using var writer = new StringWriter();
Console.SetError(writer);
try
{
// Act
await rootCommand.InvokeAsync(["sql"]);
// Assert
var output = writer.ToString();
output.ShouldContain("Failed");
}
finally
{
Console.SetError(originalErr);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public void SqlCommand_NameOptionExists()
{
// Arrange & Act
var command = TestConnectionCommand.CreateSqlCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
var nameOption = command.Options.FirstOrDefault(o => o.Name == "name");
nameOption.ShouldNotBeNull();
nameOption.Description.ShouldBe("Name of the connection string to test");
}
[Fact]
public void AllCommand_HasCorrectDescription()
{
// Arrange & Act
var command = TestConnectionCommand.CreateAllCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.Description.ShouldBe("Test all configured connections");
}
}
@@ -0,0 +1,298 @@
using System.CommandLine;
using JdeScoping.ConfigManager.Cli.Commands;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.DataSync.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class ValidateCommandTests
{
private readonly IServiceProvider _serviceProvider;
private readonly IConfigFileService _configFileService;
private readonly IAutoDiscoveryService _autoDiscoveryService;
private readonly IValidationService _validationService;
private readonly IRuntimeConfigValidationService _runtimeValidationService;
private readonly Option<string?> _configPathOption;
private readonly Option<bool> _verboseOption;
private readonly Option<bool> _quietOption;
public ValidateCommandTests()
{
_configFileService = Substitute.For<IConfigFileService>();
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
_validationService = Substitute.For<IValidationService>();
_runtimeValidationService = Substitute.For<IRuntimeConfigValidationService>();
var services = new ServiceCollection();
services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService);
services.AddSingleton(_validationService);
services.AddSingleton(_runtimeValidationService);
_serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]);
_verboseOption = new Option<bool>(["--verbose", "-v"]);
_quietOption = new Option<bool>(["--quiet", "-q"]);
}
[Fact]
public void CreateAppSettingsCommand_ReturnsCommand()
{
// Act
var command = ValidateCommand.CreateAppSettingsCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("appsettings");
command.Description.ShouldBe("Validate appsettings.json");
}
[Fact]
public void CreatePipelinesCommand_ReturnsCommand()
{
// Act
var command = ValidateCommand.CreatePipelinesCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("pipelines");
command.Description.ShouldBe("Validate pipeline configuration files");
}
[Fact]
public void CreateAllCommand_ReturnsCommand()
{
// Act
var command = ValidateCommand.CreateAllCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("all");
command.Description.ShouldBe("Validate all configuration files");
}
[Fact]
public void CreateRuntimeCommand_ReturnsCommand()
{
// Act
var command = ValidateCommand.CreateRuntimeCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("runtime");
command.Description.ShouldBe("Run Infrastructure validators");
}
[Fact]
public async Task AppSettingsCommand_WithNoConfigFolder_ReturnsError()
{
// Arrange
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(null));
var command = ValidateCommand.CreateAppSettingsCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
var exitCode = await rootCommand.InvokeAsync(["appsettings"]);
// Assert - command completes without throwing
command.ShouldNotBeNull();
}
[Fact]
public async Task AppSettingsCommand_WithValidConfig_ReturnsSuccess()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel();
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
_validationService.ValidateAppSettings(config)
.Returns(new ValidationResult()); // No errors = IsValid true
var command = ValidateCommand.CreateAppSettingsCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["appsettings"]);
// Assert
var output = writer.ToString();
output.ShouldContain("Valid");
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task AppSettingsCommand_WithInvalidConfig_ReturnsErrors()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var config = new ConfigModel();
_configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(config));
var result = new ValidationResult();
result.Errors.Add("Connection string 'LocalCache' is required");
_validationService.ValidateAppSettings(config).Returns(result);
var command = ValidateCommand.CreateAppSettingsCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console error output
var originalErr = Console.Error;
using var writer = new StringWriter();
Console.SetError(writer);
try
{
// Act
await rootCommand.InvokeAsync(["appsettings"]);
// Assert
var output = writer.ToString();
output.ShouldContain("Error");
}
finally
{
Console.SetError(originalErr);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task PipelinesCommand_WithValidPipelines_ReturnsSuccess()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipelines = new Dictionary<string, EtlPipelineConfig>
{
["TestPipeline"] = new EtlPipelineConfig { Name = "TestPipeline" }
};
_configFileService.LoadAllPipelinesAsync(pipelinesDir, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipelines));
_validationService.ValidatePipelines(pipelines)
.Returns(new ValidationResult()); // No errors = IsValid true
var command = ValidateCommand.CreatePipelinesCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Capture console output
var originalOut = Console.Out;
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
// Act
await rootCommand.InvokeAsync(["pipelines"]);
// Assert
var output = writer.ToString();
output.ShouldContain("Valid");
}
finally
{
Console.SetOut(originalOut);
}
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task RuntimeCommand_WithNoConfigFolder_ReturnsError()
{
// Arrange
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(null));
var command = ValidateCommand.CreateRuntimeCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { command };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
var exitCode = await rootCommand.InvokeAsync(["runtime"]);
// Assert - command completes without throwing
command.ShouldNotBeNull();
}
}
@@ -0,0 +1,10 @@
namespace JdeScoping.ConfigManager.Cli.Tests;
/// <summary>
/// Collection definition for tests that redirect Console.Out or Console.Error.
/// These tests must run sequentially to avoid interference.
/// </summary>
[CollectionDefinition("Console Tests", DisableParallelization = true)]
public class ConsoleTestCollection
{
}