feat: implement ETL pipeline redesign and ConfigManager improvements
- Add pipeline registry with JSON-based configuration and hot-reload support - Implement manual sync request feature with API, client UI, and database - Improve ConfigManager: connection string dropdown in pipeline editor, step delete/reorder functionality, and fix JSON parsing for ConnectionStrings
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataAccess.Services;
|
||||
using JdeScoping.Domain.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.DataAccess.Tests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for ManualSyncRequestService.
|
||||
/// Tests constructor validation, interface contract compliance, and static helper methods.
|
||||
/// Note: Since this service uses Dapper with raw SQL, full integration tests with
|
||||
/// an actual database are required for complete coverage of the SQL operations.
|
||||
/// </summary>
|
||||
public class ManualSyncRequestServiceTests
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<ManualSyncRequestService> _logger;
|
||||
|
||||
public ManualSyncRequestServiceTests()
|
||||
{
|
||||
_connectionFactory = Substitute.For<IDbConnectionFactory>();
|
||||
_logger = NullLogger<ManualSyncRequestService>.Instance;
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Should.Throw<ArgumentNullException>(() =>
|
||||
new ManualSyncRequestService(null!, _logger));
|
||||
|
||||
exception.ParamName.ShouldBe("connectionFactory");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Should.Throw<ArgumentNullException>(() =>
|
||||
new ManualSyncRequestService(_connectionFactory, null!));
|
||||
|
||||
exception.ParamName.ShouldBe("logger");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidDependencies_CreatesInstance()
|
||||
{
|
||||
// Act
|
||||
var service = new ManualSyncRequestService(_connectionFactory, _logger);
|
||||
|
||||
// Assert
|
||||
service.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface Contract Tests
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequestService_ImplementsIManualSyncRequestService()
|
||||
{
|
||||
// Arrange & Act
|
||||
var service = new ManualSyncRequestService(_connectionFactory, _logger);
|
||||
|
||||
// Assert
|
||||
service.ShouldBeAssignableTo<IManualSyncRequestService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRequestsAsync_HasCorrectSignature()
|
||||
{
|
||||
// Arrange
|
||||
var service = new ManualSyncRequestService(_connectionFactory, _logger);
|
||||
|
||||
// Act - Verify method exists with correct return type
|
||||
var methodInfo = typeof(ManualSyncRequestService).GetMethod(nameof(ManualSyncRequestService.GetRequestsAsync));
|
||||
|
||||
// Assert
|
||||
methodInfo.ShouldNotBeNull();
|
||||
methodInfo.ReturnType.ShouldBe(typeof(Task<IReadOnlyList<ManualSyncRequest>>));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNextPendingRequestAsync_HasCorrectSignature()
|
||||
{
|
||||
// Arrange
|
||||
var service = new ManualSyncRequestService(_connectionFactory, _logger);
|
||||
|
||||
// Act - Verify method exists with correct return type
|
||||
var methodInfo = typeof(ManualSyncRequestService).GetMethod(nameof(ManualSyncRequestService.GetNextPendingRequestAsync));
|
||||
|
||||
// Assert
|
||||
methodInfo.ShouldNotBeNull();
|
||||
methodInfo.ReturnType.ShouldBe(typeof(Task<ManualSyncRequest?>));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRequestAsync_HasCorrectSignature()
|
||||
{
|
||||
// Arrange
|
||||
var service = new ManualSyncRequestService(_connectionFactory, _logger);
|
||||
|
||||
// Act - Verify method exists with correct return type
|
||||
var methodInfo = typeof(ManualSyncRequestService).GetMethod(nameof(ManualSyncRequestService.CreateRequestAsync));
|
||||
|
||||
// Assert
|
||||
methodInfo.ShouldNotBeNull();
|
||||
methodInfo.ReturnType.ShouldBe(typeof(Task<ManualSyncRequest>));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelRequestAsync_HasCorrectSignature()
|
||||
{
|
||||
// Arrange
|
||||
var service = new ManualSyncRequestService(_connectionFactory, _logger);
|
||||
|
||||
// Act - Verify method exists with correct return type
|
||||
var methodInfo = typeof(ManualSyncRequestService).GetMethod(nameof(ManualSyncRequestService.CancelRequestAsync));
|
||||
|
||||
// Assert
|
||||
methodInfo.ShouldNotBeNull();
|
||||
methodInfo.ReturnType.ShouldBe(typeof(Task<bool>));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteRequestAsync_HasCorrectSignature()
|
||||
{
|
||||
// Arrange
|
||||
var service = new ManualSyncRequestService(_connectionFactory, _logger);
|
||||
|
||||
// Act - Verify method exists with correct return type
|
||||
var methodInfo = typeof(ManualSyncRequestService).GetMethod(nameof(ManualSyncRequestService.CompleteRequestAsync));
|
||||
|
||||
// Assert
|
||||
methodInfo.ShouldNotBeNull();
|
||||
methodInfo.ReturnType.ShouldBe(typeof(Task<bool>));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Domain Model Tests
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_Status_WhenPending_ReturnsPending()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ManualSyncRequest
|
||||
{
|
||||
Id = 1,
|
||||
PipelineName = "TestPipeline",
|
||||
SyncType = "mass",
|
||||
RequestedBy = "testuser",
|
||||
RequestDT = DateTime.UtcNow,
|
||||
CompletedDT = null,
|
||||
CancelDT = null
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
request.Status.ShouldBe("Pending");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_Status_WhenCompleted_ReturnsCompleted()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ManualSyncRequest
|
||||
{
|
||||
Id = 1,
|
||||
PipelineName = "TestPipeline",
|
||||
SyncType = "mass",
|
||||
RequestedBy = "testuser",
|
||||
RequestDT = DateTime.UtcNow.AddHours(-1),
|
||||
CompletedDT = DateTime.UtcNow,
|
||||
CancelDT = null
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
request.Status.ShouldBe("Completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_Status_WhenCancelled_ReturnsCancelled()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ManualSyncRequest
|
||||
{
|
||||
Id = 1,
|
||||
PipelineName = "TestPipeline",
|
||||
SyncType = "mass",
|
||||
RequestedBy = "testuser",
|
||||
RequestDT = DateTime.UtcNow.AddHours(-1),
|
||||
CompletedDT = null,
|
||||
CancelDT = DateTime.UtcNow,
|
||||
CancelledBy = "admin"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
request.Status.ShouldBe("Cancelled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_Status_WhenCancelledAndCompleted_ReturnsCancelled()
|
||||
{
|
||||
// Arrange - Edge case: both CancelDT and CompletedDT are set
|
||||
// Based on the implementation, CancelDT takes precedence
|
||||
var request = new ManualSyncRequest
|
||||
{
|
||||
Id = 1,
|
||||
PipelineName = "TestPipeline",
|
||||
SyncType = "mass",
|
||||
RequestedBy = "testuser",
|
||||
RequestDT = DateTime.UtcNow.AddHours(-2),
|
||||
CompletedDT = DateTime.UtcNow.AddHours(-1),
|
||||
CancelDT = DateTime.UtcNow,
|
||||
CancelledBy = "admin"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
// CancelDT is checked first in the Status property, so it should return "Cancelled"
|
||||
request.Status.ShouldBe("Cancelled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_DefaultRowVersion_IsEmptyArray()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ManualSyncRequest();
|
||||
|
||||
// Act & Assert
|
||||
request.RowVersion.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_DefaultPipelineName_IsEmptyString()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ManualSyncRequest();
|
||||
|
||||
// Act & Assert
|
||||
request.PipelineName.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_DefaultSyncType_IsEmptyString()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ManualSyncRequest();
|
||||
|
||||
// Act & Assert
|
||||
request.SyncType.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_DefaultRequestedBy_IsEmptyString()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ManualSyncRequest();
|
||||
|
||||
// Act & Assert
|
||||
request.RequestedBy.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualSyncRequest_CancelledBy_IsNullableAndDefaultsToNull()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ManualSyncRequest();
|
||||
|
||||
// Act & Assert
|
||||
request.CancelledBy.ShouldBeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Method Parameter Tests
|
||||
|
||||
[Fact]
|
||||
public void GetRequestsAsync_PendingOnlyParameter_DefaultsToFalse()
|
||||
{
|
||||
// Verify the interface defines correct default parameter
|
||||
var methodInfo = typeof(IManualSyncRequestService).GetMethod(nameof(IManualSyncRequestService.GetRequestsAsync));
|
||||
var parameters = methodInfo!.GetParameters();
|
||||
|
||||
// The pendingOnly parameter should have a default value of false
|
||||
var pendingOnlyParam = parameters.FirstOrDefault(p => p.Name == "pendingOnly");
|
||||
pendingOnlyParam.ShouldNotBeNull();
|
||||
pendingOnlyParam.HasDefaultValue.ShouldBeTrue();
|
||||
pendingOnlyParam.DefaultValue.ShouldBe(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRequestAsync_RequiresPipelineName()
|
||||
{
|
||||
// Verify the method has a pipelineName parameter
|
||||
var methodInfo = typeof(IManualSyncRequestService).GetMethod(nameof(IManualSyncRequestService.CreateRequestAsync));
|
||||
var parameters = methodInfo!.GetParameters();
|
||||
|
||||
var param = parameters.FirstOrDefault(p => p.Name == "pipelineName");
|
||||
param.ShouldNotBeNull();
|
||||
param.ParameterType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRequestAsync_RequiresSyncType()
|
||||
{
|
||||
// Verify the method has a syncType parameter
|
||||
var methodInfo = typeof(IManualSyncRequestService).GetMethod(nameof(IManualSyncRequestService.CreateRequestAsync));
|
||||
var parameters = methodInfo!.GetParameters();
|
||||
|
||||
var param = parameters.FirstOrDefault(p => p.Name == "syncType");
|
||||
param.ShouldNotBeNull();
|
||||
param.ParameterType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRequestAsync_RequiresRequestedBy()
|
||||
{
|
||||
// Verify the method has a requestedBy parameter
|
||||
var methodInfo = typeof(IManualSyncRequestService).GetMethod(nameof(IManualSyncRequestService.CreateRequestAsync));
|
||||
var parameters = methodInfo!.GetParameters();
|
||||
|
||||
var param = parameters.FirstOrDefault(p => p.Name == "requestedBy");
|
||||
param.ShouldNotBeNull();
|
||||
param.ParameterType.ShouldBe(typeof(string));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelRequestAsync_RequiresId()
|
||||
{
|
||||
// Verify the method has an id parameter
|
||||
var methodInfo = typeof(IManualSyncRequestService).GetMethod(nameof(IManualSyncRequestService.CancelRequestAsync));
|
||||
var parameters = methodInfo!.GetParameters();
|
||||
|
||||
var param = parameters.FirstOrDefault(p => p.Name == "id");
|
||||
param.ShouldNotBeNull();
|
||||
param.ParameterType.ShouldBe(typeof(int));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelRequestAsync_RequiresRowVersion()
|
||||
{
|
||||
// Verify the method has a rowVersion parameter for optimistic concurrency
|
||||
var methodInfo = typeof(IManualSyncRequestService).GetMethod(nameof(IManualSyncRequestService.CancelRequestAsync));
|
||||
var parameters = methodInfo!.GetParameters();
|
||||
|
||||
var param = parameters.FirstOrDefault(p => p.Name == "rowVersion");
|
||||
param.ShouldNotBeNull();
|
||||
param.ParameterType.ShouldBe(typeof(byte[]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteRequestAsync_RequiresId()
|
||||
{
|
||||
// Verify the method has an id parameter
|
||||
var methodInfo = typeof(IManualSyncRequestService).GetMethod(nameof(IManualSyncRequestService.CompleteRequestAsync));
|
||||
var parameters = methodInfo!.GetParameters();
|
||||
|
||||
var param = parameters.FirstOrDefault(p => p.Name == "id");
|
||||
param.ShouldNotBeNull();
|
||||
param.ParameterType.ShouldBe(typeof(int));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteRequestAsync_RequiresRowVersion()
|
||||
{
|
||||
// Verify the method has a rowVersion parameter for optimistic concurrency
|
||||
var methodInfo = typeof(IManualSyncRequestService).GetMethod(nameof(IManualSyncRequestService.CompleteRequestAsync));
|
||||
var parameters = methodInfo!.GetParameters();
|
||||
|
||||
var param = parameters.FirstOrDefault(p => p.Name == "rowVersion");
|
||||
param.ShouldNotBeNull();
|
||||
param.ParameterType.ShouldBe(typeof(byte[]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user