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