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.
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user