- Add AutoMapper profile for CfgProfile and ProfileSqlJob entity-to-DTO mappings - Consolidate GetProfileByIdQuery and GetAllActiveProfilesQuery into unified GetProfileQuery - Implement flexible filtering with nullable query options (Id, Active, TypeId, ProfileName) - Add IncludeSqlJobs option for optimized SQL job loading - Move CfgProfileDto to Common/Dtos for better architecture alignment - Add comprehensive unit tests for GetProfileQuery with multiple filter scenarios - Move ISQLExecutor interface from Domain to Application layer - Add GetByIdWithSqlJobsAsync and GetAllActiveWithSqlJobsAsync to repository
253 lines
9.5 KiB
C#
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.Application.Common.Interfaces;
|
|
using ECMJobRunner.Application.Common.Options;
|
|
using ECMJobRunner.Application.DEXJob.Commands;
|
|
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> { }
|
|
}
|
|
}
|