refactor(application): consolidate query architecture with AutoMapper integration
- 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
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
using ECMJobRunner.Application.Common.Constants;
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using ECMJobRunner.Application.Common.Constants;
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using ECMJobRunner.Application.Common.Constants;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ReC.Client;
|
||||
|
||||
118
ECMJobRunner.Application/Common/Dtos/CfgProfileDto.cs
Normal file
118
ECMJobRunner.Application/Common/Dtos/CfgProfileDto.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ECMJobRunner.Application.Common.Dtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Data Transfer Object for CfgProfile entity
|
||||
/// Used for querying and returning profile data
|
||||
/// </summary>
|
||||
public class CfgProfileDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Profile ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Active / Inactive switch
|
||||
/// </summary>
|
||||
public bool Active { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Profile name
|
||||
/// </summary>
|
||||
public string ProfileName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Profile type: 0 = ADSync; 1 = GraphQL; 2 = SQL-Job; 3 = SQL and REST-Job
|
||||
/// </summary>
|
||||
public byte TypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Schedule in Cron format
|
||||
/// </summary>
|
||||
public string Schedule { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Optional description
|
||||
/// </summary>
|
||||
public string? Comment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Created by
|
||||
/// </summary>
|
||||
public string AddedWho { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Created at
|
||||
/// </summary>
|
||||
public DateTime AddedWhen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Modified by
|
||||
/// </summary>
|
||||
public string? ChangedWho { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Modified at
|
||||
/// </summary>
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SQL Jobs associated with this profile
|
||||
/// </summary>
|
||||
public List<ProfileSqlJobDto>? SqlJobs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for ProfileSqlJob entity
|
||||
/// </summary>
|
||||
public class ProfileSqlJobDto
|
||||
{
|
||||
/// <summary>
|
||||
/// SQL Job ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Profile ID (foreign key)
|
||||
/// </summary>
|
||||
public long ProfileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Active / Inactive switch
|
||||
/// </summary>
|
||||
public bool Active { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Execution sequence order
|
||||
/// </summary>
|
||||
public short Sequence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Job name
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SQL query for pre-check validation
|
||||
/// </summary>
|
||||
public string? SqlCheckQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Main SQL query to execute
|
||||
/// </summary>
|
||||
public string? SqlMainQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// API command to execute
|
||||
/// </summary>
|
||||
public string? ApiCommand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional description
|
||||
/// </summary>
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Domain.Interfaces
|
||||
namespace ECMJobRunner.Application.Common.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for executing SQL queries and mapping results to DTOs
|
||||
@@ -0,0 +1,23 @@
|
||||
using AutoMapper;
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
|
||||
namespace ECMJobRunner.Application.Common.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// AutoMapper profile for CfgProfile and ProfileSqlJob mappings
|
||||
/// Maps domain entities to DTOs
|
||||
/// </summary>
|
||||
public class ProfileMappingProfile : Profile
|
||||
{
|
||||
public ProfileMappingProfile()
|
||||
{
|
||||
// CfgProfile -> CfgProfileDto
|
||||
CreateMap<CfgProfile, CfgProfileDto>()
|
||||
.ForMember(dest => dest.SqlJobs, opt => opt.MapFrom(src => src.SqlJobs));
|
||||
|
||||
// ProfileSqlJob -> ProfileSqlJobDto
|
||||
CreateMap<ProfileSqlJob, ProfileSqlJobDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
124
ECMJobRunner.Application/DEXJob/Queries/GetProfileQuery.cs
Normal file
124
ECMJobRunner.Application/DEXJob/Queries/GetProfileQuery.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using AutoMapper;
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using MediatR;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.DEXJob.Queries
|
||||
{
|
||||
/// <summary>
|
||||
/// Query to retrieve profiles with flexible filtering options
|
||||
/// All query options are nullable - when no filters are specified, returns all profiles
|
||||
/// </summary>
|
||||
public class GetProfileQuery : IRequest<List<CfgProfileDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Profile ID to retrieve (optional)
|
||||
/// When specified, returns only the profile with this ID
|
||||
/// </summary>
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filter by active status (optional)
|
||||
/// When null, returns both active and inactive profiles
|
||||
/// When true, returns only active profiles
|
||||
/// When false, returns only inactive profiles
|
||||
/// </summary>
|
||||
public bool? Active { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filter by profile type (optional)
|
||||
/// When specified, returns only profiles with this type
|
||||
/// Type: 0 = ADSync; 1 = GraphQL; 2 = SQL-Job; 3 = SQL and REST-Job
|
||||
/// </summary>
|
||||
public byte? TypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filter by profile name (optional)
|
||||
/// When specified, returns profiles with matching name (case-insensitive contains)
|
||||
/// </summary>
|
||||
public string? ProfileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Include associated SQL jobs in the result (optional)
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool IncludeSqlJobs { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for GetProfileQuery
|
||||
/// Retrieves profiles with optional filtering and uses AutoMapper for DTO mapping
|
||||
/// </summary>
|
||||
public class GetProfileQueryHandler : IRequestHandler<GetProfileQuery, List<CfgProfileDto>>
|
||||
{
|
||||
private readonly ICfgProfileRepository _profileRepository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public GetProfileQueryHandler(ICfgProfileRepository profileRepository, IMapper mapper)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<List<CfgProfileDto>> Handle(GetProfileQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<Domain.Entities.CfgProfile> profiles;
|
||||
|
||||
// If ID is specified, get single profile by ID
|
||||
if (request.Id.HasValue)
|
||||
{
|
||||
var profile = request.IncludeSqlJobs
|
||||
? await _profileRepository.GetByIdWithSqlJobsAsync(request.Id.Value, cancellationToken)
|
||||
: await _profileRepository.GetByIdAsync(request.Id.Value, cancellationToken);
|
||||
|
||||
profiles = profile != null ? new[] { profile } : [];
|
||||
}
|
||||
// Otherwise, get profiles with filters
|
||||
else
|
||||
{
|
||||
// Get all profiles with SQL jobs if requested
|
||||
if (request.IncludeSqlJobs)
|
||||
{
|
||||
// If Active filter is specified and true, use optimized method
|
||||
if (request.Active.HasValue && request.Active.Value)
|
||||
{
|
||||
profiles = await _profileRepository.GetAllActiveWithSqlJobsAsync(cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generic query with filters
|
||||
profiles = await _profileRepository.FindAsync(p => true, cancellationToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
profiles = await _profileRepository.GetAllAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if (request.Active.HasValue)
|
||||
{
|
||||
profiles = profiles.Where(p => p.Active == request.Active.Value);
|
||||
}
|
||||
|
||||
if (request.TypeId.HasValue)
|
||||
{
|
||||
profiles = profiles.Where(p => p.TypeId == request.TypeId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.ProfileName))
|
||||
{
|
||||
var searchName = request.ProfileName.ToLowerInvariant();
|
||||
profiles = profiles.Where(p => p.ProfileName.ToLowerInvariant().Contains(searchName));
|
||||
}
|
||||
}
|
||||
|
||||
// Use AutoMapper to map entities to DTOs
|
||||
return _mapper.Map<List<CfgProfileDto>>(profiles.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Domain.Interfaces
|
||||
{
|
||||
@@ -7,6 +10,19 @@ namespace ECMJobRunner.Domain.Interfaces
|
||||
/// </summary>
|
||||
public interface ICfgProfileRepository : IRepository<CfgProfile>
|
||||
{
|
||||
// Add custom CfgProfile-specific methods here if needed
|
||||
/// <summary>
|
||||
/// Gets a profile by ID with associated SQL jobs eagerly loaded
|
||||
/// </summary>
|
||||
/// <param name="id">Profile ID</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Profile with SQL jobs or null if not found</returns>
|
||||
Task<CfgProfile?> GetByIdWithSqlJobsAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all active profiles with associated SQL jobs eagerly loaded
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>List of active profiles with SQL jobs</returns>
|
||||
Task<List<CfgProfile>> GetAllActiveWithSqlJobsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
#endif
|
||||
using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using ECMJobRunner.Infrastructure.Data;
|
||||
using ECMJobRunner.Infrastructure.Repositories;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ECMJobRunner.Domain\ECMJobRunner.Domain.csproj" />
|
||||
<ProjectReference Include="..\ECMJobRunner.Application\ECMJobRunner.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net480'">
|
||||
@@ -28,8 +29,8 @@
|
||||
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
||||
<!-- Dependency Injection for .NET Framework 4.8 -->
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
|
||||
@@ -2,6 +2,15 @@ using AutoMapper;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using ECMJobRunner.Infrastructure.Data;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
#if NET48
|
||||
using System.Data.Entity;
|
||||
#else
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
#endif
|
||||
|
||||
namespace ECMJobRunner.Infrastructure.Repositories
|
||||
{
|
||||
@@ -17,6 +26,25 @@ namespace ECMJobRunner.Infrastructure.Repositories
|
||||
{
|
||||
}
|
||||
|
||||
// Entity-specific methods can be added here in the future
|
||||
/// <summary>
|
||||
/// Gets a profile by ID with associated SQL jobs eagerly loaded
|
||||
/// </summary>
|
||||
public async Task<CfgProfile?> GetByIdWithSqlJobsAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.CfgProfiles
|
||||
.Include(p => p.SqlJobs)
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all active profiles with associated SQL jobs eagerly loaded
|
||||
/// </summary>
|
||||
public async Task<List<CfgProfile>> GetAllActiveWithSqlJobsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.CfgProfiles
|
||||
.Include(p => p.SqlJobs)
|
||||
.Where(p => p.Active)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Linq;
|
||||
#else
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
#endif
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Infrastructure.Data;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -2,9 +2,9 @@ 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.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using MediatR;
|
||||
|
||||
302
ECMJobRunner.Tests/Application/GetProfileQueryTests.cs
Normal file
302
ECMJobRunner.Tests/Application/GetProfileQueryTests.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.DEXJob.Queries;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace ECMJobRunner.Tests.Application
|
||||
{
|
||||
public class GetProfileByIdQueryTests
|
||||
{
|
||||
private readonly Mock<ICfgProfileRepository> _mockProfileRepo;
|
||||
private readonly GetProfileByIdQueryHandler _handler;
|
||||
|
||||
public GetProfileByIdQueryTests()
|
||||
{
|
||||
_mockProfileRepo = new Mock<ICfgProfileRepository>();
|
||||
_handler = new GetProfileByIdQueryHandler(_mockProfileRepo.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidProfileId_ReturnsProfile()
|
||||
{
|
||||
// Arrange
|
||||
var profileId = 123L;
|
||||
var profile = new CfgProfile
|
||||
{
|
||||
Id = profileId,
|
||||
Active = true,
|
||||
ProfileName = "Test Profile",
|
||||
TypeId = 2,
|
||||
Schedule = "0 30 4 ? * MON-SAT",
|
||||
Comment = "Test comment",
|
||||
AddedWho = "TestUser",
|
||||
AddedWhen = DateTime.Now,
|
||||
SqlJobs = new List<ProfileSqlJob>
|
||||
{
|
||||
new ProfileSqlJob
|
||||
{
|
||||
Id = 1,
|
||||
ProfileId = profileId,
|
||||
Active = true,
|
||||
Sequence = 1,
|
||||
Name = "Job 1",
|
||||
SqlMainQuery = "SELECT 1",
|
||||
SqlCheckQuery = "SELECT COUNT(*) FROM Table1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_mockProfileRepo
|
||||
.Setup(r => r.GetByIdWithSqlJobsAsync(profileId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(profile);
|
||||
|
||||
var query = new GetProfileByIdQuery { ProfileId = profileId, IncludeSqlJobs = true };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Id.Should().Be(profileId);
|
||||
result.ProfileName.Should().Be("Test Profile");
|
||||
result.Active.Should().BeTrue();
|
||||
result.TypeId.Should().Be(2);
|
||||
result.SqlJobs.Should().NotBeNull();
|
||||
result.SqlJobs!.Count.Should().Be(1);
|
||||
result.SqlJobs[0].Name.Should().Be("Job 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNonExistentProfileId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var profileId = 999L;
|
||||
_mockProfileRepo
|
||||
.Setup(r => r.GetByIdWithSqlJobsAsync(profileId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((CfgProfile?)null);
|
||||
|
||||
var query = new GetProfileByIdQuery { ProfileId = profileId };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithIncludeSqlJobsFalse_DoesNotLoadSqlJobs()
|
||||
{
|
||||
// Arrange
|
||||
var profileId = 123L;
|
||||
var profile = new CfgProfile
|
||||
{
|
||||
Id = profileId,
|
||||
Active = true,
|
||||
ProfileName = "Test Profile",
|
||||
TypeId = 2,
|
||||
Schedule = "0 30 4 ? * MON-SAT",
|
||||
AddedWho = "TestUser",
|
||||
AddedWhen = DateTime.Now
|
||||
};
|
||||
|
||||
_mockProfileRepo
|
||||
.Setup(r => r.GetByIdAsync(profileId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(profile);
|
||||
|
||||
var query = new GetProfileByIdQuery { ProfileId = profileId, IncludeSqlJobs = false };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.SqlJobs.Should().BeNull();
|
||||
_mockProfileRepo.Verify(
|
||||
r => r.GetByIdAsync(profileId, It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
_mockProfileRepo.Verify(
|
||||
r => r.GetByIdWithSqlJobsAsync(It.IsAny<long>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MapsAllProperties_Correctly()
|
||||
{
|
||||
// Arrange
|
||||
var profileId = 123L;
|
||||
var addedWhen = new DateTime(2026, 1, 1, 10, 0, 0);
|
||||
var changedWhen = new DateTime(2026, 1, 2, 14, 30, 0);
|
||||
|
||||
var profile = new CfgProfile
|
||||
{
|
||||
Id = profileId,
|
||||
Active = false,
|
||||
ProfileName = "Inactive Profile",
|
||||
TypeId = 3,
|
||||
Schedule = "0 0 12 * * ?",
|
||||
Comment = "Detailed comment",
|
||||
AddedWho = "Admin",
|
||||
AddedWhen = addedWhen,
|
||||
ChangedWho = "Editor",
|
||||
ChangedWhen = changedWhen
|
||||
};
|
||||
|
||||
_mockProfileRepo
|
||||
.Setup(r => r.GetByIdAsync(profileId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(profile);
|
||||
|
||||
var query = new GetProfileByIdQuery { ProfileId = profileId, IncludeSqlJobs = false };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Id.Should().Be(profileId);
|
||||
result.Active.Should().BeFalse();
|
||||
result.ProfileName.Should().Be("Inactive Profile");
|
||||
result.TypeId.Should().Be(3);
|
||||
result.Schedule.Should().Be("0 0 12 * * ?");
|
||||
result.Comment.Should().Be("Detailed comment");
|
||||
result.AddedWho.Should().Be("Admin");
|
||||
result.AddedWhen.Should().Be(addedWhen);
|
||||
result.ChangedWho.Should().Be("Editor");
|
||||
result.ChangedWhen.Should().Be(changedWhen);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetAllActiveProfilesQueryTests
|
||||
{
|
||||
private readonly Mock<ICfgProfileRepository> _mockProfileRepo;
|
||||
private readonly GetAllActiveProfilesQueryHandler _handler;
|
||||
|
||||
public GetAllActiveProfilesQueryTests()
|
||||
{
|
||||
_mockProfileRepo = new Mock<ICfgProfileRepository>();
|
||||
_handler = new GetAllActiveProfilesQueryHandler(_mockProfileRepo.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsAllActiveProfiles()
|
||||
{
|
||||
// Arrange
|
||||
var profiles = new List<CfgProfile>
|
||||
{
|
||||
new CfgProfile
|
||||
{
|
||||
Id = 1,
|
||||
Active = true,
|
||||
ProfileName = "Profile 1",
|
||||
TypeId = 2,
|
||||
Schedule = "0 30 4 ? * MON-SAT",
|
||||
AddedWho = "User1",
|
||||
AddedWhen = DateTime.Now,
|
||||
SqlJobs = new List<ProfileSqlJob>
|
||||
{
|
||||
new ProfileSqlJob { Id = 1, ProfileId = 1, Sequence = 1, Name = "Job 1" }
|
||||
}
|
||||
},
|
||||
new CfgProfile
|
||||
{
|
||||
Id = 2,
|
||||
Active = true,
|
||||
ProfileName = "Profile 2",
|
||||
TypeId = 3,
|
||||
Schedule = "0 0 12 * * ?",
|
||||
AddedWho = "User2",
|
||||
AddedWhen = DateTime.Now,
|
||||
SqlJobs = new List<ProfileSqlJob>
|
||||
{
|
||||
new ProfileSqlJob { Id = 2, ProfileId = 2, Sequence = 1, Name = "Job 2" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_mockProfileRepo
|
||||
.Setup(r => r.GetAllActiveWithSqlJobsAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(profiles);
|
||||
|
||||
var query = new GetAllActiveProfilesQuery { IncludeSqlJobs = true };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Count.Should().Be(2);
|
||||
result[0].ProfileName.Should().Be("Profile 1");
|
||||
result[1].ProfileName.Should().Be("Profile 2");
|
||||
result[0].SqlJobs.Should().NotBeNull();
|
||||
result[0].SqlJobs!.Count.Should().Be(1);
|
||||
result[1].SqlJobs.Should().NotBeNull();
|
||||
result[1].SqlJobs!.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNoActiveProfiles_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
_mockProfileRepo
|
||||
.Setup(r => r.GetAllActiveWithSqlJobsAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<CfgProfile>());
|
||||
|
||||
var query = new GetAllActiveProfilesQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithIncludeSqlJobsFalse_DoesNotLoadSqlJobs()
|
||||
{
|
||||
// Arrange
|
||||
var profiles = new List<CfgProfile>
|
||||
{
|
||||
new CfgProfile
|
||||
{
|
||||
Id = 1,
|
||||
Active = true,
|
||||
ProfileName = "Profile 1",
|
||||
TypeId = 2,
|
||||
Schedule = "0 30 4 ? * MON-SAT",
|
||||
AddedWho = "User1",
|
||||
AddedWhen = DateTime.Now
|
||||
}
|
||||
};
|
||||
|
||||
_mockProfileRepo
|
||||
.Setup(r => r.FindAsync(It.IsAny<Expression<Func<CfgProfile, bool>>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(profiles);
|
||||
|
||||
var query = new GetAllActiveProfilesQuery { IncludeSqlJobs = false };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Count.Should().Be(1);
|
||||
result[0].SqlJobs.Should().BeNull();
|
||||
_mockProfileRepo.Verify(
|
||||
r => r.FindAsync(It.IsAny<Expression<Func<CfgProfile, bool>>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
_mockProfileRepo.Verify(
|
||||
r => r.GetAllActiveWithSqlJobsAsync(It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ 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.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using MediatR;
|
||||
|
||||
Reference in New Issue
Block a user