using System.Net; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Api.Contracts; using Tiku.Application.PlatformAdmin; using Tiku.Application.Security; using Tiku.Domain.Common; using Tiku.Domain.Identity; using Tiku.Domain.Operations; using Tiku.Domain.Platform; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.IntegrationTests.Api; public sealed class PlatformApprovalEndpointTests { private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions(); [Fact] public async Task Tenant_archive_requires_another_authorized_operator_and_executes_once() { await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary { ["Tenancy:Resolution:PlatformHosts:0"] = "localhost" }); var permissions = new[] { BackendPermissions.PlatformDashboardView, BackendPermissions.PlatformTenantManage, BackendPermissions.PlatformApprovalView, BackendPermissions.PlatformApprovalDecide }; var (requester, approver) = await SeedActorsAsync(factory, permissions); var tenant = new Tenant { Slug = $"approval-{Guid.NewGuid():N}"[..30], Name = "Approval Tenant", Status = TenantStatus.Active, BillingStatus = BillingStatus.Active }; await factory.SeedAsync( tenant, new PlatformApprovalPolicy { Code = PlatformApprovalPolicyCodes.TenantArchive, Name = "Tenant archive", RequiredPermission = BackendPermissions.PlatformTenantManage, AlwaysRequireApproval = true }); using var requesterClient = factory.CreateClient(); requesterClient.UseAccessToken(await requesterClient.LoginAsPlatformAsync(requester.Email)); using var archive = new HttpRequestMessage(HttpMethod.Patch, "/api/platform/tenants/status") { Content = JsonContent.Create(new UpdatePlatformTenantStatusDto { TenantId = tenant.Id, Status = TenantStatus.Archived, Reason = "Close expired customer" }) }; archive.Headers.Add("Idempotency-Key", "archive-approval-1"); var submission = await requesterClient.SendAsync(archive); Assert.Equal(HttpStatusCode.Accepted, submission.StatusCode); var body = await submission.Content.ReadFromJsonAsync(JsonOptions); Assert.NotNull(body?.ApprovalRequest); var selfApproval = await requesterClient.PostAsJsonAsync( $"/api/platform/approvals/{body.ApprovalRequest.Id}/approve", new PlatformApprovalDecisionDto("Self approval")); Assert.Equal(HttpStatusCode.Conflict, selfApproval.StatusCode); using var approverClient = factory.CreateClient(); approverClient.UseAccessToken(await approverClient.LoginAsPlatformAsync(approver.Email)); var approval = await approverClient.PostAsJsonAsync( $"/api/platform/approvals/{body.ApprovalRequest.Id}/approve", new PlatformApprovalDecisionDto("Independent verification complete")); Assert.Equal(HttpStatusCode.OK, approval.StatusCode); var approved = await approval.Content.ReadFromJsonAsync(JsonOptions); Assert.Equal(PlatformApprovalRequestStatus.Approved, approved?.Status); using (var executionScope = factory.CreateSystemScope("Execute approved platform command")) { var processor = executionScope.ServiceProvider.GetRequiredService(); Assert.Equal(1, await processor.ProcessApprovedAsync()); } var replay = await approverClient.PostAsJsonAsync($"/api/platform/approvals/{body.ApprovalRequest.Id}/approve", new PlatformApprovalDecisionDto("Replay")); Assert.Equal(HttpStatusCode.Conflict, replay.StatusCode); using var scope = factory.CreateSystemScope("Verify platform approval execution"); var db = scope.ServiceProvider.GetRequiredService(); var executed = await db.PlatformApprovalRequests.AsNoTracking() .SingleAsync(item => item.Id == body.ApprovalRequest.Id); Assert.True(executed.Status == PlatformApprovalRequestStatus.Succeeded, $"Approval execution failed: {executed.Error}"); Assert.Equal(TenantStatus.Archived, await db.Tenants.Where(item => item.Id == tenant.Id).Select(item => item.Status).SingleAsync()); Assert.True(await db.AuditLogs.AnyAsync(item => item.Action == "platform.approval.executed" && item.TargetId == body.ApprovalRequest.Id.ToString("N"))); } private static async Task<((Guid UserId, string Email) Requester, (Guid UserId, string Email) Approver)> SeedActorsAsync( ApiTestFactory factory, IReadOnlyCollection permissionCodes) { var requester = (UserId: Guid.NewGuid(), Email: $"approval-requester-{Guid.NewGuid():N}@example.test"); var approver = (UserId: Guid.NewGuid(), Email: $"approval-approver-{Guid.NewGuid():N}@example.test"); var requesterRole = Guid.NewGuid(); var approverRole = Guid.NewGuid(); var modules = permissionCodes.Select(PermissionModuleCatalog.ResolvePermissionModuleCode) .Distinct(StringComparer.Ordinal) .Select(code => new PermissionModule { Code = code, Name = code, Area = BackendPermissionArea.Platform, RequiredFeatureCode = PermissionModuleCatalog.RequiredFeatures[code] }); var permissions = permissionCodes.Select(code => new BackendPermission { Code = code, Name = code, Area = BackendPermissionArea.Platform, PermissionModuleCode = PermissionModuleCatalog.ResolvePermissionModuleCode(code), IsSystem = true }); await factory.SeedAsync([ .. modules, .. permissions, User(requester.UserId, requester.Email), User(approver.UserId, approver.Email), new PlatformBackendRole { Id = requesterRole, Code = $"approval_requester_{requesterRole:N}", Name = "Approval Requester", Status = BackendRoleStatus.Active }, new PlatformBackendRole { Id = approverRole, Code = $"approval_approver_{approverRole:N}", Name = "Approval Approver", Status = BackendRoleStatus.Active }, .. permissionCodes.Select(code => new PlatformBackendRolePermission { RoleId = requesterRole, PermissionCode = code }), .. permissionCodes.Select(code => new PlatformBackendRolePermission { RoleId = approverRole, PermissionCode = code }), new PlatformBackendUserRole { UserId = requester.UserId, RoleId = requesterRole }, new PlatformBackendUserRole { UserId = approver.UserId, RoleId = approverRole } ]); return (requester, approver); } private static User User(Guid id, string email) { return new User { Id = id, Email = email, NormalizedEmail = email.ToUpperInvariant(), UserName = email, NormalizedUserName = email.ToUpperInvariant(), Name = email, PrimaryRole = "platform_admin", RawProfile = JsonDefaults.Object() }.WithTestPassword(); } private static JsonSerializerOptions CreateJsonOptions() { var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); options.Converters.Add(new JsonStringEnumConverter()); return options; } }