Files
jdescopingtool/NEW/tests/JdeScoping.DataSync.Tests/Services/BulkMergeHelperTests.cs
T
Joseph Doherty 26ff8d9b4f Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
2026-01-02 07:43:29 -05:00

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
}