26ff8d9b4f
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
272 lines
8.1 KiB
C#
272 lines
8.1 KiB
C#
using System.Data;
|
|
using JdeScoping.DataAccess.Interfaces;
|
|
using JdeScoping.DataSync.Contracts;
|
|
using JdeScoping.DataSync.Exceptions;
|
|
using JdeScoping.DataSync.Models;
|
|
using JdeScoping.DataSync.Services;
|
|
using Microsoft.Data.SqlClient;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
|
|
namespace JdeScoping.DataSync.Tests.Services;
|
|
|
|
public class BulkMergeHelperTests
|
|
{
|
|
private readonly IDbConnectionFactory _connectionFactory;
|
|
private readonly IDataReaderFactory _dataReaderFactory;
|
|
private readonly ISchemaValidator _schemaValidator;
|
|
private readonly ILogger<BulkMergeHelper> _logger;
|
|
private readonly BulkMergeHelper _helper;
|
|
|
|
private class TestEntity
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; } = string.Empty;
|
|
public decimal Amount { get; set; }
|
|
}
|
|
|
|
public BulkMergeHelperTests()
|
|
{
|
|
_connectionFactory = Substitute.For<IDbConnectionFactory>();
|
|
_dataReaderFactory = Substitute.For<IDataReaderFactory>();
|
|
_schemaValidator = Substitute.For<ISchemaValidator>();
|
|
_logger = Substitute.For<ILogger<BulkMergeHelper>>();
|
|
|
|
// Setup default mock returns
|
|
_dataReaderFactory.GetColumnNames<TestEntity>()
|
|
.Returns(new List<string> { "Id", "Name", "Amount" });
|
|
|
|
_helper = new BulkMergeHelper(
|
|
_connectionFactory,
|
|
_dataReaderFactory,
|
|
_schemaValidator,
|
|
_logger);
|
|
}
|
|
|
|
#region Constructor Tests
|
|
|
|
[Fact]
|
|
public void Constructor_NullConnectionFactory_ThrowsArgumentNullException()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new BulkMergeHelper(null!, _dataReaderFactory, _schemaValidator, _logger));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NullDataReaderFactory_ThrowsArgumentNullException()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new BulkMergeHelper(_connectionFactory, null!, _schemaValidator, _logger));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NullSchemaValidator_ThrowsArgumentNullException()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new BulkMergeHelper(_connectionFactory, _dataReaderFactory, null!, _logger));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new BulkMergeHelper(_connectionFactory, _dataReaderFactory, _schemaValidator, null!));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MergeAsync Parameter Validation Tests
|
|
|
|
[Fact]
|
|
public async Task MergeAsync_NullData_ThrowsArgumentNullException()
|
|
{
|
|
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
|
_helper.MergeAsync<TestEntity>(
|
|
null!,
|
|
"TestTable",
|
|
x => x.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MergeAsync_NullDestinationTable_ThrowsArgumentNullException()
|
|
{
|
|
var data = AsyncEnumerable.Empty<TestEntity>();
|
|
|
|
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
|
_helper.MergeAsync(
|
|
data,
|
|
null!,
|
|
x => x.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MergeAsync_EmptyDestinationTable_ThrowsArgumentException()
|
|
{
|
|
var data = AsyncEnumerable.Empty<TestEntity>();
|
|
|
|
await Assert.ThrowsAsync<ArgumentException>(() =>
|
|
_helper.MergeAsync(
|
|
data,
|
|
"",
|
|
x => x.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MergeAsync_NullMatchOn_ThrowsArgumentNullException()
|
|
{
|
|
var data = AsyncEnumerable.Empty<TestEntity>();
|
|
|
|
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
|
_helper.MergeAsync<TestEntity>(
|
|
data,
|
|
"TestTable",
|
|
null!));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Column Expression Tests
|
|
|
|
[Fact]
|
|
public void GetColumnNames_CalledWithMatchOn_ReturnsCorrectColumns()
|
|
{
|
|
// The ExpressionParser is tested separately, this just verifies it's being called
|
|
var columns = ExpressionParser.GetColumnNames<TestEntity>(x => x.Id);
|
|
|
|
Assert.Single(columns);
|
|
Assert.Equal("Id", columns[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetColumnNames_CalledWithMultipleColumns_ReturnsCorrectColumns()
|
|
{
|
|
var columns = ExpressionParser.GetColumnNames<TestEntity>(x => new { x.Id, x.Name });
|
|
|
|
Assert.Equal(2, columns.Count);
|
|
Assert.Equal("Id", columns[0]);
|
|
Assert.Equal("Name", columns[1]);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region TempTableName Generation Tests
|
|
|
|
[Fact]
|
|
public void TempTableName_WithDots_ReplacesWithUnderscores()
|
|
{
|
|
// This is implicitly tested by how temp table names are generated
|
|
var tableName = "dbo.TestTable";
|
|
|
|
// The actual generation happens inside MergeAsync, so we verify the pattern
|
|
var cleaned = tableName.Replace(".", "_").Replace("[", "").Replace("]", "");
|
|
Assert.Equal("dbo_TestTable", cleaned);
|
|
}
|
|
|
|
[Fact]
|
|
public void TempTableName_WithBrackets_RemovesBrackets()
|
|
{
|
|
var tableName = "[dbo].[TestTable]";
|
|
var cleaned = tableName.Replace(".", "_").Replace("[", "").Replace("]", "");
|
|
Assert.Equal("dbo_TestTable", cleaned);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MergeResult Tests
|
|
|
|
[Fact]
|
|
public void MergeResult_TotalRowsAffected_ReturnsSum()
|
|
{
|
|
var result = new MergeResult(100, 60, 40, 10, TimeSpan.FromSeconds(5));
|
|
|
|
Assert.Equal(100, result.TotalRowsAffected);
|
|
}
|
|
|
|
[Fact]
|
|
public void MergeResult_RecordProperties_AreCorrect()
|
|
{
|
|
var elapsed = TimeSpan.FromSeconds(5);
|
|
var result = new MergeResult(100, 60, 40, 10, elapsed);
|
|
|
|
Assert.Equal(100, result.TotalRowsProcessed);
|
|
Assert.Equal(60, result.RowsInserted);
|
|
Assert.Equal(40, result.RowsUpdated);
|
|
Assert.Equal(10, result.BatchCount);
|
|
Assert.Equal(elapsed, result.Elapsed);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MassInsertAsync Tests
|
|
|
|
[Fact]
|
|
public async Task MassInsertAsync_NullData_ThrowsArgumentNullException()
|
|
{
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<ArgumentNullException>(
|
|
() => _helper.MassInsertAsync<TestEntity>(null!, "TestTable"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MassInsertAsync_NullDestination_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var data = AsyncEnumerable.Empty<TestEntity>();
|
|
|
|
// Act & Assert
|
|
// ArgumentException.ThrowIfNullOrWhiteSpace throws ArgumentNullException for null values
|
|
await Assert.ThrowsAsync<ArgumentNullException>(
|
|
() => _helper.MassInsertAsync(data, null!));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MassInsertAsync_EmptyDestination_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
var data = AsyncEnumerable.Empty<TestEntity>();
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<ArgumentException>(
|
|
() => _helper.MassInsertAsync(data, ""));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Exception Tests
|
|
|
|
[Fact]
|
|
public void BulkMergeException_PropertiesAreSet()
|
|
{
|
|
var ex = new BulkMergeException("Test error")
|
|
{
|
|
TableName = "TestTable",
|
|
BatchNumber = 5,
|
|
RowsInBatch = 1000,
|
|
SqlStatement = "MERGE INTO..."
|
|
};
|
|
|
|
Assert.Equal("TestTable", ex.TableName);
|
|
Assert.Equal(5, ex.BatchNumber);
|
|
Assert.Equal(1000, ex.RowsInBatch);
|
|
Assert.Equal("MERGE INTO...", ex.SqlStatement);
|
|
}
|
|
|
|
[Fact]
|
|
public void BulkMergeValidationException_ContainsErrors()
|
|
{
|
|
var errors = new List<ValidationError>
|
|
{
|
|
new(0, "Name", "TooLong", "Exceeds max length"),
|
|
new(1, "Amount", 999999m, "Overflow")
|
|
};
|
|
|
|
var ex = new BulkMergeValidationException("Validation failed", errors);
|
|
|
|
Assert.Equal(2, ex.Errors.Count);
|
|
Assert.Equal("Name", ex.Errors[0].ColumnName);
|
|
Assert.Equal("Amount", ex.Errors[1].ColumnName);
|
|
}
|
|
|
|
#endregion
|
|
}
|