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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user