Files
jdescopingtool/NEW/tests/JdeScoping.DataSync.Tests/Etl/Destinations/DbBulkMergeDestinationTests.cs
T

304 lines
11 KiB
C#

using System.Data;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Etl.Destinations;
using NSubstitute;
namespace JdeScoping.DataSync.Tests.Etl.Destinations;
public class DbBulkMergeDestinationTests
{
/// <summary>
/// This test documents that column mapping is applied to ignore extra source columns.
/// The actual functionality requires a database connection and is an integration test concept.
/// The implementation fetches destination columns from INFORMATION_SCHEMA.COLUMNS
/// and only maps columns that exist in both source and destination.
/// </summary>
[Fact]
public void WriteAsync_SourceHasExtraColumns_IgnoresExtraColumns_IntegrationTestConcept()
{
// This is an integration test concept -
// The actual behavior verifies that column mappings are applied:
// 1. GetDestinationColumnsAsync fetches columns from INFORMATION_SCHEMA.COLUMNS
// 2. ProcessBatchAsync only adds column mappings for columns in destination
// 3. Extra source columns are silently ignored during bulk copy
//
// To test this fully, an integration test with a real database is required.
// The unit test here just verifies the component can be constructed.
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(factory, "TestTable", new[] { "Id" });
Assert.NotNull(dest);
}
[Fact]
public void Constructor_SetsDestinationName()
{
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(factory, "WorkOrder", new[] { "OrderNumber" });
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
}
[Fact]
public void Constructor_NullFactory_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
new DbBulkMergeDestination(null!, "WorkOrder", new[] { "Id" }));
}
[Fact]
public void Constructor_NullTableName_ThrowsArgumentNullException()
{
var factory = Substitute.For<IDbConnectionFactory>();
Assert.Throws<ArgumentNullException>(() =>
new DbBulkMergeDestination(factory, null!, new[] { "Id" }));
}
[Fact]
public void Constructor_EmptyTableName_ThrowsArgumentException()
{
var factory = Substitute.For<IDbConnectionFactory>();
Assert.Throws<ArgumentException>(() =>
new DbBulkMergeDestination(factory, "", new[] { "Id" }));
}
[Fact]
public void Constructor_EmptyMatchColumns_ThrowsArgumentException()
{
var factory = Substitute.For<IDbConnectionFactory>();
Assert.Throws<ArgumentException>(() =>
new DbBulkMergeDestination(factory, "WorkOrder", Array.Empty<string>()));
}
[Fact]
public void Constructor_NullMatchColumns_ThrowsArgumentNullException()
{
var factory = Substitute.For<IDbConnectionFactory>();
Assert.Throws<ArgumentNullException>(() =>
new DbBulkMergeDestination(factory, "WorkOrder", null!));
}
[Fact]
public void Constructor_WithUpdateColumns_Succeeds()
{
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(factory, "WorkOrder",
new[] { "OrderNumber" },
updateColumns: new[] { "Status", "Description" });
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
}
[Fact]
public void Constructor_CustomTimeout_SetsTimeout()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"TestTable",
new[] { "Id" },
commandTimeoutSeconds: 1800);
// Assert - can't directly test private field, but constructor should accept it
Assert.NotNull(dest);
}
[Fact]
public void Constructor_ZeroTimeout_UsesDefault()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"TestTable",
new[] { "Id" },
commandTimeoutSeconds: 0);
// Assert
Assert.NotNull(dest);
}
[Fact]
public void Constructor_WithExcludeFromUpdate_Succeeds()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
excludeFromUpdate: new[] { "CreatedDate", "CreatedBy" });
// Assert
Assert.NotNull(dest);
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
}
[Fact]
public void Constructor_WithUpdateCondition_Succeeds()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
updateCondition: "source.LastUpdate > target.LastUpdate");
// Assert
Assert.NotNull(dest);
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
}
[Fact]
public void Constructor_WithAllNewParameters_Succeeds()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
updateColumns: new[] { "Status", "Description" },
excludeFromUpdate: new[] { "CreatedDate" },
updateCondition: "source.LastUpdate > target.LastUpdate",
batchSize: 5000,
commandTimeoutSeconds: 300);
// Assert
Assert.NotNull(dest);
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
}
[Fact]
public void Constructor_WithNullExcludeFromUpdate_Succeeds()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
updateColumns: null,
excludeFromUpdate: null,
updateCondition: null);
// Assert
Assert.NotNull(dest);
}
[Fact]
public void Constructor_WithEmptyExcludeFromUpdate_Succeeds()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
excludeFromUpdate: Array.Empty<string>());
// Assert
Assert.NotNull(dest);
}
[Fact]
public void Constructor_WithEmptyUpdateCondition_Succeeds()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
updateCondition: "");
// Assert
Assert.NotNull(dest);
}
[Fact]
public void Constructor_WithWhitespaceUpdateCondition_Succeeds()
{
// Arrange & Act
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
updateCondition: " ");
// Assert
Assert.NotNull(dest);
}
/// <summary>
/// Documents that excludeFromUpdate takes precedence over updateColumns.
/// If a column is in both updateColumns and excludeFromUpdate, it should be excluded.
/// This is verified during actual MERGE SQL generation (integration test concept).
/// </summary>
[Fact]
public void Constructor_WithConflictingUpdateAndExclude_Succeeds()
{
// Arrange & Act - excludeFromUpdate takes precedence over updateColumns
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
updateColumns: new[] { "Status", "CreatedDate" },
excludeFromUpdate: new[] { "CreatedDate" });
// Assert - construction succeeds; actual exclusion behavior is in MERGE SQL generation
Assert.NotNull(dest);
}
/// <summary>
/// Documents that updateCondition is applied to the WHEN MATCHED clause.
/// This adds conditional update logic like "source.LastUpdate > target.LastUpdate"
/// to prevent stale data from overwriting newer data.
/// </summary>
[Fact]
public void UpdateCondition_DocumentsUsage_IntegrationTestConcept()
{
// The updateCondition parameter adds a condition to the MERGE statement's WHEN MATCHED clause.
// Example: WHEN MATCHED AND source.LastUpdate > target.LastUpdate THEN UPDATE SET ...
//
// This is useful for:
// - Preventing stale data from overwriting newer data
// - Only updating rows that have actually changed
// - Implementing optimistic concurrency patterns
//
// Full verification requires an integration test with a real database.
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
updateCondition: "source.LastUpdate > target.LastUpdate");
Assert.NotNull(dest);
}
/// <summary>
/// Documents that excludeFromUpdate removes columns from the UPDATE SET clause.
/// This is useful for columns that should only be set on INSERT (e.g., CreatedDate).
/// </summary>
[Fact]
public void ExcludeFromUpdate_DocumentsUsage_IntegrationTestConcept()
{
// The excludeFromUpdate parameter removes specified columns from the UPDATE SET clause.
// These columns are still included in the INSERT clause for new rows.
//
// Common use cases:
// - CreatedDate, CreatedBy - set only on insert, never updated
// - Audit columns that should preserve original values
//
// Full verification requires an integration test with a real database.
var factory = Substitute.For<IDbConnectionFactory>();
var dest = new DbBulkMergeDestination(
factory,
"WorkOrder",
new[] { "OrderNumber" },
excludeFromUpdate: new[] { "CreatedDate", "CreatedBy" });
Assert.NotNull(dest);
}
}