using ECMJobRunner.Application.Common.Constants; using ECMJobRunner.Application.Common.Dtos; using ECMJobRunner.Application.Common.Exceptions; using ECMJobRunner.Application.Common.Interfaces; using ECMJobRunner.Application.Common.Options; using ECMJobRunner.Application.DEXJob.Commands; using ECMJobRunner.Application.DEXJob.Commands.Behaviors; 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 _mockExecutor; private readonly Mock> _mockOptions; private readonly MainQueryExecutionBehavior _behavior; private readonly Mock> _mockNext; public MainQueryExecutionBehaviorTests() { _mockExecutor = new Mock(); _mockOptions = new Mock>(); _mockOptions.Setup(o => o.Value).Returns(new DexJobOptions()); _behavior = new MainQueryExecutionBehavior(_mockExecutor.Object, _mockOptions.Object); _mockNext = new Mock>(); _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(It.IsAny(), It.IsAny())) .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(It.IsAny(), It.IsAny()), 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(It.IsAny(), It.IsAny())) .ReturnsAsync(new MainQueryResult { ReturnValue = null }); // Act #if NET48 Func act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object); #else Func 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(It.IsAny(), It.IsAny())) .ReturnsAsync(new MainQueryResult { ReturnValue = 1 }); // Act #if NET48 Func act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object); #else Func act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None); #endif // Assert await act.Should().ThrowAsync() .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(It.IsAny(), It.IsAny()), 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(_mockExecutor.Object, _mockOptions.Object); var command = new TriggeringDEXJobCommand { Job = new ProfileSqlJob { SqlMainQuery = null }, BatchId = "20260711143025123456" }; // Act #if NET48 Func act = async () => await behavior.Handle(command, CancellationToken.None, _mockNext.Object); #else Func act = async () => await behavior.Handle(command, _mockNext.Object, CancellationToken.None); #endif // Assert await act.Should().ThrowAsync() .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(It.IsAny(), It.IsAny())) .Callback((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(It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("SQL error")); // Act #if NET48 Func act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object); #else Func act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None); #endif // Assert await act.Should().ThrowAsync() .WithMessage("*SQL Main Query*"); } [Fact] public async Task Handle_WithNonDEXJobCommand_SkipsExecution() { // Arrange var otherCommand = new OtherCommand(); var behavior = new MainQueryExecutionBehavior(_mockExecutor.Object, _mockOptions.Object); var mockNext = new Mock>(); 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(It.IsAny(), It.IsAny()), Times.Never); mockNext.Verify(n => n(), Times.Once); } private class OtherCommand : IRequest { } } }