Files
ECMJobRunner/ECMJobRunner.Tests/Application/MainQueryExecutionBehaviorTests.cs
TekH 52c0614975 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)
2026-07-11 16:44:53 +02:00

253 lines
9.5 KiB
C#

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> { }
}
}