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>
249 lines
9.2 KiB
C#
249 lines
9.2 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using FluentAssertions;
|
|
using ColaFlow.Modules.IssueManagement.Application.DTOs;
|
|
using ColaFlow.Modules.IssueManagement.Domain.Enums;
|
|
using ColaFlow.Modules.IssueManagement.IntegrationTests.Infrastructure;
|
|
using Xunit;
|
|
|
|
namespace ColaFlow.Modules.IssueManagement.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Integration tests for Issue Management Module
|
|
/// Tests all CRUD operations, status changes, assignments, and multi-tenant isolation
|
|
/// </summary>
|
|
public class IssueManagementIntegrationTests : IClassFixture<IssueManagementWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client;
|
|
private readonly Guid _projectId = Guid.NewGuid();
|
|
private readonly Guid _tenantId = Guid.NewGuid();
|
|
private readonly Guid _userId = Guid.NewGuid();
|
|
private readonly string _tenantSlug = "test-tenant";
|
|
private readonly string _userEmail = "test@example.com";
|
|
|
|
public IssueManagementIntegrationTests(IssueManagementWebApplicationFactory factory)
|
|
{
|
|
_client = factory.CreateClient();
|
|
_client.AddAuthHeader(_userId, _tenantId, _tenantSlug, _userEmail, "Member");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateIssue_WithStoryType_ShouldReturnCreatedIssue()
|
|
{
|
|
// Arrange
|
|
var request = new
|
|
{
|
|
Title = "Implement user authentication",
|
|
Description = "Add JWT authentication to the API",
|
|
Type = "Story",
|
|
Priority = "High"
|
|
};
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync($"/api/v1/projects/{_projectId}/issues", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
var issue = await response.Content.ReadFromJsonAsync<IssueDto>();
|
|
issue.Should().NotBeNull();
|
|
issue!.Title.Should().Be(request.Title);
|
|
issue.Description.Should().Be(request.Description);
|
|
issue.Type.Should().Be("Story");
|
|
issue.Priority.Should().Be("High");
|
|
issue.Status.Should().Be("Backlog");
|
|
issue.ProjectId.Should().Be(_projectId);
|
|
issue.TenantId.Should().Be(_tenantId);
|
|
issue.ReporterId.Should().Be(_userId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateIssue_WithTaskType_ShouldReturnCreatedIssue()
|
|
{
|
|
// Arrange
|
|
var request = new
|
|
{
|
|
Title = "Write unit tests",
|
|
Description = "Add unit tests for Issue Management module",
|
|
Type = "Task",
|
|
Priority = "Medium"
|
|
};
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync($"/api/v1/projects/{_projectId}/issues", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
var issue = await response.Content.ReadFromJsonAsync<IssueDto>();
|
|
issue.Should().NotBeNull();
|
|
issue!.Type.Should().Be("Task");
|
|
issue.Priority.Should().Be("Medium");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateIssue_WithBugType_ShouldReturnCreatedIssue()
|
|
{
|
|
// Arrange
|
|
var request = new
|
|
{
|
|
Title = "Fix login redirect issue",
|
|
Description = "Users are not redirected after login",
|
|
Type = "Bug",
|
|
Priority = "Critical"
|
|
};
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync($"/api/v1/projects/{_projectId}/issues", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
var issue = await response.Content.ReadFromJsonAsync<IssueDto>();
|
|
issue.Should().NotBeNull();
|
|
issue!.Type.Should().Be("Bug");
|
|
issue.Priority.Should().Be("Critical");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetIssueById_WithExistingIssue_ShouldReturnIssue()
|
|
{
|
|
// Arrange - Create an issue first
|
|
var createRequest = new
|
|
{
|
|
Title = "Test issue for retrieval",
|
|
Description = "This issue will be retrieved by ID",
|
|
Type = "Task",
|
|
Priority = "Low"
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync($"/api/v1/projects/{_projectId}/issues", createRequest);
|
|
var createdIssue = await createResponse.Content.ReadFromJsonAsync<IssueDto>();
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/v1/projects/{_projectId}/issues/{createdIssue!.Id}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var issue = await response.Content.ReadFromJsonAsync<IssueDto>();
|
|
issue.Should().NotBeNull();
|
|
issue!.Id.Should().Be(createdIssue.Id);
|
|
issue.Title.Should().Be(createRequest.Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListIssues_WithMultipleIssues_ShouldReturnAllIssues()
|
|
{
|
|
// Arrange - Create multiple issues
|
|
var issues = new[]
|
|
{
|
|
new { Title = "Issue 1", Description = "Description 1", Type = "Story", Priority = "High" },
|
|
new { Title = "Issue 2", Description = "Description 2", Type = "Task", Priority = "Medium" },
|
|
new { Title = "Issue 3", Description = "Description 3", Type = "Bug", Priority = "Low" }
|
|
};
|
|
|
|
foreach (var issueRequest in issues)
|
|
{
|
|
await _client.PostAsJsonAsync($"/api/v1/projects/{_projectId}/issues", issueRequest);
|
|
}
|
|
|
|
// Act
|
|
var response = await _client.GetAsync($"/api/v1/projects/{_projectId}/issues");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var retrievedIssues = await response.Content.ReadFromJsonAsync<List<IssueDto>>();
|
|
retrievedIssues.Should().NotBeNull();
|
|
retrievedIssues!.Count.Should().BeGreaterThanOrEqualTo(3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ChangeIssueStatus_FromBacklogToInProgress_ShouldUpdateStatus()
|
|
{
|
|
// Arrange - Create an issue
|
|
var createRequest = new
|
|
{
|
|
Title = "Issue for status change",
|
|
Description = "This issue will have its status changed",
|
|
Type = "Story",
|
|
Priority = "High"
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync($"/api/v1/projects/{_projectId}/issues", createRequest);
|
|
var createdIssue = await createResponse.Content.ReadFromJsonAsync<IssueDto>();
|
|
|
|
// Act - Change status to InProgress
|
|
var statusRequest = new
|
|
{
|
|
Status = "InProgress",
|
|
OldStatus = "Backlog"
|
|
};
|
|
var response = await _client.PutAsJsonAsync(
|
|
$"/api/v1/projects/{_projectId}/issues/{createdIssue!.Id}/status",
|
|
statusRequest);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
|
|
|
// Verify status was changed
|
|
var getResponse = await _client.GetAsync($"/api/v1/projects/{_projectId}/issues/{createdIssue.Id}");
|
|
var updatedIssue = await getResponse.Content.ReadFromJsonAsync<IssueDto>();
|
|
updatedIssue.Should().NotBeNull();
|
|
updatedIssue!.Status.Should().Be("InProgress");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AssignIssue_ToUser_ShouldUpdateAssignee()
|
|
{
|
|
// Arrange - Create an issue
|
|
var createRequest = new
|
|
{
|
|
Title = "Issue for assignment",
|
|
Description = "This issue will be assigned to a user",
|
|
Type = "Task",
|
|
Priority = "Medium"
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync($"/api/v1/projects/{_projectId}/issues", createRequest);
|
|
var createdIssue = await createResponse.Content.ReadFromJsonAsync<IssueDto>();
|
|
|
|
var assigneeId = Guid.NewGuid();
|
|
|
|
// Act - Assign issue to user
|
|
var assignRequest = new { AssigneeId = assigneeId };
|
|
var response = await _client.PutAsJsonAsync(
|
|
$"/api/v1/projects/{_projectId}/issues/{createdIssue!.Id}/assign",
|
|
assignRequest);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
|
|
|
// Verify assignment
|
|
var getResponse = await _client.GetAsync($"/api/v1/projects/{_projectId}/issues/{createdIssue.Id}");
|
|
var updatedIssue = await getResponse.Content.ReadFromJsonAsync<IssueDto>();
|
|
updatedIssue.Should().NotBeNull();
|
|
updatedIssue!.AssigneeId.Should().Be(assigneeId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MultiTenantIsolation_DifferentTenant_ShouldNotAccessIssues()
|
|
{
|
|
// Arrange - Create an issue with tenant A
|
|
var createRequest = new
|
|
{
|
|
Title = "Tenant A issue",
|
|
Description = "This issue belongs to Tenant A",
|
|
Type = "Story",
|
|
Priority = "High"
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync($"/api/v1/projects/{_projectId}/issues", createRequest);
|
|
var createdIssue = await createResponse.Content.ReadFromJsonAsync<IssueDto>();
|
|
|
|
// Act - Try to access with tenant B credentials
|
|
var tenantBId = Guid.NewGuid();
|
|
var tenantBClient = _client;
|
|
tenantBClient.DefaultRequestHeaders.Clear();
|
|
tenantBClient.AddAuthHeader(_userId, tenantBId, "tenant-b", _userEmail, "Member");
|
|
|
|
var response = await tenantBClient.GetAsync($"/api/v1/projects/{_projectId}/issues/{createdIssue!.Id}");
|
|
|
|
// Assert - Tenant B should not see Tenant A's issue
|
|
// The issue will return NotFound or the global query filter will exclude it
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
}
|