Files
Joseph Doherty 45f4ecab7d feat(configmanager): add pipeline element management CLI commands
Add commands to manage pipeline elements (pre-scripts, transforms,
post-scripts, source, destination) matching the UI functionality:

- prescript/postscript: add, remove, edit, move-up, move-down
- transform: add, remove, edit, move-up, move-down with type shortcuts
- source: edit (connection, query, mass-query)
- destination: edit (table, match columns, exclude from update)

Features include 1-based indices, --dry-run support, file input for
scripts/queries, and numbered element display in pipeline show output.
2026-01-28 16:08:28 -05:00

984 lines
39 KiB
C#

using System.CommandLine;
using System.Text.Json;
using JdeScoping.ConfigManager.Cli.Commands;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.DataSync.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
[Collection("Console Tests")]
public class PipelineElementCommandsTests
{
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 PipelineElementCommandsTests()
{
_configFileService = Substitute.For<IConfigFileService>();
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
var services = new ServiceCollection();
services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]);
_verboseOption = new Option<bool>(["--verbose", "-v"]);
_quietOption = new Option<bool>(["--quiet", "-q"]);
}
#region Command Structure Tests
[Fact]
public void CreatePreScriptCommand_ReturnsCommandWithSubcommands()
{
// Act
var command = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("prescript");
command.Subcommands.ShouldContain(c => c.Name == "add");
command.Subcommands.ShouldContain(c => c.Name == "remove");
command.Subcommands.ShouldContain(c => c.Name == "edit");
command.Subcommands.ShouldContain(c => c.Name == "move-up");
command.Subcommands.ShouldContain(c => c.Name == "move-down");
}
[Fact]
public void CreatePostScriptCommand_ReturnsCommandWithSubcommands()
{
// Act
var command = PipelineElementCommands.CreatePostScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("postscript");
command.Subcommands.ShouldContain(c => c.Name == "add");
command.Subcommands.ShouldContain(c => c.Name == "remove");
command.Subcommands.ShouldContain(c => c.Name == "edit");
command.Subcommands.ShouldContain(c => c.Name == "move-up");
command.Subcommands.ShouldContain(c => c.Name == "move-down");
}
[Fact]
public void CreateTransformCommand_ReturnsCommandWithSubcommands()
{
// Act
var command = PipelineElementCommands.CreateTransformCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("transform");
command.Subcommands.ShouldContain(c => c.Name == "add");
command.Subcommands.ShouldContain(c => c.Name == "remove");
command.Subcommands.ShouldContain(c => c.Name == "edit");
command.Subcommands.ShouldContain(c => c.Name == "move-up");
command.Subcommands.ShouldContain(c => c.Name == "move-down");
}
[Fact]
public void CreateSourceCommand_ReturnsCommandWithEditSubcommand()
{
// Act
var command = PipelineElementCommands.CreateSourceCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("source");
command.Subcommands.ShouldContain(c => c.Name == "edit");
}
[Fact]
public void CreateDestinationCommand_ReturnsCommandWithEditSubcommand()
{
// Act
var command = PipelineElementCommands.CreateDestinationCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
// Assert
command.ShouldNotBeNull();
command.Name.ShouldBe("destination");
command.Subcommands.ShouldContain(c => c.Name == "edit");
}
[Fact]
public void PreScriptAddCommand_HasRequiredOptions()
{
// Act
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var addCommand = prescript.Subcommands.First(c => c.Name == "add");
// Assert
addCommand.Arguments.ShouldContain(a => a.Name == "pipeline-name");
addCommand.Options.ShouldContain(o => o.Name == "connection");
addCommand.Options.ShouldContain(o => o.Name == "script");
addCommand.Options.ShouldContain(o => o.Name == "script-file");
addCommand.Options.ShouldContain(o => o.Name == "at-index");
}
[Fact]
public void TransformAddCommand_HasRequiredOptions()
{
// Act
var transform = PipelineElementCommands.CreateTransformCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var addCommand = transform.Subcommands.First(c => c.Name == "add");
// Assert
addCommand.Arguments.ShouldContain(a => a.Name == "pipeline-name");
addCommand.Options.ShouldContain(o => o.Name == "type");
addCommand.Options.ShouldContain(o => o.Name == "config");
addCommand.Options.ShouldContain(o => o.Name == "columns");
addCommand.Options.ShouldContain(o => o.Name == "mappings");
addCommand.Options.ShouldContain(o => o.Name == "date-column");
addCommand.Options.ShouldContain(o => o.Name == "time-column");
addCommand.Options.ShouldContain(o => o.Name == "output-column");
addCommand.Options.ShouldContain(o => o.Name == "column");
addCommand.Options.ShouldContain(o => o.Name == "pattern");
addCommand.Options.ShouldContain(o => o.Name == "replacement");
addCommand.Options.ShouldContain(o => o.Name == "ignore-case");
}
[Fact]
public void SourceEditCommand_HasRequiredOptions()
{
// Act
var source = PipelineElementCommands.CreateSourceCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var editCommand = source.Subcommands.First(c => c.Name == "edit");
// Assert
editCommand.Arguments.ShouldContain(a => a.Name == "pipeline-name");
editCommand.Options.ShouldContain(o => o.Name == "connection");
editCommand.Options.ShouldContain(o => o.Name == "query");
editCommand.Options.ShouldContain(o => o.Name == "query-file");
editCommand.Options.ShouldContain(o => o.Name == "mass-query");
editCommand.Options.ShouldContain(o => o.Name == "mass-query-file");
editCommand.Options.ShouldContain(o => o.Name == "dry-run");
}
[Fact]
public void DestinationEditCommand_HasRequiredOptions()
{
// Act
var destination = PipelineElementCommands.CreateDestinationCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var editCommand = destination.Subcommands.First(c => c.Name == "edit");
// Assert
editCommand.Arguments.ShouldContain(a => a.Name == "pipeline-name");
editCommand.Options.ShouldContain(o => o.Name == "table");
editCommand.Options.ShouldContain(o => o.Name == "match-columns");
editCommand.Options.ShouldContain(o => o.Name == "add-match-column");
editCommand.Options.ShouldContain(o => o.Name == "remove-match-column");
editCommand.Options.ShouldContain(o => o.Name == "exclude-from-update");
editCommand.Options.ShouldContain(o => o.Name == "add-exclude");
editCommand.Options.ShouldContain(o => o.Name == "remove-exclude");
editCommand.Options.ShouldContain(o => o.Name == "dry-run");
}
#endregion
#region PreScript Add Tests
[Fact]
public async Task PreScriptAddCommand_AddsScriptToPipeline()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}"); // File must exist for the command
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
PreScripts = []
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { prescript };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
await rootCommand.InvokeAsync(["prescript", "add", "TestPipeline", "--script", "SELECT 1;"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.PreScripts.Count.ShouldBe(1);
savedPipeline.PreScripts[0].Script.ShouldBe("SELECT 1;");
savedPipeline.PreScripts[0].Connection.ShouldBe("lotfinder");
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task PreScriptAddCommand_InsertsAtSpecifiedIndex()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
PreScripts =
[
new ScriptElement { Script = "Script1" },
new ScriptElement { Script = "Script2" }
]
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { prescript };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act - insert at position 2 (between Script1 and Script2)
await rootCommand.InvokeAsync(["prescript", "add", "TestPipeline", "--script", "NewScript", "--at-index", "2"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.PreScripts.Count.ShouldBe(3);
savedPipeline.PreScripts[0].Script.ShouldBe("Script1");
savedPipeline.PreScripts[1].Script.ShouldBe("NewScript");
savedPipeline.PreScripts[2].Script.ShouldBe("Script2");
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region PreScript Remove Tests
[Fact]
public async Task PreScriptRemoveCommand_RemovesScriptWithForce()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
PreScripts =
[
new ScriptElement { Script = "Script1" },
new ScriptElement { Script = "Script2" },
new ScriptElement { Script = "Script3" }
]
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { prescript };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act - remove script at index 2 (Script2)
await rootCommand.InvokeAsync(["prescript", "remove", "TestPipeline", "2", "--force"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.PreScripts.Count.ShouldBe(2);
savedPipeline.PreScripts[0].Script.ShouldBe("Script1");
savedPipeline.PreScripts[1].Script.ShouldBe("Script3");
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task PreScriptRemoveCommand_DryRunDoesNotSave()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
PreScripts = [new ScriptElement { Script = "Script1" }]
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { prescript };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
await rootCommand.InvokeAsync(["prescript", "remove", "TestPipeline", "1", "--dry-run"]);
// Assert - SavePipelineAsync should not be called
await _configFileService.DidNotReceive()
.SavePipelineAsync(Arg.Any<string>(), Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>());
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region PreScript Move Tests
[Fact]
public async Task PreScriptMoveUpCommand_SwapsWithPreviousElement()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
PreScripts =
[
new ScriptElement { Script = "Script1" },
new ScriptElement { Script = "Script2" },
new ScriptElement { Script = "Script3" }
]
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { prescript };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act - move Script2 (index 2) up
await rootCommand.InvokeAsync(["prescript", "move-up", "TestPipeline", "2"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.PreScripts[0].Script.ShouldBe("Script2");
savedPipeline.PreScripts[1].Script.ShouldBe("Script1");
savedPipeline.PreScripts[2].Script.ShouldBe("Script3");
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task PreScriptMoveDownCommand_SwapsWithNextElement()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
PreScripts =
[
new ScriptElement { Script = "Script1" },
new ScriptElement { Script = "Script2" },
new ScriptElement { Script = "Script3" }
]
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { prescript };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act - move Script2 (index 2) down
await rootCommand.InvokeAsync(["prescript", "move-down", "TestPipeline", "2"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.PreScripts[0].Script.ShouldBe("Script1");
savedPipeline.PreScripts[1].Script.ShouldBe("Script3");
savedPipeline.PreScripts[2].Script.ShouldBe("Script2");
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region Transform Add Tests
[Fact]
public async Task TransformAddCommand_AddsColumnDropTransform()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
Transforms = []
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var transform = PipelineElementCommands.CreateTransformCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { transform };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
await rootCommand.InvokeAsync(["transform", "add", "TestPipeline", "--type", "ColumnDrop", "--columns", "Col1,Col2,Col3"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.Transforms.Count.ShouldBe(1);
savedPipeline.Transforms[0].TransformType.ShouldBe("ColumnDrop");
savedPipeline.Transforms[0].Config.ShouldNotBeNull();
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task TransformAddCommand_AddsJdeDateTransform()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
Transforms = []
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var transform = PipelineElementCommands.CreateTransformCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { transform };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
await rootCommand.InvokeAsync(["transform", "add", "TestPipeline",
"--type", "JdeDate",
"--date-column", "WADOCO",
"--time-column", "WATIME",
"--output-column", "UpdatedDT"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.Transforms.Count.ShouldBe(1);
savedPipeline.Transforms[0].TransformType.ShouldBe("JdeDate");
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region Source Edit Tests
[Fact]
public async Task SourceEditCommand_UpdatesConnection()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" }
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var source = PipelineElementCommands.CreateSourceCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { source };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
await rootCommand.InvokeAsync(["source", "edit", "TestPipeline", "--connection", "cms"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.Source.Connection.ShouldBe("cms");
savedPipeline.Source.Query.ShouldBe("SELECT 1"); // Unchanged
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region Destination Edit Tests
[Fact]
public async Task DestinationEditCommand_UpdatesMatchColumns()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
Destination = new DestinationElement
{
Table = "dbo.TestTable",
MatchColumns = ["OldCol1", "OldCol2"]
}
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var destination = PipelineElementCommands.CreateDestinationCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { destination };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
await rootCommand.InvokeAsync(["destination", "edit", "TestPipeline", "--match-columns", "NewCol1,NewCol2,NewCol3"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.Destination.MatchColumns.Count.ShouldBe(3);
savedPipeline.Destination.MatchColumns.ShouldContain("NewCol1");
savedPipeline.Destination.MatchColumns.ShouldContain("NewCol2");
savedPipeline.Destination.MatchColumns.ShouldContain("NewCol3");
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task DestinationEditCommand_AddsMatchColumn()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
EtlPipelineConfig? savedPipeline = null;
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
Destination = new DestinationElement
{
Table = "dbo.TestTable",
MatchColumns = ["Col1", "Col2"]
}
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
_configFileService.SavePipelineAsync(pipelinePath, Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
savedPipeline = ci.ArgAt<EtlPipelineConfig>(1);
return Task.CompletedTask;
});
var destination = PipelineElementCommands.CreateDestinationCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { destination };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act
await rootCommand.InvokeAsync(["destination", "edit", "TestPipeline", "--add-match-column", "Col3"]);
// Assert
savedPipeline.ShouldNotBeNull();
savedPipeline.Destination.MatchColumns.Count.ShouldBe(3);
savedPipeline.Destination.MatchColumns.ShouldContain("Col1");
savedPipeline.Destination.MatchColumns.ShouldContain("Col2");
savedPipeline.Destination.MatchColumns.ShouldContain("Col3");
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region Error Handling Tests
[Fact]
public async Task PreScriptRemoveCommand_InvalidIndex_ReturnsError()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
PreScripts = [new ScriptElement { Script = "Script1" }]
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { prescript };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act - try to remove index 5 when only 1 exists
await rootCommand.InvokeAsync(["prescript", "remove", "TestPipeline", "5", "--force"]);
// Assert - SavePipelineAsync should not be called
await _configFileService.DidNotReceive()
.SavePipelineAsync(Arg.Any<string>(), Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>());
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task TransformAddCommand_InvalidType_ReturnsError()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
Transforms = []
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
var transform = PipelineElementCommands.CreateTransformCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { transform };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act - try to add invalid transform type
await rootCommand.InvokeAsync(["transform", "add", "TestPipeline", "--type", "InvalidType"]);
// Assert - SavePipelineAsync should not be called
await _configFileService.DidNotReceive()
.SavePipelineAsync(Arg.Any<string>(), Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>());
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Fact]
public async Task PreScriptMoveUpCommand_AtFirstPosition_DoesNotSave()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var pipelinesDir = Path.Combine(tempDir, "Pipelines");
Directory.CreateDirectory(pipelinesDir);
var pipelinePath = Path.Combine(pipelinesDir, "pipeline.TestPipeline.json");
File.WriteAllText(pipelinePath, "{}");
try
{
_autoDiscoveryService.FindConfigFolderAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<string?>(tempDir));
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
PreScripts =
[
new ScriptElement { Script = "Script1" },
new ScriptElement { Script = "Script2" }
]
};
_configFileService.LoadPipelineAsync(pipelinePath, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(pipeline));
var prescript = PipelineElementCommands.CreatePreScriptCommand(
_serviceProvider, _configPathOption, _verboseOption, _quietOption);
var rootCommand = new RootCommand { prescript };
rootCommand.AddGlobalOption(_configPathOption);
rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption);
// Act - try to move first element up (should be no-op)
await rootCommand.InvokeAsync(["prescript", "move-up", "TestPipeline", "1"]);
// Assert - SavePipelineAsync should not be called (already at first position)
await _configFileService.DidNotReceive()
.SavePipelineAsync(Arg.Any<string>(), Arg.Any<EtlPipelineConfig>(), Arg.Any<CancellationToken>());
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
}