using System.Net; using System.Net.Http.Json; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using Xunit; using FluentAssertions; using IamService.API.Application.Common; using IamService.API.Controllers; namespace IamService.FunctionalTests.Controllers; /// /// EN: Functional tests for OrganizationsController endpoints. /// VI: Functional tests cho các endpoints của OrganizationsController. /// public class OrganizationsControllerTests : IClassFixture { private readonly HttpClient _client; private readonly CustomWebApplicationFactory _factory; public OrganizationsControllerTests(CustomWebApplicationFactory factory) { _factory = factory; _client = factory.CreateClient(); // EN: Add authorization header with test token // VI: Thêm authorization header với test token _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", GenerateTestToken()); } #region Create Organization Tests [Fact] public async Task CreateOrganization_ValidRequest_Returns201WithLocation() { // Arrange var request = new CreateOrganizationRequest { Name = $"Test Organization {Guid.NewGuid():N}", Slug = $"test-org-{Guid.NewGuid():N}".Substring(0, 30), Description = "A test organization" }; // Act var response = await _client.PostAsJsonAsync("/api/v1/organizations", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); response.Headers.Location.Should().NotBeNull(); var result = await response.Content.ReadFromJsonAsync>(); result.Should().NotBeNull(); result!.Success.Should().BeTrue(); result.Data.Should().NotBeNull(); result.Data!.Name.Should().Be(request.Name); result.Data.Slug.Should().Be(request.Slug); result.Data.Status.Should().Be("Active"); } [Fact] public async Task CreateOrganization_DuplicateSlug_Returns409Conflict() { // Arrange var uniqueSlug = $"duplicate-{Guid.NewGuid():N}".Substring(0, 30); var request = new CreateOrganizationRequest { Name = "Organization 1", Slug = uniqueSlug }; // First creation - should succeed var firstResponse = await _client.PostAsJsonAsync("/api/v1/organizations", request); firstResponse.StatusCode.Should().Be(HttpStatusCode.Created); // Second creation with same slug - should fail request.Name = "Organization 2"; // Different name, same slug // Act var response = await _client.PostAsJsonAsync("/api/v1/organizations", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.Conflict); var result = await response.Content.ReadFromJsonAsync>(); result.Should().NotBeNull(); result!.Success.Should().BeFalse(); result.Error.Should().NotBeNull(); result.Error!.Code.Should().Be("SLUG_EXISTS"); } [Fact] public async Task CreateOrganization_WithParentOrganization_Returns201WithParentId() { // Arrange - Create parent first var parentSlug = $"parent-{Guid.NewGuid():N}".Substring(0, 30); var parentRequest = new CreateOrganizationRequest { Name = "Parent Organization", Slug = parentSlug }; var parentResponse = await _client.PostAsJsonAsync("/api/v1/organizations", parentRequest); var parentResult = await parentResponse.Content.ReadFromJsonAsync>(); var parentId = parentResult!.Data!.Id; // Create child var childSlug = $"child-{Guid.NewGuid():N}".Substring(0, 30); var childRequest = new CreateOrganizationRequest { Name = "Child Organization", Slug = childSlug, ParentOrganizationId = parentId }; // Act var response = await _client.PostAsJsonAsync("/api/v1/organizations", childRequest); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); var result = await response.Content.ReadFromJsonAsync>(); result!.Data!.ParentOrganizationId.Should().Be(parentId); } #endregion #region Get Organization Tests [Fact] public async Task GetOrganizationById_ExistingId_Returns200WithData() { // Arrange - Create organization first var slug = $"get-test-{Guid.NewGuid():N}".Substring(0, 30); var createRequest = new CreateOrganizationRequest { Name = "Get Test Organization", Slug = slug }; var createResponse = await _client.PostAsJsonAsync("/api/v1/organizations", createRequest); var createResult = await createResponse.Content.ReadFromJsonAsync>(); var orgId = createResult!.Data!.Id; // Act var response = await _client.GetAsync($"/api/v1/organizations/{orgId}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync>(); result.Should().NotBeNull(); result!.Success.Should().BeTrue(); result.Data!.Id.Should().Be(orgId); result.Data.Name.Should().Be("Get Test Organization"); } [Fact] public async Task GetOrganizationById_NonExistingId_Returns404() { // Arrange var nonExistentId = Guid.NewGuid(); // Act var response = await _client.GetAsync($"/api/v1/organizations/{nonExistentId}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); var result = await response.Content.ReadFromJsonAsync>(); result!.Success.Should().BeFalse(); result.Error!.Code.Should().Be("ORG_NOT_FOUND"); } [Fact] public async Task GetOrganizationBySlug_ExistingSlug_Returns200() { // Arrange - Create organization first var slug = $"slug-test-{Guid.NewGuid():N}".Substring(0, 30); var createRequest = new CreateOrganizationRequest { Name = "Slug Test Organization", Slug = slug }; await _client.PostAsJsonAsync("/api/v1/organizations", createRequest); // Act var response = await _client.GetAsync($"/api/v1/organizations/slug/{slug}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync>(); result!.Data!.Slug.Should().Be(slug); } [Fact] public async Task GetOrganizationBySlug_NonExistingSlug_Returns404() { // Act var response = await _client.GetAsync("/api/v1/organizations/slug/non-existent-slug-12345"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } #endregion #region Update Organization Tests [Fact] public async Task UpdateOrganization_ValidRequest_Returns200() { // Arrange - Create organization first var slug = $"update-test-{Guid.NewGuid():N}".Substring(0, 30); var createRequest = new CreateOrganizationRequest { Name = "Original Name", Slug = slug, Description = "Original Description" }; var createResponse = await _client.PostAsJsonAsync("/api/v1/organizations", createRequest); var createResult = await createResponse.Content.ReadFromJsonAsync>(); var orgId = createResult!.Data!.Id; var updateRequest = new UpdateOrganizationRequest { Name = "Updated Name", Description = "Updated Description" }; // Act var response = await _client.PutAsJsonAsync($"/api/v1/organizations/{orgId}", updateRequest); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync>(); result!.Data!.Name.Should().Be("Updated Name"); result.Data.Description.Should().Be("Updated Description"); } [Fact] public async Task UpdateOrganization_NonExistingId_Returns404() { // Arrange var updateRequest = new UpdateOrganizationRequest { Name = "Updated Name" }; // Act var response = await _client.PutAsJsonAsync($"/api/v1/organizations/{Guid.NewGuid()}", updateRequest); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } #endregion #region Archive Organization Tests [Fact] public async Task ArchiveOrganization_ExistingOrg_Returns200() { // Arrange - Create organization first var slug = $"archive-test-{Guid.NewGuid():N}".Substring(0, 30); var createRequest = new CreateOrganizationRequest { Name = "Archive Test Organization", Slug = slug }; var createResponse = await _client.PostAsJsonAsync("/api/v1/organizations", createRequest); var createResult = await createResponse.Content.ReadFromJsonAsync>(); var orgId = createResult!.Data!.Id; // Act var response = await _client.DeleteAsync($"/api/v1/organizations/{orgId}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] public async Task ArchiveOrganization_NonExistingId_Returns404() { // Act var response = await _client.DeleteAsync($"/api/v1/organizations/{Guid.NewGuid()}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } #endregion #region Hierarchy Tests [Fact] public async Task GetOrganizationHierarchy_ExistingOrg_Returns200() { // Arrange - Create organization first var slug = $"hierarchy-{Guid.NewGuid():N}".Substring(0, 30); var createRequest = new CreateOrganizationRequest { Name = "Hierarchy Organization", Slug = slug }; var createResponse = await _client.PostAsJsonAsync("/api/v1/organizations", createRequest); var createResult = await createResponse.Content.ReadFromJsonAsync>(); var orgId = createResult!.Data!.Id; // Act var response = await _client.GetAsync($"/api/v1/organizations/{orgId}/hierarchy"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] public async Task GetChildOrganizations_ExistingOrg_Returns200() { // Arrange - Create parent organization first var slug = $"parent-child-{Guid.NewGuid():N}".Substring(0, 30); var createRequest = new CreateOrganizationRequest { Name = "Parent Organization", Slug = slug }; var createResponse = await _client.PostAsJsonAsync("/api/v1/organizations", createRequest); var createResult = await createResponse.Content.ReadFromJsonAsync>(); var orgId = createResult!.Data!.Id; // Act var response = await _client.GetAsync($"/api/v1/organizations/{orgId}/children"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); } #endregion #region Helper Methods /// /// EN: Generate a test JWT token for authentication. /// VI: Tạo JWT token test để xác thực. /// private static string GenerateTestToken() { var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Email, "test@example.com"), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim("name", "Test User") }; var key = new SymmetricSecurityKey("test-secret-key-for-testing-purposes-only-32-chars!"u8.ToArray()); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: "http://localhost", audience: "api", claims: claims, expires: DateTime.UtcNow.AddHours(1), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } #endregion }