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); } }