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:
+480
@@ -0,0 +1,480 @@
|
||||
using System.Data;
|
||||
using System.Reflection;
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess.Extensions;
|
||||
using JdeScoping.DataAccess.Models;
|
||||
using JdeScoping.DataAccess.Models.FilterEntries;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.DataAccess.Tests.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for TableValuedParameterExtensions.
|
||||
/// </summary>
|
||||
public sealed class TableValuedParameterExtensionsTests
|
||||
{
|
||||
#region CreateWorkOrderFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkOrderFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkOrderFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.ShouldNotBeNull();
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("WorkOrderNumber").ShouldBeTrue();
|
||||
dataTable.Columns["WorkOrderNumber"]!.DataType.ShouldBe(typeof(long));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkOrderFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkOrderFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.ShouldNotBeNull();
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkOrderFilterParameter_PopulatesCorrectData()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkOrderFilter =
|
||||
[
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 12345 },
|
||||
new WorkOrderFilterEntry { WorkOrderNumber = 67890 }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkOrderFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(2);
|
||||
dataTable.Rows[0]["WorkOrderNumber"].ShouldBe(12345L);
|
||||
dataTable.Rows[1]["WorkOrderNumber"].ShouldBe(67890L);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateItemNumberFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateItemNumberFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemNumberFilter =
|
||||
[
|
||||
new ItemNumberFilterEntry { ItemNumber = "ABC123" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemNumberFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.ShouldNotBeNull();
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
|
||||
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateItemNumberFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemNumberFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemNumberFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateProfitCenterFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateProfitCenterFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ProfitCenterFilter =
|
||||
[
|
||||
new ProfitCenterFilterEntry { Code = "PC001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateProfitCenterFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("Code").ShouldBeTrue();
|
||||
dataTable.Columns["Code"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateProfitCenterFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ProfitCenterFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateProfitCenterFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateWorkCenterFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkCenterFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkCenterFilter =
|
||||
[
|
||||
new WorkCenterFilterEntry { Code = "WC001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkCenterFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("Code").ShouldBeTrue();
|
||||
dataTable.Columns["Code"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateWorkCenterFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
WorkCenterFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateWorkCenterFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateComponentLotFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateComponentLotFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateComponentLotFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(2);
|
||||
dataTable.Columns.Contains("ComponentLotNumber").ShouldBeTrue();
|
||||
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
|
||||
dataTable.Columns["ComponentLotNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateComponentLotFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateComponentLotFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateComponentLotFilterParameter_PopulatesCorrectData()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ComponentLotFilter =
|
||||
[
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" },
|
||||
new ComponentLotFilterEntry { LotNumber = "LOT002", ItemNumber = "ITEM002" }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateComponentLotFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(2);
|
||||
dataTable.Rows[0]["ComponentLotNumber"].ShouldBe("LOT001");
|
||||
dataTable.Rows[0]["ItemNumber"].ShouldBe("ITEM001");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateOperatorFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateOperatorFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
OperatorFilter =
|
||||
[
|
||||
new OperatorFilterEntry { UserId = "USER01", AddressNumber = 123 }
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateOperatorFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(1);
|
||||
dataTable.Columns.Contains("UserName").ShouldBeTrue();
|
||||
dataTable.Columns["UserName"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateOperatorFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
OperatorFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateOperatorFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateItemOperationMisFilterParameter Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateItemOperationMisFilterParameter_ProducesCorrectSchema()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemOperationMisFilter =
|
||||
[
|
||||
new ItemOperationMisFilterEntry
|
||||
{
|
||||
ItemNumber = "ITEM001",
|
||||
OperationNumber = "010",
|
||||
MisNumber = "MIS001",
|
||||
MisRevision = "A"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemOperationMisFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Columns.Count.ShouldBe(4);
|
||||
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
|
||||
dataTable.Columns.Contains("OperationNumber").ShouldBeTrue();
|
||||
dataTable.Columns.Contains("MisNumber").ShouldBeTrue();
|
||||
dataTable.Columns.Contains("MisRevision").ShouldBeTrue();
|
||||
|
||||
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
dataTable.Columns["OperationNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
dataTable.Columns["MisNumber"]!.DataType.ShouldBe(typeof(string));
|
||||
dataTable.Columns["MisRevision"]!.DataType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateItemOperationMisFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemOperationMisFilter = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemOperationMisFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateItemOperationMisFilterParameter_PopulatesCorrectData()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SearchModel
|
||||
{
|
||||
ItemOperationMisFilter =
|
||||
[
|
||||
new ItemOperationMisFilterEntry
|
||||
{
|
||||
ItemNumber = "ITEM001",
|
||||
OperationNumber = "010",
|
||||
MisNumber = "MIS001",
|
||||
MisRevision = "A"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var param = model.CreateItemOperationMisFilterParameter();
|
||||
var dataTable = ExtractDataTable(param);
|
||||
|
||||
// Assert
|
||||
dataTable.Rows.Count.ShouldBe(1);
|
||||
dataTable.Rows[0]["ItemNumber"].ShouldBe("ITEM001");
|
||||
dataTable.Rows[0]["OperationNumber"].ShouldBe("010");
|
||||
dataTable.Rows[0]["MisNumber"].ShouldBe("MIS001");
|
||||
dataTable.Rows[0]["MisRevision"].ShouldBe("A");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the underlying DataTable from a Dapper table-valued parameter.
|
||||
/// Uses reflection to access internal fields across different Dapper versions.
|
||||
/// </summary>
|
||||
private static DataTable ExtractDataTable(SqlMapper.ICustomQueryParameter param)
|
||||
{
|
||||
// The TableValuedParameter wraps a DataTable - try multiple field/property names
|
||||
// across different Dapper versions
|
||||
var type = param.GetType();
|
||||
var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
|
||||
|
||||
// Try field names used in different Dapper versions
|
||||
var fieldNames = new[] { "_table", "table", "Table", "_dataTable", "dataTable" };
|
||||
foreach (var fieldName in fieldNames)
|
||||
{
|
||||
var field = type.GetField(fieldName, bindingFlags);
|
||||
if (field != null && field.FieldType == typeof(DataTable))
|
||||
{
|
||||
var value = field.GetValue(param);
|
||||
if (value is DataTable dt)
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
// Try property names
|
||||
var propertyNames = new[] { "Table", "DataTable", "table", "_table" };
|
||||
foreach (var propName in propertyNames)
|
||||
{
|
||||
var prop = type.GetProperty(propName, bindingFlags);
|
||||
if (prop != null && prop.PropertyType == typeof(DataTable))
|
||||
{
|
||||
var value = prop.GetValue(param);
|
||||
if (value is DataTable dt)
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: scan all fields
|
||||
foreach (var field in type.GetFields(bindingFlags))
|
||||
{
|
||||
if (field.FieldType == typeof(DataTable))
|
||||
{
|
||||
var value = field.GetValue(param);
|
||||
if (value is DataTable dt)
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all properties
|
||||
foreach (var prop in type.GetProperties(bindingFlags))
|
||||
{
|
||||
if (prop.PropertyType == typeof(DataTable))
|
||||
{
|
||||
var value = prop.GetValue(param);
|
||||
if (value is DataTable dt)
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Could not extract DataTable from {type.FullName}. " +
|
||||
$"Fields: {string.Join(", ", type.GetFields(bindingFlags).Select(f => f.Name))}. " +
|
||||
$"Properties: {string.Join(", ", type.GetProperties(bindingFlags).Select(p => p.Name))}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user