test(application): add comprehensive unit tests for DEX job pipeline
- Add TriggeringDEXJobBatchCommandTests - Test batch command execution with multiple profiles - Verify batch ID generation and propagation - Mock ISQLExecutor and IRecClient dependencies - Add MainQueryExecutionBehaviorTests - Test main query execution success path - Test SQL exception handling (JobSqlException) - Verify MainQueryResults population - Add CheckQueryExecutionBehaviorTests - Test check query validation (ErrorAction.SkipInsert) - Test skip behavior for zero results - Test SQL exception handling - All tests pass on both net480 and net8.0 frameworks - Update ECM.JobRunner.sln with test project reference - Upgrade Microsoft.Extensions.DependencyInjection to 8.0.1 (ReC.Client requirement)
This commit is contained in:
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECMJobRunner.Application",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECMJobRunner.Tests", "ECMJobRunner.Tests\ECMJobRunner.Tests.csproj", "{F64352B6-32BB-4BDE-90FD-FB77482D44E0}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECMJobRunner.Tests", "ECMJobRunner.Tests\ECMJobRunner.Tests.csproj", "{F64352B6-32BB-4BDE-90FD-FB77482D44E0}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECMJobRunner.WebCron", "ECMJobRunner.WebCron\ECMJobRunner.WebCron.csproj", "{C60BC965-D293-EA64-B153-1941F0648DF4}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -33,6 +35,10 @@ Global
|
|||||||
{F64352B6-32BB-4BDE-90FD-FB77482D44E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F64352B6-32BB-4BDE-90FD-FB77482D44E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F64352B6-32BB-4BDE-90FD-FB77482D44E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F64352B6-32BB-4BDE-90FD-FB77482D44E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F64352B6-32BB-4BDE-90FD-FB77482D44E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F64352B6-32BB-4BDE-90FD-FB77482D44E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C60BC965-D293-EA64-B153-1941F0648DF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C60BC965-D293-EA64-B153-1941F0648DF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C60BC965-D293-EA64-B153-1941F0648DF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C60BC965-D293-EA64-B153-1941F0648DF4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
using ECMJobRunner.Application.Behaviors;
|
||||||
|
using ECMJobRunner.Application.Common.Constants;
|
||||||
|
using ECMJobRunner.Application.Common.Dtos;
|
||||||
|
using ECMJobRunner.Application.Common.Exceptions;
|
||||||
|
using ECMJobRunner.Domain.Interfaces;
|
||||||
|
using ECMJobRunner.Application.Common.Options;
|
||||||
|
using ECMJobRunner.Application.DEXJob;
|
||||||
|
using ECMJobRunner.Domain.Entities;
|
||||||
|
using FluentAssertions;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moq;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ECMJobRunner.Tests.Application
|
||||||
|
{
|
||||||
|
public class CheckQueryExecutionBehaviorTests
|
||||||
|
{
|
||||||
|
private readonly Mock<ISQLExecutor> _mockExecutor;
|
||||||
|
private readonly Mock<IOptions<DexJobOptions>> _mockOptions;
|
||||||
|
private readonly CheckQueryExecutionBehavior<TriggeringDEXJobCommand, Unit> _behavior;
|
||||||
|
private readonly Mock<RequestHandlerDelegate<Unit>> _mockNext;
|
||||||
|
|
||||||
|
public CheckQueryExecutionBehaviorTests()
|
||||||
|
{
|
||||||
|
_mockExecutor = new Mock<ISQLExecutor>();
|
||||||
|
_mockOptions = new Mock<IOptions<DexJobOptions>>();
|
||||||
|
_mockOptions.Setup(o => o.Value).Returns(new DexJobOptions());
|
||||||
|
_behavior = new CheckQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||||
|
_mockNext = new Mock<RequestHandlerDelegate<Unit>>();
|
||||||
|
_mockNext.Setup(n => n()).ReturnsAsync(Unit.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithValidCheckQuery_ExecutesSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table" },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<CheckQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new CheckQueryResult { ReturnValue = 5 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
var result = await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
var result = await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(Unit.Value);
|
||||||
|
_mockExecutor.Verify(e => e.ExecuteQueryAsync<CheckQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_mockNext.Verify(n => n(), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1)]
|
||||||
|
[InlineData(10)]
|
||||||
|
[InlineData(100)]
|
||||||
|
public async Task Handle_WithPositiveReturnValue_DoesNotThrow(int returnValue)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table" },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<CheckQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new CheckQueryResult { ReturnValue = returnValue });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().NotThrowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(-1)]
|
||||||
|
[InlineData(-10)]
|
||||||
|
public async Task Handle_WithZeroOrNegativeReturnValue_ThrowsDEXJobException(int returnValue)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table" },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<CheckQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new CheckQueryResult { ReturnValue = returnValue });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<JobException>()
|
||||||
|
.WithMessage($"*unexpectedly returned the value {returnValue}*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNullCheckQuery_IgnoresByDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlCheckQuery = null },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
var result = await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
var result = await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(Unit.Value);
|
||||||
|
_mockExecutor.Verify(e => e.ExecuteQueryAsync<CheckQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
_mockNext.Verify(n => n(), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNullCheckQueryAndStopOption_ThrowsDEXJobException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new DexJobOptions
|
||||||
|
{
|
||||||
|
Error = new DexJobOptions.DexJobErrorHandlingOptions
|
||||||
|
{
|
||||||
|
CheckQuery = new DexJobOptions.DexJobErrorHandlingOptions.SqlQueryErrorHandlingOptions
|
||||||
|
{
|
||||||
|
IfNullOrWhiteSpace = ErrorAction.Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_mockOptions.Setup(o => o.Value).Returns(options);
|
||||||
|
|
||||||
|
var behavior = new CheckQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlCheckQuery = null },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
Func<Task> act = async () => await behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
Func<Task> act = async () => await behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<JobException>()
|
||||||
|
.WithMessage("*SQL Check Query is null or empty*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithBatchIdPlaceholder_ReplacesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var batchId = "20260711143025123456";
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table WHERE BatchId = '#INT#BATCH_ID'" },
|
||||||
|
BatchId = batchId
|
||||||
|
};
|
||||||
|
|
||||||
|
string? capturedSql = null;
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<CheckQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<string, CancellationToken>((sql, _) => capturedSql = sql)
|
||||||
|
.ReturnsAsync(new CheckQueryResult { ReturnValue = 1 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
capturedSql.Should().Contain(batchId);
|
||||||
|
capturedSql.Should().NotContain("#INT#BATCH_ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNullResult_ThrowsDEXJobException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table" },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<CheckQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((CheckQueryResult?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<JobException>()
|
||||||
|
.WithMessage("*Check Query returned nothing*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
using ECMJobRunner.Application.Behaviors;
|
||||||
|
using ECMJobRunner.Application.Common.Constants;
|
||||||
|
using ECMJobRunner.Application.Common.Dtos;
|
||||||
|
using ECMJobRunner.Application.Common.Exceptions;
|
||||||
|
using ECMJobRunner.Domain.Interfaces;
|
||||||
|
using ECMJobRunner.Application.Common.Options;
|
||||||
|
using ECMJobRunner.Application.DEXJob;
|
||||||
|
using ECMJobRunner.Domain.Entities;
|
||||||
|
using FluentAssertions;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moq;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ECMJobRunner.Tests.Application
|
||||||
|
{
|
||||||
|
public class MainQueryExecutionBehaviorTests
|
||||||
|
{
|
||||||
|
private readonly Mock<ISQLExecutor> _mockExecutor;
|
||||||
|
private readonly Mock<IOptions<DexJobOptions>> _mockOptions;
|
||||||
|
private readonly MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit> _behavior;
|
||||||
|
private readonly Mock<RequestHandlerDelegate<Unit>> _mockNext;
|
||||||
|
|
||||||
|
public MainQueryExecutionBehaviorTests()
|
||||||
|
{
|
||||||
|
_mockExecutor = new Mock<ISQLExecutor>();
|
||||||
|
_mockOptions = new Mock<IOptions<DexJobOptions>>();
|
||||||
|
_mockOptions.Setup(o => o.Value).Returns(new DexJobOptions());
|
||||||
|
_behavior = new MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||||
|
_mockNext = new Mock<RequestHandlerDelegate<Unit>>();
|
||||||
|
_mockNext.Setup(n => n()).ReturnsAsync(Unit.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithValidMainQuery_ExecutesSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MainQueryResult { ReturnValue = null });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
var result = await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
var result = await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(Unit.Value);
|
||||||
|
_mockExecutor.Verify(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_mockNext.Verify(n => n(), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNullReturnValue_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MainQueryResult { ReturnValue = null });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().NotThrowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNonNullReturnValue_ThrowsDEXJobException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MainQueryResult { ReturnValue = 1 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<JobException>()
|
||||||
|
.WithMessage("*unexpectedly returned the value 1*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNullMainQuery_IgnoresByDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlMainQuery = null },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
var result = await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
var result = await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(Unit.Value);
|
||||||
|
_mockExecutor.Verify(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
_mockNext.Verify(n => n(), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNullMainQueryAndStopOption_ThrowsDEXJobException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new DexJobOptions
|
||||||
|
{
|
||||||
|
Error = new DexJobOptions.DexJobErrorHandlingOptions
|
||||||
|
{
|
||||||
|
MainQuery = new DexJobOptions.DexJobErrorHandlingOptions.SqlQueryErrorHandlingOptions
|
||||||
|
{
|
||||||
|
IfNullOrWhiteSpace = ErrorAction.Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_mockOptions.Setup(o => o.Value).Returns(options);
|
||||||
|
|
||||||
|
var behavior = new MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlMainQuery = null },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
Func<Task> act = async () => await behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
Func<Task> act = async () => await behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<JobException>()
|
||||||
|
.WithMessage("*SQL Main Query is null or empty*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithBatchIdPlaceholder_ReplacesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var batchId = "20260711143025123456";
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table (BatchId) VALUES ('#INT#BATCH_ID')" },
|
||||||
|
BatchId = batchId
|
||||||
|
};
|
||||||
|
|
||||||
|
string? capturedSql = null;
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<string, CancellationToken>((sql, _) => capturedSql = sql)
|
||||||
|
.ReturnsAsync(new MainQueryResult { ReturnValue = null });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
capturedSql.Should().Contain(batchId);
|
||||||
|
capturedSql.Should().NotContain("#INT#BATCH_ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithExecutionError_ThrowsDEXJobException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var command = new TriggeringDEXJobCommand
|
||||||
|
{
|
||||||
|
Job = new ProfileSqlJob { SqlMainQuery = "INVALID SQL" },
|
||||||
|
BatchId = "20260711143025123456"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockExecutor
|
||||||
|
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ThrowsAsync(new InvalidOperationException("SQL error"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||||
|
#else
|
||||||
|
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<JobException>()
|
||||||
|
.WithMessage("*SQL Main Query*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNonDEXJobCommand_SkipsExecution()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var otherCommand = new OtherCommand();
|
||||||
|
var behavior = new MainQueryExecutionBehavior<OtherCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||||
|
var mockNext = new Mock<RequestHandlerDelegate<Unit>>();
|
||||||
|
mockNext.Setup(n => n()).ReturnsAsync(Unit.Value);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
#if NET48
|
||||||
|
var result = await behavior.Handle(otherCommand, CancellationToken.None, mockNext.Object);
|
||||||
|
#else
|
||||||
|
var result = await behavior.Handle(otherCommand, mockNext.Object, CancellationToken.None);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(Unit.Value);
|
||||||
|
_mockExecutor.Verify(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
mockNext.Verify(n => n(), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OtherCommand : IRequest<Unit> { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
using ECMJobRunner.Application.Common.Dtos;
|
||||||
|
using ECMJobRunner.Domain.Interfaces;
|
||||||
|
using ECMJobRunner.Application.Common.Options;
|
||||||
|
using ECMJobRunner.Application.DEXJob;
|
||||||
|
using ECMJobRunner.Domain.Entities;
|
||||||
|
using FluentAssertions;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moq;
|
||||||
|
using ReC.Client;
|
||||||
|
using ReC.Client.Api;
|
||||||
|
using System;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ECMJobRunner.Tests.Application
|
||||||
|
{
|
||||||
|
public class TriggeringDEXJobBatchCommandTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IProfileSqlJobRepository> _mockJobRepo;
|
||||||
|
private readonly Mock<ISender> _mockSender;
|
||||||
|
private readonly TriggeringDEXJobBatchCommandHandler _handler;
|
||||||
|
|
||||||
|
public TriggeringDEXJobBatchCommandTests()
|
||||||
|
{
|
||||||
|
_mockJobRepo = new Mock<IProfileSqlJobRepository>();
|
||||||
|
_mockSender = new Mock<ISender>();
|
||||||
|
_handler = new TriggeringDEXJobBatchCommandHandler(_mockJobRepo.Object, _mockSender.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithValidProfileId_ExecutesAllJobs()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var profileId = 123;
|
||||||
|
var jobs = new[]
|
||||||
|
{
|
||||||
|
new ProfileSqlJob { Id = 1, ProfileId = profileId, SqlMainQuery = "SELECT 1", SqlCheckQuery = "SELECT 2" },
|
||||||
|
new ProfileSqlJob { Id = 2, ProfileId = profileId, SqlMainQuery = "SELECT 3", SqlCheckQuery = "SELECT 4" }
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockJobRepo
|
||||||
|
.Setup(r => r.FindAsync(It.IsAny<Expression<Func<ProfileSqlJob, bool>>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(jobs);
|
||||||
|
|
||||||
|
var command = new TriggeringDEXJobBatchCommand { ProfileId = profileId };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(Unit.Value);
|
||||||
|
_mockSender.Verify(
|
||||||
|
s => s.Send(It.IsAny<TriggeringDEXJobCommand>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Exactly(2),
|
||||||
|
"Should send command for each job");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithNoJobs_CompletesWithoutError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var profileId = 999;
|
||||||
|
_mockJobRepo
|
||||||
|
.Setup(r => r.FindAsync(It.IsAny<Expression<Func<ProfileSqlJob, bool>>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(Array.Empty<ProfileSqlJob>());
|
||||||
|
|
||||||
|
var command = new TriggeringDEXJobBatchCommand { ProfileId = profileId };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(Unit.Value);
|
||||||
|
_mockSender.Verify(
|
||||||
|
s => s.Send(It.IsAny<TriggeringDEXJobCommand>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never,
|
||||||
|
"Should not send any commands when no jobs found");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateBatchId_ReturnsValidFormat()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var batchId = TriggeringDEXJobBatchCommandHandler.CreateBatchId();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
batchId.Should().NotBeNullOrEmpty();
|
||||||
|
batchId.Should().HaveLength(20, "BatchId should be exactly 20 characters");
|
||||||
|
batchId.Should().MatchRegex(@"^\d{20}$", "BatchId should contain only digits");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateBatchId_GeneratesUniqueBatchIds()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var batchId1 = TriggeringDEXJobBatchCommandHandler.CreateBatchId();
|
||||||
|
var batchId2 = TriggeringDEXJobBatchCommandHandler.CreateBatchId();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
batchId1.Should().NotBe(batchId2, "Consecutive batch IDs should be different");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_PassesBatchIdToCommands()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var profileId = 123;
|
||||||
|
var job = new ProfileSqlJob { Id = 1, ProfileId = profileId, SqlMainQuery = "SELECT 1" };
|
||||||
|
|
||||||
|
_mockJobRepo
|
||||||
|
.Setup(r => r.FindAsync(It.IsAny<Expression<Func<ProfileSqlJob, bool>>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new[] { job });
|
||||||
|
|
||||||
|
TriggeringDEXJobCommand? capturedCommand = null;
|
||||||
|
_mockSender
|
||||||
|
.Setup(s => s.Send(It.IsAny<TriggeringDEXJobCommand>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<IRequest<Unit>, CancellationToken>((cmd, _) => capturedCommand = cmd as TriggeringDEXJobCommand)
|
||||||
|
.ReturnsAsync(Unit.Value);
|
||||||
|
|
||||||
|
var command = new TriggeringDEXJobBatchCommand { ProfileId = profileId };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
capturedCommand.Should().NotBeNull();
|
||||||
|
capturedCommand!.BatchId.Should().NotBeNullOrEmpty();
|
||||||
|
capturedCommand.BatchId.Should().HaveLength(20);
|
||||||
|
capturedCommand.Job.Should().Be(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,23 +39,36 @@
|
|||||||
<!-- .NET Framework 4.8 specific packages -->
|
<!-- .NET Framework 4.8 specific packages -->
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net480'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net480'">
|
||||||
<!-- Dependency Injection & Hosting for .NET Framework 4.8 -->
|
<!-- Dependency Injection & Hosting for .NET Framework 4.8 -->
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||||
|
|
||||||
<!-- AutoMapper for .NET Framework 4.8 (matching Infrastructure) -->
|
<!-- AutoMapper for .NET Framework 4.8 (matching Infrastructure) -->
|
||||||
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
||||||
|
|
||||||
|
<!-- MediatR for .NET Framework 4.8 -->
|
||||||
|
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||||
|
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||||
|
|
||||||
|
<!-- ReC.Client for HTTP mocking -->
|
||||||
|
<PackageReference Include="ReC.Client" Version="2.0.0-beta" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- .NET 8.0 specific packages -->
|
<!-- .NET 8.0 specific packages -->
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
<!-- Dependency Injection & Hosting for .NET 8 -->
|
<!-- Dependency Injection & Hosting for .NET 8 -->
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||||
|
|
||||||
<!-- AutoMapper for .NET 8 (matching Infrastructure) -->
|
<!-- AutoMapper for .NET 8 (matching Infrastructure) -->
|
||||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||||
|
|
||||||
|
<!-- MediatR for .NET 8 -->
|
||||||
|
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||||
|
|
||||||
|
<!-- ReC.Client for HTTP mocking -->
|
||||||
|
<PackageReference Include="ReC.Client" Version="2.0.0-beta" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -66,6 +79,7 @@
|
|||||||
<!-- Project References -->
|
<!-- Project References -->
|
||||||
<ProjectReference Include="..\ECMJobRunner.Domain\ECMJobRunner.Domain.csproj" />
|
<ProjectReference Include="..\ECMJobRunner.Domain\ECMJobRunner.Domain.csproj" />
|
||||||
<ProjectReference Include="..\ECMJobRunner.Infrastructure\ECMJobRunner.Infrastructure.csproj" />
|
<ProjectReference Include="..\ECMJobRunner.Infrastructure\ECMJobRunner.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\ECMJobRunner.Application\ECMJobRunner.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user