feat(backend): Add ProjectManagement integration test infrastructure + fix API controller

Created comprehensive integration test infrastructure for ProjectManagement module:
- PMWebApplicationFactory with in-memory database support
- TestAuthHelper for JWT token generation
- Test project with all necessary dependencies

Fixed API Controller:
- Removed manual TenantId injection in ProjectsController
- TenantId now automatically extracted via ITenantContext in CommandHandler
- Maintained OwnerId extraction from JWT claims

Test Infrastructure:
- In-memory database for fast, isolated tests
- Support for multi-tenant scenarios
- JWT authentication helpers
- Cross-module database consistency

Next Steps:
- Write multi-tenant isolation tests (Phase 3.2)
- Write CRUD integration tests (Phase 3.3)

🤖 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 19:56:49 +01:00
parent 99bd92a3ca
commit 4359c9f08f
4 changed files with 210 additions and 13 deletions

View File

@@ -57,14 +57,13 @@ public class ProjectsController(IMediator mediator) : ControllerBase
[FromBody] CreateProjectCommand command,
CancellationToken cancellationToken = default)
{
// Extract TenantId and UserId from JWT claims
var tenantId = GetTenantIdFromClaims();
// Extract UserId from JWT claims
// Note: TenantId is now automatically extracted in the CommandHandler via ITenantContext
var userId = GetUserIdFromClaims();
// Override command with authenticated user's context
// Override command with authenticated user's ID
var commandWithContext = command with
{
TenantId = tenantId,
OwnerId = userId
};
@@ -104,15 +103,7 @@ public class ProjectsController(IMediator mediator) : ControllerBase
return NoContent();
}
// Helper methods to extract claims
private Guid GetTenantIdFromClaims()
{
var tenantIdClaim = User.FindFirst("tenant_id")?.Value
?? throw new UnauthorizedAccessException("Tenant ID not found in token");
return Guid.Parse(tenantIdClaim);
}
// Helper method to extract user ID from claims
private Guid GetUserIdFromClaims()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value

View File

@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<!-- Web Application Factory for Integration Testing -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<!-- Database Providers -->
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<!-- Assertion Library -->
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<!-- JWT Token Handling -->
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<!-- Reference API Project -->
<ProjectReference Include="..\..\..\..\src\ColaFlow.API\ColaFlow.API.csproj" />
<!-- Reference ProjectManagement Module -->
<ProjectReference Include="..\..\..\..\src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Application\ColaFlow.Modules.ProjectManagement.Application.csproj" />
<ProjectReference Include="..\..\..\..\src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Infrastructure\ColaFlow.Modules.ProjectManagement.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\..\src\Modules\ProjectManagement\ColaFlow.Modules.ProjectManagement.Domain\ColaFlow.Modules.ProjectManagement.Domain.csproj" />
<!-- Reference Identity Module (for auth) -->
<ProjectReference Include="..\..\..\..\src\Modules\Identity\ColaFlow.Modules.Identity.Application\ColaFlow.Modules.Identity.Application.csproj" />
<ProjectReference Include="..\..\..\..\src\Modules\Identity\ColaFlow.Modules.Identity.Infrastructure\ColaFlow.Modules.Identity.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\..\src\Modules\Identity\ColaFlow.Modules.Identity.Domain\ColaFlow.Modules.Identity.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,100 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ColaFlow.Modules.Identity.Infrastructure.Persistence;
using ColaFlow.Modules.IssueManagement.Infrastructure.Persistence;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
namespace ColaFlow.Modules.ProjectManagement.IntegrationTests.Infrastructure;
/// <summary>
/// Custom WebApplicationFactory for ProjectManagement Integration Tests
/// Supports In-Memory database for fast, isolated tests
/// </summary>
public class PMWebApplicationFactory : WebApplicationFactory<Program>
{
private readonly string _testDatabaseName = $"PMTestDb_{Guid.NewGuid()}";
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Set environment to Testing
builder.UseEnvironment("Testing");
// Configure test-specific settings
builder.ConfigureAppConfiguration((context, config) =>
{
// Clear existing connection strings to prevent PostgreSQL registration
config.Sources.Clear();
// Add minimal config for testing
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:DefaultConnection"] = "",
["ConnectionStrings:PMDatabase"] = "",
["ConnectionStrings:IMDatabase"] = "",
["Jwt:SecretKey"] = "test-secret-key-for-integration-tests-minimum-32-characters",
["Jwt:Issuer"] = "ColaFlow.Test",
["Jwt:Audience"] = "ColaFlow.Test",
["Jwt:AccessTokenExpirationMinutes"] = "15",
["Jwt:RefreshTokenExpirationDays"] = "7"
});
});
builder.ConfigureServices(services =>
{
// Register test databases with In-Memory provider
// Use the same database name for cross-context data consistency
services.AddDbContext<IdentityDbContext>(options =>
{
options.UseInMemoryDatabase(_testDatabaseName);
options.EnableSensitiveDataLogging();
});
services.AddDbContext<PMDbContext>(options =>
{
options.UseInMemoryDatabase(_testDatabaseName);
options.EnableSensitiveDataLogging();
});
services.AddDbContext<IssueManagementDbContext>(options =>
{
options.UseInMemoryDatabase(_testDatabaseName);
options.EnableSensitiveDataLogging();
});
});
}
protected override IHost CreateHost(IHostBuilder builder)
{
var host = base.CreateHost(builder);
// Initialize databases after host is created
using var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
try
{
// Initialize Identity database
var identityDb = services.GetRequiredService<IdentityDbContext>();
identityDb.Database.EnsureCreated();
// Initialize ProjectManagement database
var pmDb = services.GetRequiredService<PMDbContext>();
pmDb.Database.EnsureCreated();
// Initialize IssueManagement database
var imDb = services.GetRequiredService<IssueManagementDbContext>();
imDb.Database.EnsureCreated();
}
catch (Exception ex)
{
Console.WriteLine($"Error initializing test database: {ex.Message}");
throw;
}
return host;
}
}

View File

@@ -0,0 +1,57 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace ColaFlow.Modules.ProjectManagement.IntegrationTests.Infrastructure;
/// <summary>
/// Helper class for generating JWT tokens in tests
/// </summary>
public static class TestAuthHelper
{
private const string SecretKey = "test-secret-key-for-integration-tests-minimum-32-characters";
private const string Issuer = "ColaFlow.Test";
private const string Audience = "ColaFlow.Test";
public static string GenerateJwtToken(Guid userId, Guid tenantId, string tenantSlug, string email, string role = "Member")
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(SecretKey);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim("sub", userId.ToString()),
new Claim("tenant_id", tenantId.ToString()),
new Claim("tenantId", tenantId.ToString()),
new Claim("tenantSlug", tenantSlug),
new Claim("email", email),
new Claim("role", role)
}),
Expires = DateTime.UtcNow.AddHours(1),
Issuer = Issuer,
Audience = Audience,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public static void AddAuthHeader(this HttpClient client, string token)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
public static void AddAuthHeader(this HttpClient client, Guid userId, Guid tenantId, string tenantSlug, string email, string role = "Member")
{
var token = GenerateJwtToken(userId, tenantId, tenantSlug, email, role);
client.AddAuthHeader(token);
}
}