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,12 @@
|
||||
namespace JdeScoping.DataSync.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Test entity for BulkMergeHelper integration tests.
|
||||
/// </summary>
|
||||
public class BulkMergeTestEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal? Amount { get; set; }
|
||||
public DateTime LastUpdateDt { get; set; }
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using System.Data;
|
||||
using JdeScoping.DataSync.Generated;
|
||||
|
||||
namespace JdeScoping.DataSync.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// IDataReader implementation for BulkMergeTestEntity.
|
||||
/// </summary>
|
||||
public sealed class BulkMergeTestEntityDataReader : AsyncEnumerableDataReader<BulkMergeTestEntity>
|
||||
{
|
||||
private static readonly string[] _columnNames = ["Id", "Name", "Amount", "LastUpdateDt"];
|
||||
private static readonly Type[] _columnTypes = [typeof(int), typeof(string), typeof(decimal), typeof(DateTime)];
|
||||
|
||||
public BulkMergeTestEntityDataReader(IAsyncEnumerable<BulkMergeTestEntity> source) : base(source) { }
|
||||
|
||||
protected override string[] ColumnNames => _columnNames;
|
||||
|
||||
public static IReadOnlyList<string> GetColumnNames() => _columnNames;
|
||||
|
||||
protected override object GetColumnValue(int ordinal)
|
||||
{
|
||||
var entity = Current!;
|
||||
return ordinal switch
|
||||
{
|
||||
0 => entity.Id,
|
||||
1 => entity.Name,
|
||||
2 => entity.Amount ?? (object)DBNull.Value,
|
||||
3 => entity.LastUpdateDt,
|
||||
_ => throw new IndexOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
protected override Type GetColumnType(int ordinal) => _columnTypes[ordinal];
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Testcontainers.MsSql;
|
||||
|
||||
namespace JdeScoping.DataSync.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Shared fixture that manages the SQL Server Testcontainer lifecycle.
|
||||
/// Container is started once per test collection and shared across all tests.
|
||||
/// </summary>
|
||||
public class SqlServerFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly MsSqlContainer _container;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection string to the test SQL Server instance.
|
||||
/// </summary>
|
||||
public string ConnectionString => _container.GetConnectionString();
|
||||
|
||||
public SqlServerFixture()
|
||||
{
|
||||
_container = new MsSqlBuilder()
|
||||
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
|
||||
.WithPassword("Test@Password123!")
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the container and initializes the test database schema.
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _container.StartAsync();
|
||||
await TestDatabaseInitializer.InitializeAsync(ConnectionString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops and disposes the container.
|
||||
/// </summary>
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _container.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new open connection to the test database.
|
||||
/// Caller is responsible for disposing the connection.
|
||||
/// </summary>
|
||||
public async Task<SqlConnection> CreateConnectionAsync()
|
||||
{
|
||||
var connection = new SqlConnection(ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
return connection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates all test tables to ensure clean state between tests.
|
||||
/// </summary>
|
||||
public async Task CleanupTablesAsync()
|
||||
{
|
||||
await using var connection = await CreateConnectionAsync();
|
||||
await connection.ExecuteAsync(@"
|
||||
TRUNCATE TABLE WorkOrder_Test;
|
||||
TRUNCATE TABLE Item_Test;
|
||||
TRUNCATE TABLE LotUsage_Test;
|
||||
TRUNCATE TABLE DataUpdate_Test;
|
||||
TRUNCATE TABLE BulkMergeTest;
|
||||
");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up just the BulkMergeTest table.
|
||||
/// </summary>
|
||||
public async Task CleanupBulkMergeTestTableAsync()
|
||||
{
|
||||
await using var connection = await CreateConnectionAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE BulkMergeTest;");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection definition for sharing the SQL Server fixture across test classes.
|
||||
/// </summary>
|
||||
[CollectionDefinition("Database")]
|
||||
public class DatabaseCollection : ICollectionFixture<SqlServerFixture>
|
||||
{
|
||||
// This class has no code, and is never created.
|
||||
// Its purpose is to be the place to apply [CollectionDefinition]
|
||||
// and all the ICollectionFixture<> interfaces.
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
namespace JdeScoping.DataSync.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Generates test data for integration tests.
|
||||
/// </summary>
|
||||
public static class TestDataGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a list of WorkOrder entities with sequential IDs.
|
||||
/// </summary>
|
||||
public static List<WorkOrderTestEntity> GenerateWorkOrders(int count, DateTime? baseTime = null)
|
||||
{
|
||||
var time = baseTime ?? DateTime.UtcNow;
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => new WorkOrderTestEntity
|
||||
{
|
||||
OrderNumber = i,
|
||||
Status = i % 2 == 0 ? "Active" : "Closed",
|
||||
Description = $"Work Order {i}",
|
||||
Quantity = i * 10.5m,
|
||||
LastUpdateDT = time.AddMinutes(-i)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates WorkOrders with duplicate primary keys (for deduplication testing).
|
||||
/// Each OrderNumber appears twice with different timestamps.
|
||||
/// </summary>
|
||||
public static List<WorkOrderTestEntity> GenerateWorkOrdersWithDuplicates(int uniqueCount, DateTime baseTime)
|
||||
{
|
||||
var orders = new List<WorkOrderTestEntity>();
|
||||
|
||||
for (var i = 1; i <= uniqueCount; i++)
|
||||
{
|
||||
// Older version
|
||||
orders.Add(new WorkOrderTestEntity
|
||||
{
|
||||
OrderNumber = i,
|
||||
Status = "Old",
|
||||
Description = $"Work Order {i} - Old",
|
||||
Quantity = i * 10m,
|
||||
LastUpdateDT = baseTime.AddHours(-2)
|
||||
});
|
||||
|
||||
// Newer version (should be kept after deduplication)
|
||||
orders.Add(new WorkOrderTestEntity
|
||||
{
|
||||
OrderNumber = i,
|
||||
Status = "New",
|
||||
Description = $"Work Order {i} - New",
|
||||
Quantity = i * 20m,
|
||||
LastUpdateDT = baseTime
|
||||
});
|
||||
}
|
||||
|
||||
return orders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates Item entities (no LastUpdateDT column).
|
||||
/// </summary>
|
||||
public static List<ItemTestEntity> GenerateItems(int count)
|
||||
{
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => new ItemTestEntity
|
||||
{
|
||||
ItemNumber = $"ITEM{i:D6}",
|
||||
Description = $"Item {i}",
|
||||
UnitOfMeasure = i % 3 == 0 ? "EA" : (i % 3 == 1 ? "KG" : "LB")
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates LotUsage entities with composite primary key.
|
||||
/// </summary>
|
||||
public static List<LotUsageTestEntity> GenerateLotUsages(int count, DateTime? baseTime = null)
|
||||
{
|
||||
var time = baseTime ?? DateTime.UtcNow;
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => new LotUsageTestEntity
|
||||
{
|
||||
LotNumber = $"LOT{i:D6}",
|
||||
OrderNumber = (i % 10) + 1, // Reuse order numbers
|
||||
Quantity = i * 5.25m,
|
||||
LastUpdateDT = time.AddMinutes(-i)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a large dataset for batching tests.
|
||||
/// </summary>
|
||||
public static List<WorkOrderTestEntity> GenerateLargeDataset(int count)
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => new WorkOrderTestEntity
|
||||
{
|
||||
OrderNumber = i,
|
||||
Status = "Active",
|
||||
Description = $"WO-{i}",
|
||||
Quantity = i,
|
||||
LastUpdateDT = time
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test entity matching WorkOrder_Test table schema.
|
||||
/// </summary>
|
||||
public class WorkOrderTestEntity
|
||||
{
|
||||
public int OrderNumber { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public decimal? Quantity { get; set; }
|
||||
public DateTime LastUpdateDT { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test entity matching Item_Test table schema (no LastUpdateDT).
|
||||
/// </summary>
|
||||
public class ItemTestEntity
|
||||
{
|
||||
public string ItemNumber { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string? UnitOfMeasure { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test entity matching LotUsage_Test table schema (composite PK).
|
||||
/// </summary>
|
||||
public class LotUsageTestEntity
|
||||
{
|
||||
public string LotNumber { get; set; } = string.Empty;
|
||||
public int OrderNumber { get; set; }
|
||||
public decimal? Quantity { get; set; }
|
||||
public DateTime LastUpdateDT { get; set; }
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
using System.Data;
|
||||
using JdeScoping.DataSync.Contracts;
|
||||
using JdeScoping.DataSync.Generated;
|
||||
|
||||
namespace JdeScoping.DataSync.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// DataReaderFactory for integration tests that supports both test entities and production entities.
|
||||
/// </summary>
|
||||
public class TestDataReaderFactory : IDataReaderFactory
|
||||
{
|
||||
private readonly DataReaderFactory _innerFactory = new();
|
||||
|
||||
public IDataReader CreateReader<T>(IAsyncEnumerable<T> source) where T : class
|
||||
{
|
||||
// Handle test entity
|
||||
if (typeof(T) == typeof(BulkMergeTestEntity))
|
||||
{
|
||||
return new BulkMergeTestEntityDataReader((IAsyncEnumerable<BulkMergeTestEntity>)(object)source);
|
||||
}
|
||||
|
||||
// Delegate to production factory for other types
|
||||
return _innerFactory.CreateReader(source);
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetColumnNames<T>() where T : class
|
||||
{
|
||||
// Handle test entity
|
||||
if (typeof(T) == typeof(BulkMergeTestEntity))
|
||||
{
|
||||
return BulkMergeTestEntityDataReader.GetColumnNames();
|
||||
}
|
||||
|
||||
// Delegate to production factory for other types
|
||||
return _innerFactory.GetColumnNames<T>();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
namespace JdeScoping.DataSync.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the test database schema.
|
||||
/// Creates test tables that mirror production schemas.
|
||||
/// </summary>
|
||||
public static class TestDatabaseInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates all test tables in the database.
|
||||
/// </summary>
|
||||
public static async Task InitializeAsync(string connectionString)
|
||||
{
|
||||
await using var connection = new SqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
// WorkOrder_Test: For MERGE and bulk copy tests (has LastUpdateDT)
|
||||
await connection.ExecuteAsync(@"
|
||||
IF OBJECT_ID('WorkOrder_Test', 'U') IS NOT NULL
|
||||
DROP TABLE WorkOrder_Test;
|
||||
|
||||
CREATE TABLE WorkOrder_Test (
|
||||
OrderNumber INT NOT NULL PRIMARY KEY,
|
||||
Status VARCHAR(10) NULL,
|
||||
Description VARCHAR(100) NULL,
|
||||
Quantity DECIMAL(18,4) NULL,
|
||||
LastUpdateDT DATETIME2 NOT NULL
|
||||
);
|
||||
|
||||
CREATE NONCLUSTERED INDEX IX_WorkOrder_Test_Status
|
||||
ON WorkOrder_Test(Status);
|
||||
");
|
||||
|
||||
// Item_Test: For tables WITHOUT LastUpdateDT (unconditional update)
|
||||
await connection.ExecuteAsync(@"
|
||||
IF OBJECT_ID('Item_Test', 'U') IS NOT NULL
|
||||
DROP TABLE Item_Test;
|
||||
|
||||
CREATE TABLE Item_Test (
|
||||
ItemNumber VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
Description VARCHAR(100) NULL,
|
||||
UnitOfMeasure VARCHAR(10) NULL
|
||||
);
|
||||
");
|
||||
|
||||
// LotUsage_Test: For composite primary key tests
|
||||
await connection.ExecuteAsync(@"
|
||||
IF OBJECT_ID('LotUsage_Test', 'U') IS NOT NULL
|
||||
DROP TABLE LotUsage_Test;
|
||||
|
||||
CREATE TABLE LotUsage_Test (
|
||||
LotNumber VARCHAR(30) NOT NULL,
|
||||
OrderNumber INT NOT NULL,
|
||||
Quantity DECIMAL(18,4) NULL,
|
||||
LastUpdateDT DATETIME2 NOT NULL,
|
||||
CONSTRAINT PK_LotUsage_Test PRIMARY KEY (LotNumber, OrderNumber)
|
||||
);
|
||||
");
|
||||
|
||||
// DataUpdate_Test: For update logging tests
|
||||
await connection.ExecuteAsync(@"
|
||||
IF OBJECT_ID('DataUpdate_Test', 'U') IS NOT NULL
|
||||
DROP TABLE DataUpdate_Test;
|
||||
|
||||
CREATE TABLE DataUpdate_Test (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
TableName VARCHAR(50) NOT NULL,
|
||||
SourceSystem VARCHAR(10) NOT NULL,
|
||||
SourceData VARCHAR(50) NOT NULL,
|
||||
UpdateType INT NOT NULL,
|
||||
StartDT DATETIME2 NOT NULL,
|
||||
EndDT DATETIME2 NULL,
|
||||
NumberRecords INT NOT NULL,
|
||||
WasSuccessful BIT NULL
|
||||
);
|
||||
");
|
||||
|
||||
// BulkMergeTest: For BulkMergeHelper integration tests
|
||||
await connection.ExecuteAsync(@"
|
||||
IF OBJECT_ID('BulkMergeTest', 'U') IS NOT NULL
|
||||
DROP TABLE BulkMergeTest;
|
||||
|
||||
CREATE TABLE BulkMergeTest (
|
||||
Id INT NOT NULL PRIMARY KEY,
|
||||
Name NVARCHAR(100) NOT NULL,
|
||||
Amount DECIMAL(18,2) NULL,
|
||||
LastUpdateDt DATETIME2 NOT NULL
|
||||
);
|
||||
");
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using Oracle.ManagedDataAccess.Client;
|
||||
|
||||
namespace JdeScoping.DataSync.IntegrationTests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Connection factory for integration tests that uses the test container connection string.
|
||||
/// </summary>
|
||||
public class TestDbConnectionFactory : IDbConnectionFactory
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public TestDbConnectionFactory(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<SqlConnection> CreateLotFinderConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync(ct);
|
||||
return connection;
|
||||
}
|
||||
|
||||
public Task<OracleConnection> CreateJdeConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
throw new NotImplementedException("JDE connection not supported in integration tests");
|
||||
}
|
||||
|
||||
public Task<OracleConnection> CreateJdeStageConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
throw new NotImplementedException("JDE Stage connection not supported in integration tests");
|
||||
}
|
||||
|
||||
public Task<OracleConnection> CreateCmsConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
throw new NotImplementedException("CMS connection not supported in integration tests");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user