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:
Yaojia Wang
2025-11-04 20:30:24 +01:00
parent 07407fa79c
commit 6046bad12e
16 changed files with 192 additions and 26 deletions

View File

@@ -11,14 +11,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteStory;
/// </summary>
public sealed class DeleteStoryCommandHandler(
IProjectRepository projectRepository,
IUnitOfWork unitOfWork)
IUnitOfWork unitOfWork,
ITenantContext tenantContext)
: IRequestHandler<DeleteStoryCommand, Unit>
{
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
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)
{
// Get current tenant ID (Defense in Depth - Layer 2)
var currentTenantId = _tenantContext.GetCurrentTenantId();
// Get the project with story (Global Query Filter ensures tenant isolation)
var storyId = StoryId.From(request.StoryId);
var project = await _projectRepository.GetProjectWithStoryAsync(storyId, cancellationToken);
@@ -26,6 +31,10 @@ public sealed class DeleteStoryCommandHandler(
if (project == null)
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
var epic = project.Epics.FirstOrDefault(e => e.Stories.Any(s => s.Id.Value == request.StoryId));
if (epic == null)

View File

@@ -12,14 +12,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteTask;
/// </summary>
public sealed class DeleteTaskCommandHandler(
IProjectRepository projectRepository,
IUnitOfWork unitOfWork)
IUnitOfWork unitOfWork,
ITenantContext tenantContext)
: IRequestHandler<DeleteTaskCommand, Unit>
{
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
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)
{
// 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)
var taskId = TaskId.From(request.TaskId);
var project = await _projectRepository.GetProjectWithTaskAsync(taskId, cancellationToken);
@@ -27,6 +32,10 @@ public sealed class DeleteTaskCommandHandler(
if (project == null)
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
Story? parentStory = null;
foreach (var epic in project.Epics)

View File

@@ -12,14 +12,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
/// </summary>
public sealed class UpdateEpicCommandHandler(
IProjectRepository projectRepository,
IUnitOfWork unitOfWork)
IUnitOfWork unitOfWork,
ITenantContext tenantContext)
: IRequestHandler<UpdateEpicCommand, EpicDto>
{
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
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)
{
// 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)
var epicId = EpicId.From(request.EpicId);
var project = await _projectRepository.GetProjectWithEpicAsync(epicId, cancellationToken);
@@ -27,6 +32,10 @@ public sealed class UpdateEpicCommandHandler(
if (project == null)
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
var epic = project.Epics.FirstOrDefault(e => e.Id == epicId);
if (epic == null)

View File

@@ -12,14 +12,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateStory;
/// </summary>
public sealed class UpdateStoryCommandHandler(
IProjectRepository projectRepository,
IUnitOfWork unitOfWork)
IUnitOfWork unitOfWork,
ITenantContext tenantContext)
: IRequestHandler<UpdateStoryCommand, StoryDto>
{
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
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)
{
// Get current tenant ID (Defense in Depth - Layer 2)
var currentTenantId = _tenantContext.GetCurrentTenantId();
// Get the project with story (Global Query Filter ensures tenant isolation)
var storyId = StoryId.From(request.StoryId);
var project = await _projectRepository.GetProjectWithStoryAsync(storyId, cancellationToken);
@@ -27,6 +32,10 @@ public sealed class UpdateStoryCommandHandler(
if (project == null)
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
var story = project.Epics
.SelectMany(e => e.Stories)

View File

@@ -13,14 +13,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateTask;
/// </summary>
public sealed class UpdateTaskCommandHandler(
IProjectRepository projectRepository,
IUnitOfWork unitOfWork)
IUnitOfWork unitOfWork,
ITenantContext tenantContext)
: IRequestHandler<UpdateTaskCommand, TaskDto>
{
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
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)
{
// 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)
var taskId = TaskId.From(request.TaskId);
var project = await _projectRepository.GetProjectWithTaskAsync(taskId, cancellationToken);
@@ -28,6 +33,10 @@ public sealed class UpdateTaskCommandHandler(
if (project == null)
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
WorkTask? task = null;
foreach (var epic in project.Epics)

View File

@@ -11,13 +11,18 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicById;
/// Handler for GetEpicByIdQuery
/// </summary>
public sealed class GetEpicByIdQueryHandler(
IProjectRepository projectRepository)
IProjectRepository projectRepository,
ITenantContext tenantContext)
: IRequestHandler<GetEpicByIdQuery, EpicDto>
{
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)
{
// Get current tenant ID (Defense in Depth - Layer 2)
var currentTenantId = _tenantContext.GetCurrentTenantId();
// Use read-only method for query (AsNoTracking for better performance)
var epicId = EpicId.From(request.EpicId);
var epic = await _projectRepository.GetEpicByIdReadOnlyAsync(epicId, cancellationToken);
@@ -25,6 +30,12 @@ public sealed class GetEpicByIdQueryHandler(
if (epic == null)
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
{
Id = epic.Id.Value,

View File

@@ -1,5 +1,6 @@
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
@@ -9,15 +10,31 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicsByProje
/// <summary>
/// Handler for GetEpicsByProjectIdQuery
/// </summary>
public sealed class GetEpicsByProjectIdQueryHandler(IProjectRepository projectRepository)
public sealed class GetEpicsByProjectIdQueryHandler(
IProjectRepository projectRepository,
ITenantContext tenantContext)
: IRequestHandler<GetEpicsByProjectIdQuery, List<EpicDto>>
{
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)
{
// 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 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);
return epics.Select(epic => new EpicDto

View File

@@ -1,5 +1,6 @@
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
@@ -9,15 +10,31 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoriesByEpi
/// <summary>
/// Handler for GetStoriesByEpicIdQuery
/// </summary>
public sealed class GetStoriesByEpicIdQueryHandler(IProjectRepository projectRepository)
public sealed class GetStoriesByEpicIdQueryHandler(
IProjectRepository projectRepository,
ITenantContext tenantContext)
: IRequestHandler<GetStoriesByEpicIdQuery, List<StoryDto>>
{
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)
{
// 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 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);
// Map stories to DTOs

View File

@@ -1,5 +1,6 @@
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
@@ -9,13 +10,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoryById;
/// <summary>
/// Handler for GetStoryByIdQuery
/// </summary>
public sealed class GetStoryByIdQueryHandler(IProjectRepository projectRepository)
public sealed class GetStoryByIdQueryHandler(
IProjectRepository projectRepository,
ITenantContext tenantContext)
: IRequestHandler<GetStoryByIdQuery, StoryDto>
{
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)
{
// Get current tenant ID (Defense in Depth - Layer 2)
var currentTenantId = _tenantContext.GetCurrentTenantId();
// Use read-only method for query (AsNoTracking for better performance)
var storyId = StoryId.From(request.StoryId);
var story = await _projectRepository.GetStoryByIdReadOnlyAsync(storyId, cancellationToken);
@@ -23,6 +30,10 @@ public sealed class GetStoryByIdQueryHandler(IProjectRepository projectRepositor
if (story == null)
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
return new StoryDto
{

View File

@@ -1,5 +1,6 @@
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
@@ -10,13 +11,19 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetTaskById;
/// <summary>
/// Handler for GetTaskByIdQuery
/// </summary>
public sealed class GetTaskByIdQueryHandler(IProjectRepository projectRepository)
public sealed class GetTaskByIdQueryHandler(
IProjectRepository projectRepository,
ITenantContext tenantContext)
: IRequestHandler<GetTaskByIdQuery, TaskDto>
{
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)
{
// Get current tenant ID (Defense in Depth - Layer 2)
var currentTenantId = _tenantContext.GetCurrentTenantId();
// Use read-only method for query (AsNoTracking for better performance)
var taskId = TaskId.From(request.TaskId);
var task = await _projectRepository.GetTaskByIdReadOnlyAsync(taskId, cancellationToken);
@@ -24,6 +31,10 @@ public sealed class GetTaskByIdQueryHandler(IProjectRepository projectRepository
if (task == null)
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
return new TaskDto
{

View File

@@ -1,5 +1,6 @@
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
@@ -9,15 +10,31 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetTasksByStory
/// <summary>
/// Handler for GetTasksByStoryIdQuery
/// </summary>
public sealed class GetTasksByStoryIdQueryHandler(IProjectRepository projectRepository)
public sealed class GetTasksByStoryIdQueryHandler(
IProjectRepository projectRepository,
ITenantContext tenantContext)
: IRequestHandler<GetTasksByStoryIdQuery, List<TaskDto>>
{
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)
{
// 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 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);
// Map tasks to DTOs

View File

@@ -1,6 +1,7 @@
using FluentAssertions;
using Moq;
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.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
@@ -12,13 +13,19 @@ public class DeleteStoryCommandHandlerTests
{
private readonly Mock<IProjectRepository> _projectRepositoryMock;
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly Mock<ITenantContext> _tenantContextMock;
private readonly DeleteStoryCommandHandler _handler;
private readonly Guid _tenantId;
public DeleteStoryCommandHandlerTests()
{
_projectRepositoryMock = new Mock<IProjectRepository>();
_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]
@@ -26,7 +33,7 @@ public class DeleteStoryCommandHandlerTests
{
// Arrange
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 story = epic.CreateStory("Story to Delete", "Description", TaskPriority.Medium, userId);
var storyId = story.Id;
@@ -70,7 +77,7 @@ public class DeleteStoryCommandHandlerTests
{
// Arrange
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 story = epic.CreateStory("Story with Tasks", "Description", TaskPriority.Medium, userId);

View File

@@ -1,6 +1,7 @@
using FluentAssertions;
using Moq;
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.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
@@ -12,13 +13,19 @@ public class DeleteTaskCommandHandlerTests
{
private readonly Mock<IProjectRepository> _projectRepositoryMock;
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly Mock<ITenantContext> _tenantContextMock;
private readonly DeleteTaskCommandHandler _handler;
private readonly Guid _tenantId;
public DeleteTaskCommandHandlerTests()
{
_projectRepositoryMock = new Mock<IProjectRepository>();
_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]
@@ -26,7 +33,7 @@ public class DeleteTaskCommandHandlerTests
{
// Arrange
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 story = epic.CreateStory("Test Story", "Story Description", TaskPriority.Medium, userId);
var task = story.CreateTask("Task to Delete", "Description", TaskPriority.Medium, userId);

View File

@@ -1,6 +1,7 @@
using FluentAssertions;
using Moq;
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.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
@@ -12,13 +13,19 @@ public class UpdateStoryCommandHandlerTests
{
private readonly Mock<IProjectRepository> _projectRepositoryMock;
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly Mock<ITenantContext> _tenantContextMock;
private readonly UpdateStoryCommandHandler _handler;
private readonly Guid _tenantId;
public UpdateStoryCommandHandlerTests()
{
_projectRepositoryMock = new Mock<IProjectRepository>();
_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]
@@ -26,7 +33,7 @@ public class UpdateStoryCommandHandlerTests
{
// Arrange
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 story = epic.CreateStory("Original Title", "Original Description", TaskPriority.Low, userId);
var storyId = story.Id;
@@ -89,7 +96,7 @@ public class UpdateStoryCommandHandlerTests
{
// Arrange
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 story = epic.CreateStory("Original", "Original", TaskPriority.Low, userId);

View File

@@ -1,6 +1,7 @@
using FluentAssertions;
using Moq;
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.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
@@ -11,12 +12,18 @@ namespace ColaFlow.Application.Tests.Queries.GetStoryById;
public class GetStoryByIdQueryHandlerTests
{
private readonly Mock<IProjectRepository> _projectRepositoryMock;
private readonly Mock<ITenantContext> _tenantContextMock;
private readonly GetStoryByIdQueryHandler _handler;
private readonly Guid _tenantId;
public GetStoryByIdQueryHandlerTests()
{
_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]
@@ -24,7 +31,7 @@ public class GetStoryByIdQueryHandlerTests
{
// Arrange
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 story = epic.CreateStory("Test Story", "Story Description", TaskPriority.High, userId);
var task1 = story.CreateTask("Task 1", "Description 1", TaskPriority.Medium, userId);

View File

@@ -1,6 +1,7 @@
using FluentAssertions;
using Moq;
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.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
@@ -11,12 +12,20 @@ namespace ColaFlow.Application.Tests.Queries.GetTaskById;
public class GetTaskByIdQueryHandlerTests
{
private readonly Mock<IProjectRepository> _projectRepositoryMock;
private readonly Mock<ITenantContext> _tenantContextMock;
private readonly GetTaskByIdQueryHandler _handler;
private readonly Guid _tenantId;
public GetTaskByIdQueryHandlerTests()
{
_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]
@@ -24,7 +33,7 @@ public class GetTaskByIdQueryHandlerTests
{
// Arrange
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 story = epic.CreateStory("Test Story", "Story Description", TaskPriority.Medium, userId);
var task = story.CreateTask("Test Task", "Task Description", TaskPriority.High, userId);