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
@@ -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);
}
}
}