refactor: remove unused classes and consolidate ViewModels in Core

Remove 9 unused types from Core (duplicate extension classes, TableSpec, ColumnSpec, LotLocation), move ComponentLotViewModel and OperatorViewModel from Client to Core, and refactor DataSync.Dev to use pipeline-based configuration. Fix Login.razor to use UserInfoDto directly.
This commit is contained in:
Joseph Doherty
2026-01-19 00:13:12 -05:00
parent 80057590f4
commit 7e36bb4225
89 changed files with 1049 additions and 2282 deletions
@@ -1,177 +0,0 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Integration tests for Branch development ETL.
/// Requires: Local SQL Server, CACHED_DB_FILES directory with branch.json.zstd
/// </summary>
public class BranchDevEtlTests : IAsyncLifetime
{
private readonly string _connectionString;
private readonly string _cacheDirectory;
private readonly IDbConnectionFactory _connectionFactory;
public BranchDevEtlTests()
{
// Load configuration
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionString = config.GetConnectionString("LotFinderDB")
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
}
public async Task InitializeAsync()
{
// Ensure Branch table is empty before test
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await connection.ExecuteAsync("TRUNCATE TABLE dbo.Branch");
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public void Create_ReturnsValidPipeline()
{
// Arrange
var cacheFilePath = Path.Combine(_cacheDirectory, BranchDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath))
{
// Skip test if cache file doesn't exist
return;
}
// Act
var pipeline = BranchDevEtl.Create(_connectionFactory, cacheFilePath);
// Assert
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("Branch_Dev");
}
[Fact]
public async Task Execute_LoadsBranchData()
{
// Arrange
var cacheFilePath = Path.Combine(_cacheDirectory, BranchDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath))
{
// Skip test if cache file doesn't exist
return;
}
var pipeline = BranchDevEtl.Create(_connectionFactory, cacheFilePath);
// Act
var result = await pipeline.ExecuteAsync();
// Assert
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row");
// Verify data in database
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.Branch");
count.ShouldBe((int)result.TotalRows, "Database row count should match pipeline result");
}
[Fact]
public async Task Registry_RunAsync_LoadsBranch()
{
// Arrange
if (!Directory.Exists(_cacheDirectory))
{
// Skip test if cache directory doesn't exist
return;
}
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
// Act
var result = await registry.RunAsync("Branch");
// Assert
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
}
[Fact]
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
{
// Arrange
var cacheFilePath = Path.Combine(_cacheDirectory, BranchDevEtl.CacheFileName);
// Act & Assert
Should.Throw<ArgumentNullException>(() => BranchDevEtl.Create(null!, cacheFilePath));
}
[Fact]
public void Create_WithEmptyCacheFilePath_ThrowsArgumentException()
{
// Arrange
var mockFactory = Substitute.For<IDbConnectionFactory>();
// Act & Assert
Should.Throw<ArgumentException>(() => BranchDevEtl.Create(mockFactory, string.Empty));
}
[Fact]
public void Create_WithNonExistentCacheFile_ThrowsFileNotFoundException()
{
// Arrange
var mockFactory = Substitute.For<IDbConnectionFactory>();
var nonExistentPath = "/nonexistent/path/branch.json.zstd";
// Act & Assert
Should.Throw<FileNotFoundException>(() => BranchDevEtl.Create(mockFactory, nonExistentPath));
}
[Fact]
public void DevEtlRegistry_WithNonExistentCacheDirectory_ThrowsDirectoryNotFoundException()
{
// Arrange
var mockFactory = Substitute.For<IDbConnectionFactory>();
var nonExistentPath = "/nonexistent/cache/directory";
// Act & Assert
Should.Throw<DirectoryNotFoundException>(() => new DevEtlRegistry(mockFactory, nonExistentPath));
}
[Fact]
public void DevEtlRegistry_GetAvailableTables_IncludesBranch()
{
// Arrange
if (!Directory.Exists(_cacheDirectory))
{
return;
}
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
// Act
var tables = registry.GetAvailableTables().ToList();
// Assert
tables.ShouldContain("Branch");
}
}
@@ -0,0 +1,210 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev.Configuration;
using JdeScoping.DataSync.Dev.Contracts;
using JdeScoping.DataSync.Dev.Options;
using JdeScoping.DataSync.Dev.Services;
using JdeScoping.DataSync.Etl.Pipeline;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Tests for DevEtlPipelineFactory.
/// </summary>
public class DevEtlPipelineFactoryTests
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<EtlPipeline> _logger;
private readonly string _cacheDirectory;
public DevEtlPipelineFactoryTests()
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
_logger = NullLogger<EtlPipeline>.Instance;
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
}
[Fact]
public void Constructor_WithValidConfig_LoadsPipelines()
{
// Arrange
var pipelines = new Dictionary<string, DevPipelineConfig>
{
["TestTable"] = new(new DevSourceConfig("test.pb.zstd"), new DevDestinationConfig("TestTable"))
};
var config = new DevPipelinesRoot(null, pipelines);
// Act
var factory = new DevEtlPipelineFactory(_connectionFactory, config, _logger);
// Assert
factory.GetAvailableTables().ShouldContain("TestTable");
}
[Fact]
public void GetAvailableTables_Returns21Tables()
{
// Arrange
var factory = CreateFactoryFromConfig();
// Act
var tables = factory.GetAvailableTables().ToList();
// Assert
tables.Count.ShouldBe(21);
tables.ShouldContain("Branch");
tables.ShouldContain("WorkOrder_Curr");
tables.ShouldContain("LotUsage_Curr");
}
[Fact]
public void IsVeryLargeTable_ReturnsTrueForVeryLargeTables()
{
// Arrange
var factory = CreateFactoryFromConfig();
// Act & Assert
factory.IsVeryLargeTable("WorkOrderTime_Curr").ShouldBeTrue();
factory.IsVeryLargeTable("WorkOrderStep_Curr").ShouldBeTrue();
factory.IsVeryLargeTable("LotUsage_Curr").ShouldBeTrue();
}
[Fact]
public void IsVeryLargeTable_ReturnsFalseForSmallTables()
{
// Arrange
var factory = CreateFactoryFromConfig();
// Act & Assert
factory.IsVeryLargeTable("Branch").ShouldBeFalse();
factory.IsVeryLargeTable("Item").ShouldBeFalse();
factory.IsVeryLargeTable("JdeUser").ShouldBeFalse();
}
[Fact]
public void GetPipeline_WithValidTable_ReturnsPipeline()
{
// Arrange
var factory = CreateFactoryFromConfig();
// Skip if cache directory doesn't exist
if (!Directory.Exists(_cacheDirectory))
return;
var cacheFilePath = Path.Combine(_cacheDirectory, "branch.pb.zstd");
if (!File.Exists(cacheFilePath))
return;
// Act
var pipeline = factory.GetPipeline("Branch", _cacheDirectory);
// Assert
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("Branch_Dev");
}
[Fact]
public void GetPipeline_WithInvalidTable_ThrowsInvalidOperationException()
{
// Arrange
var factory = CreateFactoryFromConfig();
// Act & Assert
Should.Throw<InvalidOperationException>(() => factory.GetPipeline("NonExistentTable", _cacheDirectory));
}
[Fact]
public void GetPipeline_WithNullTableName_ThrowsArgumentException()
{
// Arrange
var factory = CreateFactoryFromConfig();
// Act & Assert
Should.Throw<ArgumentException>(() => factory.GetPipeline(null!, _cacheDirectory));
}
[Fact]
public void GetPipeline_WithEmptyCacheDirectory_ThrowsArgumentException()
{
// Arrange
var factory = CreateFactoryFromConfig();
// Act & Assert
Should.Throw<ArgumentException>(() => factory.GetPipeline("Branch", string.Empty));
}
[Fact]
public void Constructor_WithNullConnectionFactory_ThrowsArgumentNullException()
{
// Arrange
var config = new DevPipelinesRoot(null, new Dictionary<string, DevPipelineConfig>());
// Act & Assert
Should.Throw<ArgumentNullException>(() => new DevEtlPipelineFactory(null!, config, _logger));
}
[Fact]
public void Constructor_WithNullConfig_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new DevEtlPipelineFactory(_connectionFactory, (DevPipelinesRoot)null!, _logger));
}
private DevEtlPipelineFactory CreateFactoryFromConfig()
{
// Load actual config from JSON file
var settings = new DevPipelineSettings
{
SizeCategories = new SizeCategories
{
Small = ["Branch", "OrgHierarchy", "WorkCenter", "ProfitCenter"],
Medium = ["JdeUser", "FunctionCode", "Item", "RouteMaster"],
Large = ["Lot", "MisData", "WorkOrder_Curr", "WorkOrder_Hist", "LotUsage_Hist", "WorkOrderComponent_Hist"],
VeryLarge = ["WorkOrderStep_Hist", "WorkOrderComponent_Curr", "WorkOrderRouting", "LotUsage_Curr", "WorkOrderStep_Curr", "WorkOrderTime_Hist", "WorkOrderTime_Curr"]
}
};
var pipelines = new Dictionary<string, DevPipelineConfig>
{
["Branch"] = new(new DevSourceConfig("branch.pb.zstd"), new DevDestinationConfig("Branch")),
["OrgHierarchy"] = new(new DevSourceConfig("orghierarchy.pb.zstd"), new DevDestinationConfig("OrgHierarchy")),
["WorkCenter"] = new(new DevSourceConfig("workcenter.pb.zstd"), new DevDestinationConfig("WorkCenter")),
["ProfitCenter"] = new(new DevSourceConfig("profitcenter.pb.zstd"), new DevDestinationConfig("ProfitCenter")),
["JdeUser"] = new(new DevSourceConfig("jdeuser.pb.zstd"), new DevDestinationConfig("JdeUser")),
["FunctionCode"] = new(new DevSourceConfig("functioncode.pb.zstd"), new DevDestinationConfig("FunctionCode")),
["Item"] = new(new DevSourceConfig("item.pb.zstd"), new DevDestinationConfig("Item")),
["RouteMaster"] = new(new DevSourceConfig("routemaster.pb.zstd"), new DevDestinationConfig("RouteMaster")),
["Lot"] = new(new DevSourceConfig("lot.pb.zstd"), new DevDestinationConfig("Lot")),
["MisData"] = new(new DevSourceConfig("misdata.pb.zstd"), new DevDestinationConfig("MisData")),
["WorkOrder_Curr"] = new(new DevSourceConfig("workorder_curr.pb.zstd"), new DevDestinationConfig("WorkOrder_Curr")),
["WorkOrder_Hist"] = new(new DevSourceConfig("workorder_hist.pb.zstd"), new DevDestinationConfig("WorkOrder_Hist")),
["LotUsage_Curr"] = new(new DevSourceConfig("lotusage_curr.pb.zstd"), new DevDestinationConfig("LotUsage_Curr")),
["LotUsage_Hist"] = new(new DevSourceConfig("lotusage_hist.pb.zstd"), new DevDestinationConfig("LotUsage_Hist")),
["WorkOrderComponent_Curr"] = new(new DevSourceConfig("workordercomponent_curr.pb.zstd"), new DevDestinationConfig("WorkOrderComponent_Curr")),
["WorkOrderComponent_Hist"] = new(new DevSourceConfig("workordercomponent_hist.pb.zstd"), new DevDestinationConfig("WorkOrderComponent_Hist")),
["WorkOrderStep_Curr"] = new(new DevSourceConfig("workorderstep_curr.pb.zstd"), new DevDestinationConfig("WorkOrderStep_Curr")),
["WorkOrderStep_Hist"] = new(new DevSourceConfig("workorderstep_hist.pb.zstd"), new DevDestinationConfig("WorkOrderStep_Hist")),
["WorkOrderTime_Curr"] = new(new DevSourceConfig("workordertime_curr.pb.zstd"), new DevDestinationConfig("WorkOrderTime_Curr")),
["WorkOrderTime_Hist"] = new(new DevSourceConfig("workordertime_hist.pb.zstd"), new DevDestinationConfig("WorkOrderTime_Hist")),
["WorkOrderRouting"] = new(new DevSourceConfig("workorderrouting.pb.zstd"), new DevDestinationConfig("WorkOrderRouting"))
};
var config = new DevPipelinesRoot(settings, pipelines);
return new DevEtlPipelineFactory(_connectionFactory, config, _logger);
}
}
@@ -0,0 +1,101 @@
using JdeScoping.DataSync.Dev.Configuration;
using JdeScoping.DataSync.Dev.Contracts;
using JdeScoping.DataSync.Dev.Services;
using JdeScoping.DataSync.Etl.Pipeline;
using JdeScoping.DataSync.Etl.Results;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Tests for DevEtlRegistry.
/// </summary>
public class DevEtlRegistryTests : IDisposable
{
private readonly IDevEtlPipelineFactory _mockFactory;
private readonly ILogger<DevEtlRegistry> _logger;
private readonly string _tempDirectory;
public DevEtlRegistryTests()
{
_mockFactory = Substitute.For<IDevEtlPipelineFactory>();
_logger = NullLogger<DevEtlRegistry>.Instance;
// Create a temp directory for tests
_tempDirectory = Path.Combine(Path.GetTempPath(), $"DevEtlRegistryTests_{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDirectory);
}
[Fact]
public void Constructor_WithValidArguments_Succeeds()
{
// Arrange
_mockFactory.GetAvailableTables().Returns(new[] { "Branch" });
// Act
var registry = new DevEtlRegistry(_mockFactory, _tempDirectory, _logger);
// Assert
registry.ShouldNotBeNull();
}
[Fact]
public void Constructor_WithNullFactory_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new DevEtlRegistry(null!, _tempDirectory, _logger));
}
[Fact]
public void Constructor_WithEmptyCacheDirectory_ThrowsArgumentException()
{
// Act & Assert
Should.Throw<ArgumentException>(() => new DevEtlRegistry(_mockFactory, string.Empty, _logger));
}
[Fact]
public void Constructor_WithNonExistentCacheDirectory_ThrowsDirectoryNotFoundException()
{
// Arrange
var nonExistentPath = "/nonexistent/cache/directory";
// Act & Assert
Should.Throw<DirectoryNotFoundException>(() => new DevEtlRegistry(_mockFactory, nonExistentPath, _logger));
}
[Fact]
public void GetAvailableTables_DelegatesToFactory()
{
// Arrange
var expectedTables = new[] { "Branch", "Item", "Lot" };
_mockFactory.GetAvailableTables().Returns(expectedTables);
var registry = new DevEtlRegistry(_mockFactory, _tempDirectory, _logger);
// Act
var tables = registry.GetAvailableTables().ToList();
// Assert
tables.ShouldBe(expectedTables);
_mockFactory.Received(1).GetAvailableTables();
}
public void Dispose()
{
// Cleanup temp directory
if (Directory.Exists(_tempDirectory))
{
try
{
Directory.Delete(_tempDirectory, recursive: true);
}
catch
{
// Ignore cleanup errors
}
}
}
}
@@ -1,96 +0,0 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Integration tests for FunctionCode development ETL.
/// </summary>
public class FunctionCodeDevEtlTests : IAsyncLifetime
{
private readonly string _connectionString;
private readonly string _cacheDirectory;
private readonly IDbConnectionFactory _connectionFactory;
public FunctionCodeDevEtlTests()
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionString = config.GetConnectionString("LotFinderDB")
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
}
public async Task InitializeAsync()
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await connection.ExecuteAsync("TRUNCATE TABLE dbo.FunctionCode");
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public void Create_ReturnsValidPipeline()
{
var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = FunctionCodeDevEtl.Create(_connectionFactory, cacheFilePath);
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("FunctionCode_Dev");
}
[Fact]
public async Task Execute_LoadsData()
{
var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = FunctionCodeDevEtl.Create(_connectionFactory, cacheFilePath);
var result = await pipeline.ExecuteAsync();
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.FunctionCode");
count.ShouldBe((int)result.TotalRows);
}
[Fact]
public async Task Registry_RunAsync_LoadsTable()
{
if (!Directory.Exists(_cacheDirectory)) return;
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
var result = await registry.RunAsync("FunctionCode");
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
}
[Fact]
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
{
var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName);
Should.Throw<ArgumentNullException>(() => FunctionCodeDevEtl.Create(null!, cacheFilePath));
}
}
@@ -1,96 +0,0 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Integration tests for Item development ETL.
/// </summary>
public class ItemDevEtlTests : IAsyncLifetime
{
private readonly string _connectionString;
private readonly string _cacheDirectory;
private readonly IDbConnectionFactory _connectionFactory;
public ItemDevEtlTests()
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionString = config.GetConnectionString("LotFinderDB")
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
}
public async Task InitializeAsync()
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await connection.ExecuteAsync("TRUNCATE TABLE dbo.Item");
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public void Create_ReturnsValidPipeline()
{
var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = ItemDevEtl.Create(_connectionFactory, cacheFilePath);
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("Item_Dev");
}
[Fact]
public async Task Execute_LoadsData()
{
var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = ItemDevEtl.Create(_connectionFactory, cacheFilePath);
var result = await pipeline.ExecuteAsync();
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.Item");
count.ShouldBe((int)result.TotalRows);
}
[Fact]
public async Task Registry_RunAsync_LoadsTable()
{
if (!Directory.Exists(_cacheDirectory)) return;
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
var result = await registry.RunAsync("Item");
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
}
[Fact]
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
{
var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName);
Should.Throw<ArgumentNullException>(() => ItemDevEtl.Create(null!, cacheFilePath));
}
}
@@ -1,96 +0,0 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Integration tests for JdeUser development ETL.
/// </summary>
public class JdeUserDevEtlTests : IAsyncLifetime
{
private readonly string _connectionString;
private readonly string _cacheDirectory;
private readonly IDbConnectionFactory _connectionFactory;
public JdeUserDevEtlTests()
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionString = config.GetConnectionString("LotFinderDB")
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
}
public async Task InitializeAsync()
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await connection.ExecuteAsync("TRUNCATE TABLE dbo.JdeUser");
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public void Create_ReturnsValidPipeline()
{
var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = JdeUserDevEtl.Create(_connectionFactory, cacheFilePath);
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("JdeUser_Dev");
}
[Fact]
public async Task Execute_LoadsData()
{
var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = JdeUserDevEtl.Create(_connectionFactory, cacheFilePath);
var result = await pipeline.ExecuteAsync();
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.JdeUser");
count.ShouldBe((int)result.TotalRows);
}
[Fact]
public async Task Registry_RunAsync_LoadsTable()
{
if (!Directory.Exists(_cacheDirectory)) return;
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
var result = await registry.RunAsync("JdeUser");
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
}
[Fact]
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
{
var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName);
Should.Throw<ArgumentNullException>(() => JdeUserDevEtl.Create(null!, cacheFilePath));
}
}
@@ -1,166 +0,0 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Integration tests for OrgHierarchy development ETL.
/// Requires: Local SQL Server, CACHED_DB_FILES directory with orghierarchy.json.zstd
/// </summary>
public class OrgHierarchyDevEtlTests : IAsyncLifetime
{
private readonly string _connectionString;
private readonly string _cacheDirectory;
private readonly IDbConnectionFactory _connectionFactory;
public OrgHierarchyDevEtlTests()
{
// Load configuration
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionString = config.GetConnectionString("LotFinderDB")
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
}
public async Task InitializeAsync()
{
// Ensure OrgHierarchy table is empty before test
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await connection.ExecuteAsync("TRUNCATE TABLE dbo.OrgHierarchy");
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public void Create_ReturnsValidPipeline()
{
// Arrange
var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath))
{
// Skip test if cache file doesn't exist
return;
}
// Act
var pipeline = OrgHierarchyDevEtl.Create(_connectionFactory, cacheFilePath);
// Assert
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("OrgHierarchy_Dev");
}
[Fact]
public async Task Execute_LoadsOrgHierarchyData()
{
// Arrange
var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath))
{
// Skip test if cache file doesn't exist
return;
}
var pipeline = OrgHierarchyDevEtl.Create(_connectionFactory, cacheFilePath);
// Act
var result = await pipeline.ExecuteAsync();
// Assert
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row");
// Verify data in database
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.OrgHierarchy");
count.ShouldBe((int)result.TotalRows, "Database row count should match pipeline result");
}
[Fact]
public async Task Registry_RunAsync_LoadsOrgHierarchy()
{
// Arrange
if (!Directory.Exists(_cacheDirectory))
{
// Skip test if cache directory doesn't exist
return;
}
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
// Act
var result = await registry.RunAsync("OrgHierarchy");
// Assert
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
}
[Fact]
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
{
// Arrange
var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName);
// Act & Assert
Should.Throw<ArgumentNullException>(() => OrgHierarchyDevEtl.Create(null!, cacheFilePath));
}
[Fact]
public void Create_WithEmptyCacheFilePath_ThrowsArgumentException()
{
// Arrange
var mockFactory = Substitute.For<IDbConnectionFactory>();
// Act & Assert
Should.Throw<ArgumentException>(() => OrgHierarchyDevEtl.Create(mockFactory, string.Empty));
}
[Fact]
public void Create_WithNonExistentCacheFile_ThrowsFileNotFoundException()
{
// Arrange
var mockFactory = Substitute.For<IDbConnectionFactory>();
var nonExistentPath = "/nonexistent/path/orghierarchy.json.zstd";
// Act & Assert
Should.Throw<FileNotFoundException>(() => OrgHierarchyDevEtl.Create(mockFactory, nonExistentPath));
}
[Fact]
public void DevEtlRegistry_GetAvailableTables_IncludesOrgHierarchy()
{
// Arrange
if (!Directory.Exists(_cacheDirectory))
{
return;
}
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
// Act
var tables = registry.GetAvailableTables().ToList();
// Assert
tables.ShouldContain("OrgHierarchy");
}
}
@@ -1,117 +0,0 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Integration tests for ProfitCenter development ETL.
/// Requires: Local SQL Server, CACHED_DB_FILES directory with profitcenter.json.zstd
/// </summary>
public class ProfitCenterDevEtlTests : IAsyncLifetime
{
private readonly string _connectionString;
private readonly string _cacheDirectory;
private readonly IDbConnectionFactory _connectionFactory;
public ProfitCenterDevEtlTests()
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionString = config.GetConnectionString("LotFinderDB")
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
}
public async Task InitializeAsync()
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await connection.ExecuteAsync("TRUNCATE TABLE dbo.ProfitCenter");
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public void Create_ReturnsValidPipeline()
{
var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = ProfitCenterDevEtl.Create(_connectionFactory, cacheFilePath);
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("ProfitCenter_Dev");
}
[Fact]
public async Task Execute_LoadsProfitCenterData()
{
var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = ProfitCenterDevEtl.Create(_connectionFactory, cacheFilePath);
var result = await pipeline.ExecuteAsync();
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row");
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.ProfitCenter");
count.ShouldBe((int)result.TotalRows);
}
[Fact]
public async Task Registry_RunAsync_LoadsProfitCenter()
{
if (!Directory.Exists(_cacheDirectory)) return;
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
var result = await registry.RunAsync("ProfitCenter");
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
}
[Fact]
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
{
var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName);
Should.Throw<ArgumentNullException>(() => ProfitCenterDevEtl.Create(null!, cacheFilePath));
}
[Fact]
public void Create_WithEmptyCacheFilePath_ThrowsArgumentException()
{
var mockFactory = Substitute.For<IDbConnectionFactory>();
Should.Throw<ArgumentException>(() => ProfitCenterDevEtl.Create(mockFactory, string.Empty));
}
[Fact]
public void DevEtlRegistry_GetAvailableTables_IncludesProfitCenter()
{
if (!Directory.Exists(_cacheDirectory)) return;
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
var tables = registry.GetAvailableTables().ToList();
tables.ShouldContain("ProfitCenter");
}
}
@@ -1,96 +0,0 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Integration tests for RouteMaster development ETL.
/// </summary>
public class RouteMasterDevEtlTests : IAsyncLifetime
{
private readonly string _connectionString;
private readonly string _cacheDirectory;
private readonly IDbConnectionFactory _connectionFactory;
public RouteMasterDevEtlTests()
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionString = config.GetConnectionString("LotFinderDB")
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
}
public async Task InitializeAsync()
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await connection.ExecuteAsync("TRUNCATE TABLE dbo.RouteMaster");
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public void Create_ReturnsValidPipeline()
{
var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = RouteMasterDevEtl.Create(_connectionFactory, cacheFilePath);
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("RouteMaster_Dev");
}
[Fact]
public async Task Execute_LoadsData()
{
var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = RouteMasterDevEtl.Create(_connectionFactory, cacheFilePath);
var result = await pipeline.ExecuteAsync();
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.RouteMaster");
count.ShouldBe((int)result.TotalRows);
}
[Fact]
public async Task Registry_RunAsync_LoadsTable()
{
if (!Directory.Exists(_cacheDirectory)) return;
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
var result = await registry.RunAsync("RouteMaster");
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
}
[Fact]
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
{
var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName);
Should.Throw<ArgumentNullException>(() => RouteMasterDevEtl.Create(null!, cacheFilePath));
}
}
@@ -1,117 +0,0 @@
using Dapper;
using JdeScoping.DataAccess;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Dev;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Shouldly;
namespace JdeScoping.DataSync.Dev.Tests;
/// <summary>
/// Integration tests for WorkCenter development ETL.
/// Requires: Local SQL Server, CACHED_DB_FILES directory with workcenter.json.zstd
/// </summary>
public class WorkCenterDevEtlTests : IAsyncLifetime
{
private readonly string _connectionString;
private readonly string _cacheDirectory;
private readonly IDbConnectionFactory _connectionFactory;
public WorkCenterDevEtlTests()
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_connectionString = config.GetConnectionString("LotFinderDB")
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
_cacheDirectory = config["DevEtl:CacheDirectory"]
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
}
public async Task InitializeAsync()
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await connection.ExecuteAsync("TRUNCATE TABLE dbo.WorkCenter");
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public void Create_ReturnsValidPipeline()
{
var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = WorkCenterDevEtl.Create(_connectionFactory, cacheFilePath);
pipeline.ShouldNotBeNull();
pipeline.PipelineName.ShouldBe("WorkCenter_Dev");
}
[Fact]
public async Task Execute_LoadsWorkCenterData()
{
var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName);
if (!File.Exists(cacheFilePath)) return;
var pipeline = WorkCenterDevEtl.Create(_connectionFactory, cacheFilePath);
var result = await pipeline.ExecuteAsync();
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row");
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.WorkCenter");
count.ShouldBe((int)result.TotalRows);
}
[Fact]
public async Task Registry_RunAsync_LoadsWorkCenter()
{
if (!Directory.Exists(_cacheDirectory)) return;
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
var result = await registry.RunAsync("WorkCenter");
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
result.TotalRows.ShouldBeGreaterThan(0);
}
[Fact]
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
{
var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName);
Should.Throw<ArgumentNullException>(() => WorkCenterDevEtl.Create(null!, cacheFilePath));
}
[Fact]
public void Create_WithEmptyCacheFilePath_ThrowsArgumentException()
{
var mockFactory = Substitute.For<IDbConnectionFactory>();
Should.Throw<ArgumentException>(() => WorkCenterDevEtl.Create(mockFactory, string.Empty));
}
[Fact]
public void DevEtlRegistry_GetAvailableTables_IncludesWorkCenter()
{
if (!Directory.Exists(_cacheDirectory)) return;
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
var tables = registry.GetAvailableTables().ToList();
tables.ShouldContain("WorkCenter");
}
}