Files
Joseph Doherty 29ac56006d 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
2026-01-22 17:48:33 -05:00

383 lines
12 KiB
C#

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
}