29ac56006d
- 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
383 lines
12 KiB
C#
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
|
|
}
|