feat(application): add inactive profile validation with custom exception
- Create InactiveProfileException for profile active status validation - Update TriggeringDEXJobBatchCommand to check profile active status before execution - Add ICfgProfileRepository dependency to batch command handler - Throw InactiveProfileException when attempting to execute inactive profiles - Add profile not found validation with descriptive error message - Reorganize commands under DEXJob/Commands directory structure
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ECMJobRunner.Application.Common.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when attempting to execute a job for an inactive profile
|
||||||
|
/// Extends JobException with profile-specific context
|
||||||
|
/// </summary>
|
||||||
|
public class InactiveProfileException : JobException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of InactiveProfileException
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profileId">ID of the inactive profile</param>
|
||||||
|
/// <param name="profileName">Name of the inactive profile (nullable)</param>
|
||||||
|
/// <param name="batchId">Unique batch identifier for tracking</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Use this exception when:
|
||||||
|
/// - Attempting to trigger DEX job batch for an inactive profile
|
||||||
|
/// - Attempting to execute individual jobs from an inactive profile
|
||||||
|
/// - Profile is deactivated during execution
|
||||||
|
/// </remarks>
|
||||||
|
public InactiveProfileException(long profileId, string? profileName, string batchId)
|
||||||
|
: base(
|
||||||
|
jobName: "Profile Execution",
|
||||||
|
processName: "Profile Active Status Validation",
|
||||||
|
batchId: batchId,
|
||||||
|
reason: "The profile is marked as inactive and cannot be executed",
|
||||||
|
innerException: null,
|
||||||
|
("Profile ID", profileId.ToString(), false),
|
||||||
|
("Profile Name", profileName, true))
|
||||||
|
{
|
||||||
|
ProfileId = profileId;
|
||||||
|
ProfileName = profileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the inactive profile
|
||||||
|
/// </summary>
|
||||||
|
public long ProfileId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the inactive profile (nullable)
|
||||||
|
/// </summary>
|
||||||
|
public string? ProfileName { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
using ECMJobRunner.Domain.Interfaces;
|
using ECMJobRunner.Application.Common.Exceptions;
|
||||||
|
using ECMJobRunner.Application.Common.Interfaces;
|
||||||
|
using ECMJobRunner.Domain.Interfaces;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ECMJobRunner.Application.DEXJob
|
namespace ECMJobRunner.Application.DEXJob.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command to trigger DEX job batch for a profile
|
/// Command to trigger DEX job batch for a profile
|
||||||
@@ -21,8 +23,9 @@ namespace ECMJobRunner.Application.DEXJob
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for TriggeringDEXJobBatchCommand
|
/// Handler for TriggeringDEXJobBatchCommand
|
||||||
/// Retrieves all SQL jobs for a profile and executes them sequentially
|
/// Retrieves all SQL jobs for a profile and executes them sequentially
|
||||||
|
/// Validates that the profile is active before execution
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TriggeringDEXJobBatchCommandHandler(IProfileSqlJobRepository jobRepo, ISender sender)
|
public class TriggeringDEXJobBatchCommandHandler(ICfgProfileRepository profileRepo, IProfileSqlJobRepository jobRepo, ISender sender)
|
||||||
: IRequestHandler<TriggeringDEXJobBatchCommand, Unit>
|
: IRequestHandler<TriggeringDEXJobBatchCommand, Unit>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -32,12 +35,28 @@ namespace ECMJobRunner.Application.DEXJob
|
|||||||
/// <param name="request">The command containing the profile ID</param>
|
/// <param name="request">The command containing the profile ID</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Unit value indicating completion</returns>
|
/// <returns>Unit value indicating completion</returns>
|
||||||
|
/// <exception cref="InactiveProfileException">Thrown when the profile is not active</exception>
|
||||||
public async Task<Unit> Handle(TriggeringDEXJobBatchCommand request, CancellationToken cancellationToken)
|
public async Task<Unit> Handle(TriggeringDEXJobBatchCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var jobs = await jobRepo.FindAsync(j => j.ProfileId == request.ProfileId, cancellationToken);
|
|
||||||
|
|
||||||
var batchId = CreateBatchId();
|
var batchId = CreateBatchId();
|
||||||
|
|
||||||
|
// Retrieve the profile to check if it's active
|
||||||
|
var profile = await profileRepo.GetByIdAsync(request.ProfileId, cancellationToken);
|
||||||
|
|
||||||
|
// Check if profile exists and is active
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Profile with ID {request.ProfileId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile.Active)
|
||||||
|
{
|
||||||
|
throw new InactiveProfileException(profile.Id, profile.ProfileName, batchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all jobs for the profile
|
||||||
|
var jobs = await jobRepo.FindAsync(j => j.ProfileId == request.ProfileId, cancellationToken);
|
||||||
|
|
||||||
foreach (var job in jobs)
|
foreach (var job in jobs)
|
||||||
{
|
{
|
||||||
await sender.Send(new TriggeringDEXJobCommand
|
await sender.Send(new TriggeringDEXJobCommand
|
||||||
@@ -3,7 +3,7 @@ using MediatR;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ECMJobRunner.Application.DEXJob
|
namespace ECMJobRunner.Application.DEXJob.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command to trigger a single DEX job execution
|
/// Command to trigger a single DEX job execution
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
using ECMJobRunner.Application.Common.Dtos;
|
using ECMJobRunner.Application.Common.Dtos;
|
||||||
using ECMJobRunner.Domain.Interfaces;
|
using ECMJobRunner.Application.Common.Interfaces;
|
||||||
using ECMJobRunner.Application.Common.Options;
|
using ECMJobRunner.Application.Common.Options;
|
||||||
using ECMJobRunner.Application.DEXJob;
|
using ECMJobRunner.Application.DEXJob.Commands;
|
||||||
using ECMJobRunner.Domain.Entities;
|
using ECMJobRunner.Domain.Entities;
|
||||||
|
using ECMJobRunner.Domain.Interfaces;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
using ECMJobRunner.Application.Common.Exceptions;
|
||||||
|
using FluentAssertions;
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ECMJobRunner.Tests.Application
|
||||||
|
{
|
||||||
|
public class TriggeringDEXJobCommandExceptionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void JobSqlException_HasCorrectMessage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var jobName = "Main Query Execution";
|
||||||
|
var processName = "MainQueryExecution";
|
||||||
|
var batchId = "12345678901234567890";
|
||||||
|
var reason = "SQL syntax error";
|
||||||
|
var query = "SELECT * FROM NonExistentTable";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = new JobSqlException(
|
||||||
|
jobName,
|
||||||
|
processName,
|
||||||
|
batchId,
|
||||||
|
reason,
|
||||||
|
query,
|
||||||
|
null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exception.JobName.Should().Be(jobName);
|
||||||
|
exception.ProcessName.Should().Be(processName);
|
||||||
|
exception.BatchId.Should().Be(batchId);
|
||||||
|
exception.Query.Should().Be(query);
|
||||||
|
exception.Message.Should().Contain(jobName);
|
||||||
|
exception.Message.Should().Contain(processName);
|
||||||
|
exception.Message.Should().Contain(batchId);
|
||||||
|
exception.Message.Should().Contain(reason);
|
||||||
|
exception.Message.Should().Contain(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JobSqlException_WithInnerException_PreservesInnerException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var innerException = new InvalidOperationException("Database connection failed");
|
||||||
|
var exception = new JobSqlException(
|
||||||
|
"Main Query",
|
||||||
|
"Execution",
|
||||||
|
"12345678901234567890",
|
||||||
|
"Connection error",
|
||||||
|
"SELECT 1",
|
||||||
|
innerException);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exception.InnerException.Should().Be(innerException);
|
||||||
|
exception.InnerException!.Message.Should().Be("Database connection failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JobHttpException_HasCorrectMessage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var jobName = "ReC Request Execution";
|
||||||
|
var processName = "ReCRequestExecution";
|
||||||
|
var batchId = "12345678901234567890";
|
||||||
|
var reason = "HTTP 500 Internal Server Error";
|
||||||
|
var clientLibrary = "ReC.Client";
|
||||||
|
var clientMethod = "ExecuteAsync";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = new JobHttpException(
|
||||||
|
jobName,
|
||||||
|
processName,
|
||||||
|
batchId,
|
||||||
|
reason,
|
||||||
|
clientLibrary,
|
||||||
|
clientMethod,
|
||||||
|
null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exception.JobName.Should().Be(jobName);
|
||||||
|
exception.ProcessName.Should().Be(processName);
|
||||||
|
exception.BatchId.Should().Be(batchId);
|
||||||
|
exception.ClientLibrary.Should().Be(clientLibrary);
|
||||||
|
exception.ClientMethod.Should().Be(clientMethod);
|
||||||
|
exception.Message.Should().Contain(jobName);
|
||||||
|
exception.Message.Should().Contain(processName);
|
||||||
|
exception.Message.Should().Contain(batchId);
|
||||||
|
exception.Message.Should().Contain(reason);
|
||||||
|
exception.Message.Should().Contain(clientLibrary);
|
||||||
|
exception.Message.Should().Contain(clientMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JobHttpException_WithInnerException_PreservesInnerException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var innerException = new TimeoutException("Request timed out");
|
||||||
|
var exception = new JobHttpException(
|
||||||
|
"ReC Request",
|
||||||
|
"Execution",
|
||||||
|
"12345678901234567890",
|
||||||
|
"Timeout error",
|
||||||
|
"ReC.Client",
|
||||||
|
"ExecuteAsync",
|
||||||
|
innerException);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exception.InnerException.Should().Be(innerException);
|
||||||
|
exception.InnerException!.Message.Should().Be("Request timed out");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JobException_OmitsNullValues_WhenIgnoreIfNullIsTrue()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var exception = new JobException(
|
||||||
|
"Test Job",
|
||||||
|
"Test Process",
|
||||||
|
"12345678901234567890",
|
||||||
|
null, // reason is null
|
||||||
|
null,
|
||||||
|
("Custom Detail", "Value", false));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exception.Message.Should().NotContain("Reason:"); // Should be omitted because it's null and IgnoreIfNull=true
|
||||||
|
exception.Message.Should().Contain("Custom Detail: Value");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JobException_IncludesNullValues_WhenIgnoreIfNullIsFalse()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var exception = new JobException(
|
||||||
|
"Test Job",
|
||||||
|
"Test Process",
|
||||||
|
"12345678901234567890",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
("Custom Detail", null, false)); // IgnoreIfNull=false
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exception.Message.Should().Contain("Custom Detail:"); // Should be included even though value is null
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JobException_FormatsMessageWithSeparators()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var exception = new JobException(
|
||||||
|
"Test Job",
|
||||||
|
"Test Process",
|
||||||
|
"12345678901234567890",
|
||||||
|
"Test reason",
|
||||||
|
null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exception.Message.Should().Contain("─────────────────────────────────────────");
|
||||||
|
exception.Message.Should().Contain("Test Job could not be completed.");
|
||||||
|
exception.Message.Should().Contain("Process Name: Test Process");
|
||||||
|
exception.Message.Should().Contain("Batch Id: 12345678901234567890");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user