fix(backend): Add explicit TenantId validation to Epic/Story/Task Query/Command Handlers
CRITICAL SECURITY FIX: Implemented Defense in Depth security pattern by adding explicit TenantId verification to all Epic/Story/Task Query and Command Handlers. Security Impact: - BEFORE: Relied solely on EF Core global query filters (single layer) - AFTER: Explicit TenantId validation + EF Core filters (defense in depth) This ensures that even if EF Core query filters are accidentally disabled or bypassed, tenant isolation is still maintained at the application layer. Changes: Query Handlers (6 handlers): - GetEpicByIdQueryHandler: Added ITenantContext injection + explicit TenantId check - GetStoryByIdQueryHandler: Added ITenantContext injection + explicit TenantId check - GetTaskByIdQueryHandler: Added ITenantContext injection + explicit TenantId check - GetEpicsByProjectIdQueryHandler: Verify Project.TenantId before querying Epics - GetStoriesByEpicIdQueryHandler: Verify Epic.TenantId before querying Stories - GetTasksByStoryIdQueryHandler: Verify Story.TenantId before querying Tasks Command Handlers (5 handlers): - UpdateEpicCommandHandler: Verify Project.TenantId before updating - UpdateStoryCommandHandler: Verify Project.TenantId before updating - UpdateTaskCommandHandler: Verify Project.TenantId before updating - DeleteStoryCommandHandler: Verify Project.TenantId before deleting - DeleteTaskCommandHandler: Verify Project.TenantId before deleting Unit Tests: - Updated 5 unit test files to mock ITenantContext - All 32 unit tests passing - All 7 multi-tenant isolation integration tests passing Defense Layers (Security in Depth): Layer 1: EF Core global query filters (database level) Layer 2: Application-layer explicit TenantId validation (handler level) Layer 3: Integration tests verifying tenant isolation (test level) Test Results: - Unit Tests: 32/32 PASSING - Integration Tests: 7/7 PASSING (multi-tenant isolation) This fix addresses a critical security vulnerability where we relied on a single layer of defense (EF Core query filters) for tenant data isolation. Now we have multiple layers ensuring no cross-tenant data leaks can occur. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,14 +11,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteStory;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DeleteStoryCommandHandler(
|
public sealed class DeleteStoryCommandHandler(
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
IUnitOfWork unitOfWork)
|
IUnitOfWork unitOfWork,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<DeleteStoryCommand, Unit>
|
: IRequestHandler<DeleteStoryCommand, Unit>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<Unit> Handle(DeleteStoryCommand request, CancellationToken cancellationToken)
|
public async Task<Unit> Handle(DeleteStoryCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
// Get the project with story (Global Query Filter ensures tenant isolation)
|
// Get the project with story (Global Query Filter ensures tenant isolation)
|
||||||
var storyId = StoryId.From(request.StoryId);
|
var storyId = StoryId.From(request.StoryId);
|
||||||
var project = await _projectRepository.GetProjectWithStoryAsync(storyId, cancellationToken);
|
var project = await _projectRepository.GetProjectWithStoryAsync(storyId, cancellationToken);
|
||||||
@@ -26,6 +31,10 @@ public sealed class DeleteStoryCommandHandler(
|
|||||||
if (project == null)
|
if (project == null)
|
||||||
throw new NotFoundException("Story", request.StoryId);
|
throw new NotFoundException("Story", request.StoryId);
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (project.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Story", request.StoryId);
|
||||||
|
|
||||||
// Find the epic containing the story
|
// Find the epic containing the story
|
||||||
var epic = project.Epics.FirstOrDefault(e => e.Stories.Any(s => s.Id.Value == request.StoryId));
|
var epic = project.Epics.FirstOrDefault(e => e.Stories.Any(s => s.Id.Value == request.StoryId));
|
||||||
if (epic == null)
|
if (epic == null)
|
||||||
|
|||||||
@@ -12,14 +12,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteTask;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DeleteTaskCommandHandler(
|
public sealed class DeleteTaskCommandHandler(
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
IUnitOfWork unitOfWork)
|
IUnitOfWork unitOfWork,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<DeleteTaskCommand, Unit>
|
: IRequestHandler<DeleteTaskCommand, Unit>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<Unit> Handle(DeleteTaskCommand request, CancellationToken cancellationToken)
|
public async Task<Unit> Handle(DeleteTaskCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
// Get the project containing the task (Global Query Filter ensures tenant isolation)
|
// Get the project containing the task (Global Query Filter ensures tenant isolation)
|
||||||
var taskId = TaskId.From(request.TaskId);
|
var taskId = TaskId.From(request.TaskId);
|
||||||
var project = await _projectRepository.GetProjectWithTaskAsync(taskId, cancellationToken);
|
var project = await _projectRepository.GetProjectWithTaskAsync(taskId, cancellationToken);
|
||||||
@@ -27,6 +32,10 @@ public sealed class DeleteTaskCommandHandler(
|
|||||||
if (project == null)
|
if (project == null)
|
||||||
throw new NotFoundException("Task", request.TaskId);
|
throw new NotFoundException("Task", request.TaskId);
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (project.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Task", request.TaskId);
|
||||||
|
|
||||||
// Find the story containing the task
|
// Find the story containing the task
|
||||||
Story? parentStory = null;
|
Story? parentStory = null;
|
||||||
foreach (var epic in project.Epics)
|
foreach (var epic in project.Epics)
|
||||||
|
|||||||
@@ -12,14 +12,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UpdateEpicCommandHandler(
|
public sealed class UpdateEpicCommandHandler(
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
IUnitOfWork unitOfWork)
|
IUnitOfWork unitOfWork,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<UpdateEpicCommand, EpicDto>
|
: IRequestHandler<UpdateEpicCommand, EpicDto>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<EpicDto> Handle(UpdateEpicCommand request, CancellationToken cancellationToken)
|
public async Task<EpicDto> Handle(UpdateEpicCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
// Get the project containing the epic (Global Query Filter ensures tenant isolation)
|
// Get the project containing the epic (Global Query Filter ensures tenant isolation)
|
||||||
var epicId = EpicId.From(request.EpicId);
|
var epicId = EpicId.From(request.EpicId);
|
||||||
var project = await _projectRepository.GetProjectWithEpicAsync(epicId, cancellationToken);
|
var project = await _projectRepository.GetProjectWithEpicAsync(epicId, cancellationToken);
|
||||||
@@ -27,6 +32,10 @@ public sealed class UpdateEpicCommandHandler(
|
|||||||
if (project == null)
|
if (project == null)
|
||||||
throw new NotFoundException("Epic", request.EpicId);
|
throw new NotFoundException("Epic", request.EpicId);
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (project.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Epic", request.EpicId);
|
||||||
|
|
||||||
// Find the epic
|
// Find the epic
|
||||||
var epic = project.Epics.FirstOrDefault(e => e.Id == epicId);
|
var epic = project.Epics.FirstOrDefault(e => e.Id == epicId);
|
||||||
if (epic == null)
|
if (epic == null)
|
||||||
|
|||||||
@@ -12,14 +12,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateStory;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UpdateStoryCommandHandler(
|
public sealed class UpdateStoryCommandHandler(
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
IUnitOfWork unitOfWork)
|
IUnitOfWork unitOfWork,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<UpdateStoryCommand, StoryDto>
|
: IRequestHandler<UpdateStoryCommand, StoryDto>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<StoryDto> Handle(UpdateStoryCommand request, CancellationToken cancellationToken)
|
public async Task<StoryDto> Handle(UpdateStoryCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
// Get the project with story (Global Query Filter ensures tenant isolation)
|
// Get the project with story (Global Query Filter ensures tenant isolation)
|
||||||
var storyId = StoryId.From(request.StoryId);
|
var storyId = StoryId.From(request.StoryId);
|
||||||
var project = await _projectRepository.GetProjectWithStoryAsync(storyId, cancellationToken);
|
var project = await _projectRepository.GetProjectWithStoryAsync(storyId, cancellationToken);
|
||||||
@@ -27,6 +32,10 @@ public sealed class UpdateStoryCommandHandler(
|
|||||||
if (project == null)
|
if (project == null)
|
||||||
throw new NotFoundException("Story", request.StoryId);
|
throw new NotFoundException("Story", request.StoryId);
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (project.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Story", request.StoryId);
|
||||||
|
|
||||||
// Find the story
|
// Find the story
|
||||||
var story = project.Epics
|
var story = project.Epics
|
||||||
.SelectMany(e => e.Stories)
|
.SelectMany(e => e.Stories)
|
||||||
|
|||||||
@@ -13,14 +13,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateTask;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UpdateTaskCommandHandler(
|
public sealed class UpdateTaskCommandHandler(
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
IUnitOfWork unitOfWork)
|
IUnitOfWork unitOfWork,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<UpdateTaskCommand, TaskDto>
|
: IRequestHandler<UpdateTaskCommand, TaskDto>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<TaskDto> Handle(UpdateTaskCommand request, CancellationToken cancellationToken)
|
public async Task<TaskDto> Handle(UpdateTaskCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
// Get the project containing the task (Global Query Filter ensures tenant isolation)
|
// Get the project containing the task (Global Query Filter ensures tenant isolation)
|
||||||
var taskId = TaskId.From(request.TaskId);
|
var taskId = TaskId.From(request.TaskId);
|
||||||
var project = await _projectRepository.GetProjectWithTaskAsync(taskId, cancellationToken);
|
var project = await _projectRepository.GetProjectWithTaskAsync(taskId, cancellationToken);
|
||||||
@@ -28,6 +33,10 @@ public sealed class UpdateTaskCommandHandler(
|
|||||||
if (project == null)
|
if (project == null)
|
||||||
throw new NotFoundException("Task", request.TaskId);
|
throw new NotFoundException("Task", request.TaskId);
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (project.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Task", request.TaskId);
|
||||||
|
|
||||||
// Find the task within the project aggregate
|
// Find the task within the project aggregate
|
||||||
WorkTask? task = null;
|
WorkTask? task = null;
|
||||||
foreach (var epic in project.Epics)
|
foreach (var epic in project.Epics)
|
||||||
|
|||||||
@@ -11,13 +11,18 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicById;
|
|||||||
/// Handler for GetEpicByIdQuery
|
/// Handler for GetEpicByIdQuery
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetEpicByIdQueryHandler(
|
public sealed class GetEpicByIdQueryHandler(
|
||||||
IProjectRepository projectRepository)
|
IProjectRepository projectRepository,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<GetEpicByIdQuery, EpicDto>
|
: IRequestHandler<GetEpicByIdQuery, EpicDto>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<EpicDto> Handle(GetEpicByIdQuery request, CancellationToken cancellationToken)
|
public async Task<EpicDto> Handle(GetEpicByIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
// Use read-only method for query (AsNoTracking for better performance)
|
// Use read-only method for query (AsNoTracking for better performance)
|
||||||
var epicId = EpicId.From(request.EpicId);
|
var epicId = EpicId.From(request.EpicId);
|
||||||
var epic = await _projectRepository.GetEpicByIdReadOnlyAsync(epicId, cancellationToken);
|
var epic = await _projectRepository.GetEpicByIdReadOnlyAsync(epicId, cancellationToken);
|
||||||
@@ -25,6 +30,12 @@ public sealed class GetEpicByIdQueryHandler(
|
|||||||
if (epic == null)
|
if (epic == null)
|
||||||
throw new NotFoundException("Epic", request.EpicId);
|
throw new NotFoundException("Epic", request.EpicId);
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Explicit TenantId validation (Defense in Depth)
|
||||||
|
// Even though EF Core global query filters should prevent cross-tenant access,
|
||||||
|
// we explicitly verify tenant ownership to ensure defense in depth.
|
||||||
|
if (epic.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Epic", request.EpicId);
|
||||||
|
|
||||||
return new EpicDto
|
return new EpicDto
|
||||||
{
|
{
|
||||||
Id = epic.Id.Value,
|
Id = epic.Id.Value,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||||
@@ -9,15 +10,31 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicsByProje
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for GetEpicsByProjectIdQuery
|
/// Handler for GetEpicsByProjectIdQuery
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetEpicsByProjectIdQueryHandler(IProjectRepository projectRepository)
|
public sealed class GetEpicsByProjectIdQueryHandler(
|
||||||
|
IProjectRepository projectRepository,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<GetEpicsByProjectIdQuery, List<EpicDto>>
|
: IRequestHandler<GetEpicsByProjectIdQuery, List<EpicDto>>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<List<EpicDto>> Handle(GetEpicsByProjectIdQuery request, CancellationToken cancellationToken)
|
public async Task<List<EpicDto>> Handle(GetEpicsByProjectIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Use read-only method for query (AsNoTracking for better performance)
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Verify Project belongs to current tenant before querying epics
|
||||||
var projectId = ProjectId.From(request.ProjectId);
|
var projectId = ProjectId.From(request.ProjectId);
|
||||||
|
var project = await _projectRepository.GetByIdAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
|
if (project == null)
|
||||||
|
throw new NotFoundException("Project", request.ProjectId);
|
||||||
|
|
||||||
|
// Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (project.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Project", request.ProjectId);
|
||||||
|
|
||||||
|
// Now fetch epics (already filtered by EF Core, but we verified project ownership)
|
||||||
var epics = await _projectRepository.GetEpicsByProjectIdAsync(projectId, cancellationToken);
|
var epics = await _projectRepository.GetEpicsByProjectIdAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
return epics.Select(epic => new EpicDto
|
return epics.Select(epic => new EpicDto
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||||
@@ -9,15 +10,31 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoriesByEpi
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for GetStoriesByEpicIdQuery
|
/// Handler for GetStoriesByEpicIdQuery
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetStoriesByEpicIdQueryHandler(IProjectRepository projectRepository)
|
public sealed class GetStoriesByEpicIdQueryHandler(
|
||||||
|
IProjectRepository projectRepository,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<GetStoriesByEpicIdQuery, List<StoryDto>>
|
: IRequestHandler<GetStoriesByEpicIdQuery, List<StoryDto>>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<List<StoryDto>> Handle(GetStoriesByEpicIdQuery request, CancellationToken cancellationToken)
|
public async Task<List<StoryDto>> Handle(GetStoriesByEpicIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Use read-only method for query (AsNoTracking for better performance)
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Verify Epic belongs to current tenant before querying stories
|
||||||
var epicId = EpicId.From(request.EpicId);
|
var epicId = EpicId.From(request.EpicId);
|
||||||
|
var epic = await _projectRepository.GetEpicByIdReadOnlyAsync(epicId, cancellationToken);
|
||||||
|
|
||||||
|
if (epic == null)
|
||||||
|
throw new NotFoundException("Epic", request.EpicId);
|
||||||
|
|
||||||
|
// Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (epic.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Epic", request.EpicId);
|
||||||
|
|
||||||
|
// Now fetch stories (already filtered by EF Core, but we verified epic ownership)
|
||||||
var stories = await _projectRepository.GetStoriesByEpicIdAsync(epicId, cancellationToken);
|
var stories = await _projectRepository.GetStoriesByEpicIdAsync(epicId, cancellationToken);
|
||||||
|
|
||||||
// Map stories to DTOs
|
// Map stories to DTOs
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||||
@@ -9,13 +10,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoryById;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for GetStoryByIdQuery
|
/// Handler for GetStoryByIdQuery
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetStoryByIdQueryHandler(IProjectRepository projectRepository)
|
public sealed class GetStoryByIdQueryHandler(
|
||||||
|
IProjectRepository projectRepository,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<GetStoryByIdQuery, StoryDto>
|
: IRequestHandler<GetStoryByIdQuery, StoryDto>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<StoryDto> Handle(GetStoryByIdQuery request, CancellationToken cancellationToken)
|
public async Task<StoryDto> Handle(GetStoryByIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
// Use read-only method for query (AsNoTracking for better performance)
|
// Use read-only method for query (AsNoTracking for better performance)
|
||||||
var storyId = StoryId.From(request.StoryId);
|
var storyId = StoryId.From(request.StoryId);
|
||||||
var story = await _projectRepository.GetStoryByIdReadOnlyAsync(storyId, cancellationToken);
|
var story = await _projectRepository.GetStoryByIdReadOnlyAsync(storyId, cancellationToken);
|
||||||
@@ -23,6 +30,10 @@ public sealed class GetStoryByIdQueryHandler(IProjectRepository projectRepositor
|
|||||||
if (story == null)
|
if (story == null)
|
||||||
throw new NotFoundException("Story", request.StoryId);
|
throw new NotFoundException("Story", request.StoryId);
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (story.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Story", request.StoryId);
|
||||||
|
|
||||||
// Map to DTO
|
// Map to DTO
|
||||||
return new StoryDto
|
return new StoryDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||||
@@ -10,13 +11,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetTaskById;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for GetTaskByIdQuery
|
/// Handler for GetTaskByIdQuery
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetTaskByIdQueryHandler(IProjectRepository projectRepository)
|
public sealed class GetTaskByIdQueryHandler(
|
||||||
|
IProjectRepository projectRepository,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<GetTaskByIdQuery, TaskDto>
|
: IRequestHandler<GetTaskByIdQuery, TaskDto>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<TaskDto> Handle(GetTaskByIdQuery request, CancellationToken cancellationToken)
|
public async Task<TaskDto> Handle(GetTaskByIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
// Use read-only method for query (AsNoTracking for better performance)
|
// Use read-only method for query (AsNoTracking for better performance)
|
||||||
var taskId = TaskId.From(request.TaskId);
|
var taskId = TaskId.From(request.TaskId);
|
||||||
var task = await _projectRepository.GetTaskByIdReadOnlyAsync(taskId, cancellationToken);
|
var task = await _projectRepository.GetTaskByIdReadOnlyAsync(taskId, cancellationToken);
|
||||||
@@ -24,6 +31,10 @@ public sealed class GetTaskByIdQueryHandler(IProjectRepository projectRepository
|
|||||||
if (task == null)
|
if (task == null)
|
||||||
throw new NotFoundException("Task", request.TaskId);
|
throw new NotFoundException("Task", request.TaskId);
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (task.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Task", request.TaskId);
|
||||||
|
|
||||||
// Map to DTO
|
// Map to DTO
|
||||||
return new TaskDto
|
return new TaskDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||||
@@ -9,15 +10,31 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetTasksByStory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for GetTasksByStoryIdQuery
|
/// Handler for GetTasksByStoryIdQuery
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetTasksByStoryIdQueryHandler(IProjectRepository projectRepository)
|
public sealed class GetTasksByStoryIdQueryHandler(
|
||||||
|
IProjectRepository projectRepository,
|
||||||
|
ITenantContext tenantContext)
|
||||||
: IRequestHandler<GetTasksByStoryIdQuery, List<TaskDto>>
|
: IRequestHandler<GetTasksByStoryIdQuery, List<TaskDto>>
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
|
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
|
||||||
public async Task<List<TaskDto>> Handle(GetTasksByStoryIdQuery request, CancellationToken cancellationToken)
|
public async Task<List<TaskDto>> Handle(GetTasksByStoryIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Use read-only method for query (AsNoTracking for better performance)
|
// Get current tenant ID (Defense in Depth - Layer 2)
|
||||||
|
var currentTenantId = _tenantContext.GetCurrentTenantId();
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: Verify Story belongs to current tenant before querying tasks
|
||||||
var storyId = StoryId.From(request.StoryId);
|
var storyId = StoryId.From(request.StoryId);
|
||||||
|
var story = await _projectRepository.GetStoryByIdReadOnlyAsync(storyId, cancellationToken);
|
||||||
|
|
||||||
|
if (story == null)
|
||||||
|
throw new NotFoundException("Story", request.StoryId);
|
||||||
|
|
||||||
|
// Explicit TenantId validation (Defense in Depth)
|
||||||
|
if (story.TenantId.Value != currentTenantId)
|
||||||
|
throw new NotFoundException("Story", request.StoryId);
|
||||||
|
|
||||||
|
// Now fetch tasks (already filtered by EF Core, but we verified story ownership)
|
||||||
var tasks = await _projectRepository.GetTasksByStoryIdAsync(storyId, cancellationToken);
|
var tasks = await _projectRepository.GetTasksByStoryIdAsync(storyId, cancellationToken);
|
||||||
|
|
||||||
// Map tasks to DTOs
|
// Map tasks to DTOs
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteStory;
|
using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteStory;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
@@ -12,13 +13,19 @@ public class DeleteStoryCommandHandlerTests
|
|||||||
{
|
{
|
||||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||||
|
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||||
private readonly DeleteStoryCommandHandler _handler;
|
private readonly DeleteStoryCommandHandler _handler;
|
||||||
|
private readonly Guid _tenantId;
|
||||||
|
|
||||||
public DeleteStoryCommandHandlerTests()
|
public DeleteStoryCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||||
_handler = new DeleteStoryCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object);
|
_tenantContextMock = new Mock<ITenantContext>();
|
||||||
|
_tenantId = Guid.NewGuid();
|
||||||
|
|
||||||
|
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||||
|
_handler = new DeleteStoryCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object, _tenantContextMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -26,7 +33,7 @@ public class DeleteStoryCommandHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = UserId.Create();
|
var userId = UserId.Create();
|
||||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||||
var story = epic.CreateStory("Story to Delete", "Description", TaskPriority.Medium, userId);
|
var story = epic.CreateStory("Story to Delete", "Description", TaskPriority.Medium, userId);
|
||||||
var storyId = story.Id;
|
var storyId = story.Id;
|
||||||
@@ -70,7 +77,7 @@ public class DeleteStoryCommandHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = UserId.Create();
|
var userId = UserId.Create();
|
||||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||||
var story = epic.CreateStory("Story with Tasks", "Description", TaskPriority.Medium, userId);
|
var story = epic.CreateStory("Story with Tasks", "Description", TaskPriority.Medium, userId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteTask;
|
using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteTask;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
@@ -12,13 +13,19 @@ public class DeleteTaskCommandHandlerTests
|
|||||||
{
|
{
|
||||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||||
|
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||||
private readonly DeleteTaskCommandHandler _handler;
|
private readonly DeleteTaskCommandHandler _handler;
|
||||||
|
private readonly Guid _tenantId;
|
||||||
|
|
||||||
public DeleteTaskCommandHandlerTests()
|
public DeleteTaskCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||||
_handler = new DeleteTaskCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object);
|
_tenantContextMock = new Mock<ITenantContext>();
|
||||||
|
_tenantId = Guid.NewGuid();
|
||||||
|
|
||||||
|
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||||
|
_handler = new DeleteTaskCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object, _tenantContextMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -26,7 +33,7 @@ public class DeleteTaskCommandHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = UserId.Create();
|
var userId = UserId.Create();
|
||||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||||
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.Medium, userId);
|
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.Medium, userId);
|
||||||
var task = story.CreateTask("Task to Delete", "Description", TaskPriority.Medium, userId);
|
var task = story.CreateTask("Task to Delete", "Description", TaskPriority.Medium, userId);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateStory;
|
using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateStory;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
@@ -12,13 +13,19 @@ public class UpdateStoryCommandHandlerTests
|
|||||||
{
|
{
|
||||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||||
|
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||||
private readonly UpdateStoryCommandHandler _handler;
|
private readonly UpdateStoryCommandHandler _handler;
|
||||||
|
private readonly Guid _tenantId;
|
||||||
|
|
||||||
public UpdateStoryCommandHandlerTests()
|
public UpdateStoryCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||||
_handler = new UpdateStoryCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object);
|
_tenantContextMock = new Mock<ITenantContext>();
|
||||||
|
_tenantId = Guid.NewGuid();
|
||||||
|
|
||||||
|
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||||
|
_handler = new UpdateStoryCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object, _tenantContextMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -26,7 +33,7 @@ public class UpdateStoryCommandHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = UserId.Create();
|
var userId = UserId.Create();
|
||||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||||
var story = epic.CreateStory("Original Title", "Original Description", TaskPriority.Low, userId);
|
var story = epic.CreateStory("Original Title", "Original Description", TaskPriority.Low, userId);
|
||||||
var storyId = story.Id;
|
var storyId = story.Id;
|
||||||
@@ -89,7 +96,7 @@ public class UpdateStoryCommandHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = UserId.Create();
|
var userId = UserId.Create();
|
||||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||||
var story = epic.CreateStory("Original", "Original", TaskPriority.Low, userId);
|
var story = epic.CreateStory("Original", "Original", TaskPriority.Low, userId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoryById;
|
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoryById;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
@@ -11,12 +12,18 @@ namespace ColaFlow.Application.Tests.Queries.GetStoryById;
|
|||||||
public class GetStoryByIdQueryHandlerTests
|
public class GetStoryByIdQueryHandlerTests
|
||||||
{
|
{
|
||||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||||
|
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||||
private readonly GetStoryByIdQueryHandler _handler;
|
private readonly GetStoryByIdQueryHandler _handler;
|
||||||
|
private readonly Guid _tenantId;
|
||||||
|
|
||||||
public GetStoryByIdQueryHandlerTests()
|
public GetStoryByIdQueryHandlerTests()
|
||||||
{
|
{
|
||||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||||
_handler = new GetStoryByIdQueryHandler(_projectRepositoryMock.Object);
|
_tenantContextMock = new Mock<ITenantContext>();
|
||||||
|
_tenantId = Guid.NewGuid();
|
||||||
|
|
||||||
|
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||||
|
_handler = new GetStoryByIdQueryHandler(_projectRepositoryMock.Object, _tenantContextMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -24,7 +31,7 @@ public class GetStoryByIdQueryHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = UserId.Create();
|
var userId = UserId.Create();
|
||||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||||
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.High, userId);
|
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.High, userId);
|
||||||
var task1 = story.CreateTask("Task 1", "Description 1", TaskPriority.Medium, userId);
|
var task1 = story.CreateTask("Task 1", "Description 1", TaskPriority.Medium, userId);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetTaskById;
|
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetTaskById;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
@@ -11,12 +12,20 @@ namespace ColaFlow.Application.Tests.Queries.GetTaskById;
|
|||||||
public class GetTaskByIdQueryHandlerTests
|
public class GetTaskByIdQueryHandlerTests
|
||||||
{
|
{
|
||||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||||
|
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||||
private readonly GetTaskByIdQueryHandler _handler;
|
private readonly GetTaskByIdQueryHandler _handler;
|
||||||
|
private readonly Guid _tenantId;
|
||||||
|
|
||||||
public GetTaskByIdQueryHandlerTests()
|
public GetTaskByIdQueryHandlerTests()
|
||||||
{
|
{
|
||||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||||
_handler = new GetTaskByIdQueryHandler(_projectRepositoryMock.Object);
|
_tenantContextMock = new Mock<ITenantContext>();
|
||||||
|
_tenantId = Guid.NewGuid();
|
||||||
|
|
||||||
|
// Setup default tenant context
|
||||||
|
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||||
|
|
||||||
|
_handler = new GetTaskByIdQueryHandler(_projectRepositoryMock.Object, _tenantContextMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -24,7 +33,7 @@ public class GetTaskByIdQueryHandlerTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = UserId.Create();
|
var userId = UserId.Create();
|
||||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||||
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.Medium, userId);
|
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.Medium, userId);
|
||||||
var task = story.CreateTask("Test Task", "Task Description", TaskPriority.High, userId);
|
var task = story.CreateTask("Test Task", "Task Description", TaskPriority.High, userId);
|
||||||
|
|||||||
Reference in New Issue
Block a user