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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user