using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using ColaFlow.Modules.ProjectManagement.IntegrationTests.Infrastructure;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
namespace ColaFlow.Modules.ProjectManagement.IntegrationTests;
///
/// CRITICAL: Multi-tenant isolation tests
/// These tests verify that tenant data is properly isolated
///
public class MultiTenantIsolationTests : IClassFixture
{
private readonly PMWebApplicationFactory _factory;
private readonly HttpClient _client;
// Test tenant and user IDs
private readonly Guid _tenant1Id = Guid.NewGuid();
private readonly Guid _tenant2Id = Guid.NewGuid();
private readonly Guid _user1Id = Guid.NewGuid();
private readonly Guid _user2Id = Guid.NewGuid();
public MultiTenantIsolationTests(PMWebApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
[Fact]
public async Task Project_Should_Be_Isolated_By_TenantId()
{
// Arrange: Tenant 1 creates a project
var tenant1Token = TestAuthHelper.GenerateJwtToken(_user1Id, _tenant1Id, "tenant1", "user1@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token);
var createRequest = new
{
Name = "Tenant 1 Project",
Key = "T1P",
Description = "This is tenant 1's project"
};
var createResponse = await _client.PostAsJsonAsync("/api/v1/projects", createRequest);
createResponse.EnsureSuccessStatusCode();
var project = await createResponse.Content.ReadFromJsonAsync();
project.Should().NotBeNull();
project!.Name.Should().Be("Tenant 1 Project");
// Act: Tenant 2 tries to access Tenant 1's project
var tenant2Token = TestAuthHelper.GenerateJwtToken(_user2Id, _tenant2Id, "tenant2", "user2@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant2Token);
var getResponse = await _client.GetAsync($"/api/v1/projects/{project.Id}");
// Assert: Should return 404 (resource not found due to tenant filter)
getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Tenant_Cannot_List_Other_Tenants_Projects()
{
// Arrange: Tenant 1 creates a project
var tenant1Token = TestAuthHelper.GenerateJwtToken(_user1Id, _tenant1Id, "tenant1", "user1@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token);
await _client.PostAsJsonAsync("/api/v1/projects", new
{
Name = "Tenant 1 Project A",
Key = "T1PA",
Description = "First project"
});
await _client.PostAsJsonAsync("/api/v1/projects", new
{
Name = "Tenant 1 Project B",
Key = "T1PB",
Description = "Second project"
});
// Arrange: Tenant 2 creates a project
var tenant2Token = TestAuthHelper.GenerateJwtToken(_user2Id, _tenant2Id, "tenant2", "user2@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant2Token);
await _client.PostAsJsonAsync("/api/v1/projects", new
{
Name = "Tenant 2 Project",
Key = "T2P",
Description = "Tenant 2's project"
});
// Act: Tenant 2 lists projects
var listResponse = await _client.GetAsync("/api/v1/projects");
listResponse.EnsureSuccessStatusCode();
var projects = await listResponse.Content.ReadFromJsonAsync>();
// Assert: Should only see Tenant 2's project
projects.Should().NotBeNull();
projects!.Count.Should().Be(1);
projects[0].Name.Should().Be("Tenant 2 Project");
}
[Fact]
public async Task Epic_Should_Be_Isolated_By_TenantId()
{
// Arrange: Tenant 1 creates a project with an epic
var tenant1Token = TestAuthHelper.GenerateJwtToken(_user1Id, _tenant1Id, "tenant1", "user1@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token);
// Create project
var projectResponse = await _client.PostAsJsonAsync("/api/v1/projects", new
{
Name = "Tenant 1 Project",
Key = "T1P",
Description = "Test project"
});
var project = await projectResponse.Content.ReadFromJsonAsync();
// Create epic
var epicResponse = await _client.PostAsJsonAsync("/api/v1/epics", new
{
ProjectId = project!.Id,
Name = "Tenant 1 Epic",
Description = "Test epic",
CreatedBy = _user1Id
});
epicResponse.EnsureSuccessStatusCode();
var epic = await epicResponse.Content.ReadFromJsonAsync();
// Act: Tenant 2 tries to access Tenant 1's epic
var tenant2Token = TestAuthHelper.GenerateJwtToken(_user2Id, _tenant2Id, "tenant2", "user2@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant2Token);
var getResponse = await _client.GetAsync($"/api/v1/epics/{epic!.Id}");
// Assert: Should return 404
getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Story_Should_Be_Isolated_By_TenantId()
{
// Arrange: Tenant 1 creates project → epic → story
var tenant1Token = TestAuthHelper.GenerateJwtToken(_user1Id, _tenant1Id, "tenant1", "user1@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token);
// Create project
var projectResponse = await _client.PostAsJsonAsync("/api/v1/projects", new
{
Name = "Tenant 1 Project",
Key = "T1P",
Description = "Test project"
});
var project = await projectResponse.Content.ReadFromJsonAsync();
// Create epic
var epicResponse = await _client.PostAsJsonAsync("/api/v1/epics", new
{
ProjectId = project!.Id,
Name = "Tenant 1 Epic",
Description = "Test epic",
CreatedBy = _user1Id
});
var epic = await epicResponse.Content.ReadFromJsonAsync();
// Create story
var storyResponse = await _client.PostAsJsonAsync("/api/v1/stories", new
{
EpicId = epic!.Id,
Title = "Tenant 1 Story",
Description = "Test story",
Priority = "Medium",
CreatedBy = _user1Id
});
storyResponse.EnsureSuccessStatusCode();
var story = await storyResponse.Content.ReadFromJsonAsync();
// Act: Tenant 2 tries to access Tenant 1's story
var tenant2Token = TestAuthHelper.GenerateJwtToken(_user2Id, _tenant2Id, "tenant2", "user2@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant2Token);
var getResponse = await _client.GetAsync($"/api/v1/stories/{story!.Id}");
// Assert: Should return 404
getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Task_Should_Be_Isolated_By_TenantId()
{
// Arrange: Tenant 1 creates project → epic → story → task
var tenant1Token = TestAuthHelper.GenerateJwtToken(_user1Id, _tenant1Id, "tenant1", "user1@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token);
// Create project
var projectResponse = await _client.PostAsJsonAsync("/api/v1/projects", new
{
Name = "Tenant 1 Project",
Key = "T1P",
Description = "Test project"
});
var project = await projectResponse.Content.ReadFromJsonAsync();
// Create epic
var epicResponse = await _client.PostAsJsonAsync("/api/v1/epics", new
{
ProjectId = project!.Id,
Name = "Tenant 1 Epic",
Description = "Test epic",
CreatedBy = _user1Id
});
var epic = await epicResponse.Content.ReadFromJsonAsync();
// Create story
var storyResponse = await _client.PostAsJsonAsync("/api/v1/stories", new
{
EpicId = epic!.Id,
Title = "Tenant 1 Story",
Description = "Test story",
Priority = "Medium",
CreatedBy = _user1Id
});
var story = await storyResponse.Content.ReadFromJsonAsync();
// Create task
var taskResponse = await _client.PostAsJsonAsync("/api/v1/tasks", new
{
StoryId = story!.Id,
Title = "Tenant 1 Task",
Description = "Test task",
Priority = "Medium",
CreatedBy = _user1Id
});
taskResponse.EnsureSuccessStatusCode();
var task = await taskResponse.Content.ReadFromJsonAsync();
// Act: Tenant 2 tries to access Tenant 1's task
var tenant2Token = TestAuthHelper.GenerateJwtToken(_user2Id, _tenant2Id, "tenant2", "user2@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant2Token);
var getResponse = await _client.GetAsync($"/api/v1/tasks/{task!.Id}");
// Assert: Should return 404
getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Tenant_Cannot_Update_Other_Tenants_Project()
{
// Arrange: Tenant 1 creates a project
var tenant1Token = TestAuthHelper.GenerateJwtToken(_user1Id, _tenant1Id, "tenant1", "user1@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token);
var createResponse = await _client.PostAsJsonAsync("/api/v1/projects", new
{
Name = "Tenant 1 Project",
Key = "T1P",
Description = "Original description"
});
var project = await createResponse.Content.ReadFromJsonAsync();
// Act: Tenant 2 tries to update Tenant 1's project
var tenant2Token = TestAuthHelper.GenerateJwtToken(_user2Id, _tenant2Id, "tenant2", "user2@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant2Token);
var updateResponse = await _client.PutAsJsonAsync($"/api/v1/projects/{project!.Id}", new
{
Name = "Hacked Project Name",
Description = "Malicious update"
});
// Assert: Should return 404 (not 403, because tenant filter makes it invisible)
updateResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Tenant_Cannot_Delete_Other_Tenants_Project()
{
// Arrange: Tenant 1 creates a project
var tenant1Token = TestAuthHelper.GenerateJwtToken(_user1Id, _tenant1Id, "tenant1", "user1@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token);
var createResponse = await _client.PostAsJsonAsync("/api/v1/projects", new
{
Name = "Tenant 1 Project",
Key = "T1P",
Description = "Important project"
});
var project = await createResponse.Content.ReadFromJsonAsync();
// Act: Tenant 2 tries to delete Tenant 1's project
var tenant2Token = TestAuthHelper.GenerateJwtToken(_user2Id, _tenant2Id, "tenant2", "user2@test.com");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant2Token);
var deleteResponse = await _client.DeleteAsync($"/api/v1/projects/{project!.Id}");
// Assert: Should return 404
deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
// Verify project still exists for Tenant 1
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token);
var verifyResponse = await _client.GetAsync($"/api/v1/projects/{project.Id}");
verifyResponse.StatusCode.Should().Be(HttpStatusCode.OK);
}
}