feat: extract DevEtl to JdeScoping.DataSync.Dev project
- Create JdeScoping.DataSync.Dev for sandbox testing ETL code - Create JdeScoping.DataSync.Dev.Tests for associated tests - Move 22 source files and 8 test files - Update namespaces from DevEtl to Dev - Add both projects to solution
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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,96 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\JdeScoping.DataAccess\JdeScoping.DataAccess.csproj" />
|
||||
<ProjectReference Include="..\..\src\JdeScoping.DataSync.Dev\JdeScoping.DataSync.Dev.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,96 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"LotFinderDB": "Server=localhost,1434;Database=ScopingTool;User Id=scopingapp;Password=Sc0ping@pp_Dev#2024;TrustServerCertificate=true"
|
||||
},
|
||||
"DevEtl": {
|
||||
"CacheDirectory": "/Users/dohertj2/Desktop/JdeScopingTool/CACHED_DB_FILES"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user