Files
jdescopingtool/NEW/tests/JdeScoping.DataSync.Tests/Services/EtlPipelineFactoryTests.cs
T
Joseph Doherty bb54994f2d feat(datasync): add WithUpdateType to IEtlPipelineBuilder
- Add WithUpdateType(UpdateTypes) method to IEtlPipelineBuilder interface
- Mark existing WithMode(SyncMode) as [Obsolete("Use WithUpdateType instead")]
- Update PipelineBuilder to store UpdateTypes instead of SyncMode
- Add GetEffectiveScheduleConfig method to merge pipeline schedules with defaults
- Add BuildWithSchedules method for new Schedules-based config
- Update validation to support both old SyncModes and new Schedules formats
- Pass ScheduleDefaults from PipelinesRoot to PipelineBuilder
- For Mass mode: use massQuery, apply prePurge/reIndex from schedule config
- For Daily/Hourly: use regular query with date parameters
- Add 8 new tests for WithUpdateType functionality
2026-01-07 01:15:18 -05:00

1013 lines
34 KiB
C#

using JdeScoping.Core.Models.Enums;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Configuration;
using JdeScoping.DataSync.Contracts;
using JdeScoping.DataSync.Etl.Pipeline;
using JdeScoping.DataSync.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Tests.Services;
public class EtlPipelineFactoryTests
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<EtlPipeline> _logger;
public EtlPipelineFactoryTests()
{
_connectionFactory = Substitute.For<IDbConnectionFactory>();
_logger = NullLogger<EtlPipeline>.Instance;
}
#region ForTable Tests
[Fact]
public void ForTable_WithValidTable_ReturnsBuilder()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act
var builder = factory.ForTable("TestTable");
// Assert
builder.ShouldNotBeNull();
builder.ShouldBeAssignableTo<IEtlPipelineBuilder>();
}
[Fact]
public void ForTable_WithUnknownTable_ThrowsInvalidOperationException()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act & Assert
var ex = Should.Throw<InvalidOperationException>(() => factory.ForTable("NonExistentTable"));
ex.Message.ShouldContain("No pipeline configured for table: NonExistentTable");
ex.Message.ShouldContain("TestTable"); // Should list available tables
}
[Fact]
public void ForTable_WithNullTableName_ThrowsArgumentException()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act & Assert
Should.Throw<ArgumentException>(() => factory.ForTable(null!));
}
[Fact]
public void ForTable_WithEmptyTableName_ThrowsArgumentException()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act & Assert
Should.Throw<ArgumentException>(() => factory.ForTable(""));
}
#endregion
#region Builder WithMode Tests
[Fact]
public void Builder_WithMassMode_BuildsPipeline()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("TestTable");
}
[Fact]
public void Builder_WithIncrementalMode_BuildsPipeline()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build();
// Assert
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("TestTable");
}
[Fact]
public void Builder_DefaultMode_IsIncremental()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act - don't call WithMode()
var pipeline = factory.ForTable("TestTable")
.Build();
// Assert - should work because incremental mode is defined
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithUndefinedSyncMode_ThrowsInvalidOperationException()
{
// Arrange - config with only mass mode
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true)
// No incremental mode defined
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
// Act & Assert - validation fails at factory creation
var ex = Should.Throw<InvalidOperationException>(() => CreateFactory(config));
ex.Message.ShouldContain("missing required 'incremental' sync mode");
}
#endregion
#region Builder WithUpdateType Tests
[Fact]
public void Builder_WithUpdateTypesMass_BuildsPipeline()
{
// Arrange
var config = CreateValidConfigWithSchedules();
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithUpdateType(UpdateTypes.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("TestTable");
}
[Fact]
public void Builder_WithUpdateTypesDaily_BuildsPipeline()
{
// Arrange
var config = CreateValidConfigWithSchedules();
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithUpdateType(UpdateTypes.Daily)
.Build();
// Assert
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("TestTable");
}
[Fact]
public void Builder_WithUpdateTypesHourly_BuildsPipeline()
{
// Arrange
var config = CreateValidConfigWithSchedules();
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithUpdateType(UpdateTypes.Hourly)
.Build();
// Assert
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("TestTable");
}
[Fact]
public void Builder_WithUpdateTypesMass_UsesMassQuery()
{
// Arrange - config with massQuery should use it for Mass update type
var config = CreateValidConfigWithSchedules();
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithUpdateType(UpdateTypes.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithUpdateTypesDaily_UsesRegularQuery()
{
// Arrange - Daily should use regular query with date filtering
var config = CreateValidConfigWithSchedules();
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithUpdateType(UpdateTypes.Daily)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithUpdateTypesMass_AppliesPrePurgeFromScheduleConfig()
{
// Arrange - Mass schedule should have prePurge=true from defaults
var config = CreateValidConfigWithSchedules();
var factory = CreateFactory(config);
// Act - should not throw and should include truncate pre-script
var pipeline = factory.ForTable("TestTable")
.WithUpdateType(UpdateTypes.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithUpdateTypesMass_AppliesReIndexFromScheduleConfig()
{
// Arrange - Mass schedule should have reIndex=true from defaults
var config = CreateValidConfigWithSchedules();
var factory = CreateFactory(config);
// Act - should not throw and should include reindex post-script
var pipeline = factory.ForTable("TestTable")
.WithUpdateType(UpdateTypes.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithUpdateTypesHourly_UsesUpdateWhenFromDefaults()
{
// Arrange - Hourly should use updateWhen from defaults
var config = CreateValidConfigWithSchedules();
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithUpdateType(UpdateTypes.Hourly)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
#endregion
#region Builder WithMinimumDate Tests
[Fact]
public void Builder_WithMinimumDate_OverridesConfigOffset()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
var customDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// Act - should not throw even though we're overriding
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.WithMinimumDate(customDate)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithNullMinimumDate_UsesConfigOffset()
{
// Arrange
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act - null minDt means use config offset
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.WithMinimumDate(null)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
#endregion
#region Config Validation Tests
[Fact]
public void Validate_ConfigMissingMassMode_ThrowsInvalidOperationException()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
// Missing mass mode
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
// Act & Assert
var ex = Should.Throw<InvalidOperationException>(() => CreateFactory(config));
ex.Message.ShouldContain("missing required 'mass' sync mode");
}
[Fact]
public void Validate_ConfigWithRuntimeParameter_ThrowsNotSupportedException()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test WHERE Id = @Id",
new Dictionary<string, ParameterConfig>
{
["id"] = new ParameterConfig("@Id", null, "runtime", null)
}),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00"),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
// Act & Assert
var ex = Should.Throw<NotSupportedException>(() => CreateFactory(config));
ex.Message.ShouldContain("runtime parameter source is not yet supported");
}
#endregion
#region Destination Type Tests
[Fact]
public void Builder_MassMode_DefaultsToBulkImport()
{
// Arrange - no destination override
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act - should use bulkImport for mass mode
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_IncrementalMode_DefaultsToBulkMerge()
{
// Arrange - no destination override
var config = CreateValidConfig();
var factory = CreateFactory(config);
// Act - should use bulkMerge for incremental mode
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_ModeWithDestinationOverride_UsesOverride()
{
// Arrange - mass mode with bulkMerge override
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00",
Destination: new DestinationOverride("bulkMerge", null, null)),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act - mass mode should use bulkMerge due to override
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_BulkMergeWithoutMatchColumns_ThrowsInvalidOperationException()
{
// Arrange - bulkMerge needs matchColumns
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", null, null), // No matchColumns!
null,
null)
});
var factory = CreateFactory(config);
// Act & Assert
var ex = Should.Throw<InvalidOperationException>(() =>
factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental) // Uses bulkMerge
.Build());
ex.Message.ShouldContain("matchColumns required for bulkMerge");
}
#endregion
#region Parameter Tests
[Fact]
public void Builder_WithOffsetParameter_CreatesSource()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test WHERE UpdateDt >= @MinDt",
new Dictionary<string, ParameterConfig>
{
["minDt"] = new ParameterConfig("@MinDt", null, "offset", null)
}),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithJdeJulianParameter_CreatesSource()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("jde", "SELECT * FROM Test WHERE UPMJ >= :dateUpdated",
new Dictionary<string, ParameterConfig>
{
["minDt"] = new ParameterConfig(":dateUpdated", "jdeJulian", "offset", null)
}),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithStaticParameter_UsesConfiguredValue()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test WHERE Status = @Status",
new Dictionary<string, ParameterConfig>
{
["status"] = new ParameterConfig("@Status", null, "static", "Active")
}),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithStaticParameterNoValue_ThrowsInvalidOperationException()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test WHERE Status = @Status",
new Dictionary<string, ParameterConfig>
{
["status"] = new ParameterConfig("@Status", null, "static", null) // No value!
}),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act & Assert
var ex = Should.Throw<InvalidOperationException>(() =>
factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build());
ex.Message.ShouldContain("Static parameter '@Status' requires a value");
}
#endregion
#region Script Tests
[Fact]
public void Builder_WithPrePurge_AddsTruncateScript()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithReIndex_AddsRebuildScript()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true, ReIndex: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithPreScripts_AddsConfiguredScripts()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
["EXEC sp_BeforeSync"],
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
[Fact]
public void Builder_WithPostScripts_AddsConfiguredScripts()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
["UPDATE TestTable SET ProcessedFlag = 1 WHERE ProcessedFlag IS NULL"])
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
#endregion
#region Connection Type Tests
[Theory]
[InlineData("jde")]
[InlineData("cms")]
[InlineData("lotfinder")]
public void Builder_WithValidConnectionType_BuildsPipeline(string connectionType)
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig(connectionType, "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
#endregion
#region Settings Tests
[Fact]
public void Factory_WithNullSettings_UsesDefaults()
{
// Arrange - null settings should use defaults
var config = new PipelinesRoot(
null, // Null settings
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
#endregion
#region MinDtOffset Format Tests
[Fact]
public void Builder_WithInvalidMinDtOffsetFormat_ThrowsInvalidOperationException()
{
// Arrange
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true),
["incremental"] = new SyncModeConfig("not-a-valid-timespan") // Invalid!
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act & Assert
var ex = Should.Throw<InvalidOperationException>(() =>
factory.ForTable("TestTable")
.WithMode(SyncMode.Incremental)
.Build());
ex.Message.ShouldContain("Invalid minDtOffset format");
}
[Fact]
public void Builder_WithNullMinDtOffset_DoesNotThrow()
{
// Arrange - null offset should be valid (no date filtering)
var config = new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test", null),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig(null, PrePurge: true), // Null offset
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
var factory = CreateFactory(config);
// Act
var pipeline = factory.ForTable("TestTable")
.WithMode(SyncMode.Mass)
.Build();
// Assert
pipeline.ShouldNotBeNull();
}
#endregion
#region Helper Methods
private PipelinesRoot CreateValidConfig()
{
return new PipelinesRoot(
new PipelineSettings("UTC"),
null,
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test WHERE UpdateDt >= @MinDt",
new Dictionary<string, ParameterConfig>
{
["minDt"] = new ParameterConfig("@MinDt", null, "offset", null)
}),
new Dictionary<string, SyncModeConfig>
{
["mass"] = new SyncModeConfig("-365.00:00:00", PrePurge: true, ReIndex: true),
["incremental"] = new SyncModeConfig("-1.00:00:00")
},
null, // Schedules
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
}
private PipelinesRoot CreateValidConfigWithSchedules()
{
return new PipelinesRoot(
new PipelineSettings("UTC"),
new ScheduleDefaults(),
new Dictionary<string, PipelineConfig>
{
["TestTable"] = new PipelineConfig(
new SourceConfig("lotfinder", "SELECT * FROM Test WHERE UpdateDt >= @MinDt",
new Dictionary<string, ParameterConfig>
{
["minDt"] = new ParameterConfig("@MinDt", null, "offset", null)
},
"SELECT * FROM Test"), // MassQuery
null, // No old SyncModes
new PipelineSchedules
{
Mass = new ScheduleConfig { PrePurge = true, ReIndex = true },
Daily = new ScheduleConfig(),
Hourly = new ScheduleConfig()
},
null, // Transformers
new DestinationConfig("TestTable", ["Id"], null),
null,
null)
});
}
private EtlPipelineFactory CreateFactory(PipelinesRoot config)
{
return new EtlPipelineFactory(_connectionFactory, config, _logger);
}
#endregion
}