test(backend): Add Issue Management integration tests + fix multi-tenant isolation
Created comprehensive integration test suite for Issue Management Module with 8 test cases covering all CRUD operations, status changes, assignments, and multi-tenant isolation. Test Cases (8/8): 1. Create Issue (Story type) 2. Create Issue (Task type) 3. Create Issue (Bug type) 4. Get Issue by ID 5. List Issues 6. Change Issue Status (Kanban workflow) 7. Assign Issue to User 8. Multi-Tenant Isolation (CRITICAL security test) Bug Fix: Multi-Tenant Data Leakage - Issue: IssueRepository did not filter by TenantId, allowing cross-tenant data access - Solution: Implemented TenantContext service and added TenantId filtering to all repository queries - Security Impact: CRITICAL - prevents unauthorized access to other tenants' issues Changes: - Added ColaFlow.Modules.IssueManagement.IntegrationTests project - Added IssueManagementWebApplicationFactory for test infrastructure - Added TestAuthHelper for JWT token generation in tests - Added 8 comprehensive integration tests - Added ITenantContext and TenantContext services for tenant isolation - Updated IssueRepository to filter all queries by current tenant ID - Registered TenantContext in module DI configuration Test Status: 7/8 passed initially, 8/8 expected after multi-tenant fix Test Framework: xUnit + FluentAssertions + WebApplicationFactory Database: In-Memory (for fast, isolated tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,33 +2,38 @@ using Microsoft.EntityFrameworkCore;
|
||||
using ColaFlow.Modules.IssueManagement.Domain.Entities;
|
||||
using ColaFlow.Modules.IssueManagement.Domain.Enums;
|
||||
using ColaFlow.Modules.IssueManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.IssueManagement.Infrastructure.Services;
|
||||
|
||||
namespace ColaFlow.Modules.IssueManagement.Infrastructure.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository implementation for Issue aggregate
|
||||
/// Repository implementation for Issue aggregate with multi-tenant isolation
|
||||
/// </summary>
|
||||
public sealed class IssueRepository : IIssueRepository
|
||||
{
|
||||
private readonly IssueManagementDbContext _context;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
|
||||
public IssueRepository(IssueManagementDbContext context)
|
||||
public IssueRepository(IssueManagementDbContext context, ITenantContext tenantContext)
|
||||
{
|
||||
_context = context;
|
||||
_tenantContext = tenantContext;
|
||||
}
|
||||
|
||||
private Guid CurrentTenantId => _tenantContext.GetCurrentTenantId();
|
||||
|
||||
public async Task<Issue?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Issues
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
|
||||
.FirstOrDefaultAsync(i => i.Id == id && i.TenantId == CurrentTenantId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Issue>> GetByProjectIdAsync(Guid projectId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Issues
|
||||
.AsNoTracking()
|
||||
.Where(i => i.ProjectId == projectId)
|
||||
.Where(i => i.ProjectId == projectId && i.TenantId == CurrentTenantId)
|
||||
.OrderBy(i => i.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
@@ -40,7 +45,7 @@ public sealed class IssueRepository : IIssueRepository
|
||||
{
|
||||
return await _context.Issues
|
||||
.AsNoTracking()
|
||||
.Where(i => i.ProjectId == projectId && i.Status == status)
|
||||
.Where(i => i.ProjectId == projectId && i.Status == status && i.TenantId == CurrentTenantId)
|
||||
.OrderBy(i => i.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
@@ -49,7 +54,7 @@ public sealed class IssueRepository : IIssueRepository
|
||||
{
|
||||
return await _context.Issues
|
||||
.AsNoTracking()
|
||||
.Where(i => i.AssigneeId == assigneeId)
|
||||
.Where(i => i.AssigneeId == assigneeId && i.TenantId == CurrentTenantId)
|
||||
.OrderBy(i => i.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
@@ -73,6 +78,6 @@ public sealed class IssueRepository : IIssueRepository
|
||||
|
||||
public async Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Issues.AnyAsync(i => i.Id == id, cancellationToken);
|
||||
return await _context.Issues.AnyAsync(i => i.Id == id && i.TenantId == CurrentTenantId, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ColaFlow.Modules.IssueManagement.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the current tenant context
|
||||
/// </summary>
|
||||
public interface ITenantContext
|
||||
{
|
||||
Guid GetCurrentTenantId();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace ColaFlow.Modules.IssueManagement.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of ITenantContext that retrieves tenant ID from JWT claims
|
||||
/// </summary>
|
||||
public sealed class TenantContext : ITenantContext
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public TenantContext(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public Guid GetCurrentTenantId()
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext == null)
|
||||
throw new InvalidOperationException("HTTP context is not available");
|
||||
|
||||
var user = httpContext.User;
|
||||
var tenantClaim = user.FindFirst("tenant_id") ?? user.FindFirst("tenantId");
|
||||
|
||||
if (tenantClaim == null || !Guid.TryParse(tenantClaim.Value, out var tenantId))
|
||||
throw new UnauthorizedAccessException("Tenant ID not found in claims");
|
||||
|
||||
return tenantId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user