using Data.SeaHavenIndustries; using FluentAssertions; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using System.Security.Claims; using Xunit; namespace Api.SeaHavenIndustries.Tests; // Admin passes every permission check regardless of stored configuration. // Uplift approval authority is otherwise driven by Approvals:Tier1Roles/Tier2Roles, // which may be empty in a deployed environment; Admin must still be able to decide. public sealed class UpliftAdminApprovalTests { private static ApplicationDbContext NewContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } private static ClaimsPrincipal UserWithRoles(params string[] roles) { var claims = new List { new(ClaimTypes.NameIdentifier, "user-42") }; claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r))); return new ClaimsPrincipal(new ClaimsIdentity(claims, "Test")); } private static UpliftService NewService(ApplicationDbContext context, ApprovalsOptions? options = null) => new(new UpliftDataService(context), new DispatchDataService(context), new NoopDocumentStorage(), TimeProvider.System, Microsoft.Extensions.Options.Options.Create(options ?? new ApprovalsOptions())); private static async Task SeedPendingAsync(ApplicationDbContext context, int requiredTier) { var vendor = new Vendor { CompanyName = "Gateway", IsActive = true }; var workOrder = new WorkOrder { WorkerOrderTitle = "Repair" }; context.AddRange(vendor, workOrder); await context.SaveChangesAsync(); var dispatch = new Dispatch { VendorId = vendor.Id, WorkOrderId = workOrder.Id, Status = "Completed", NTEAmount = 1000m, DispatchNumber = "DIS-1" }; context.Dispatches.Add(dispatch); await context.SaveChangesAsync(); context.DispatchUpliftRequests.Add(new DispatchUpliftRequest { DispatchId = dispatch.Id, CurrentNTE = 1000m, RequestedNTE = 1800m, VendorReason = "reason", Status = UpliftStatus.Pending, RequiredTier = requiredTier, CreatedDate = DateTime.UtcNow }); await context.SaveChangesAsync(); return dispatch; } [Theory] [InlineData(1)] [InlineData(2)] public void CanApprove_Admin_WithEmptyTierConfig_IsTrue(int tier) { using var context = NewContext(); var service = NewService(context); service.CanApprove(UserWithRoles("Admin"), tier).Should().BeTrue(); } [Theory] [InlineData(1)] [InlineData(2)] public async Task List_Admin_WithEmptyTierConfig_CanDecidePendingRows(int tier) { using var context = NewContext(); var dispatch = await SeedPendingAsync(context, tier); var service = NewService(context); var queue = await service.ListAsync(UserWithRoles("Admin"), UpliftStatus.Pending, null, 1, 25, CancellationToken.None); var forDispatch = await service.ListForDispatchAsync(UserWithRoles("Admin"), dispatch.Id, CancellationToken.None); queue.Items.Should().ContainSingle().Which.CanDecide.Should().BeTrue(); forDispatch.Should().ContainSingle().Which.CanDecide.Should().BeTrue(); } [Theory] [InlineData(1)] [InlineData(2)] public async Task Approve_Admin_WithEmptyTierConfig_Succeeds(int tier) { using var context = NewContext(); await SeedPendingAsync(context, tier); var service = NewService(context); var result = await service.ApproveAsync(UserWithRoles("Admin"), 1, "ok", CancellationToken.None); result.Status.Should().Be(UpliftStatus.Approved); context.Dispatches.Single().NTEAmount.Should().Be(1800m); } [Fact] public async Task Reject_Admin_WithEmptyTierConfig_Succeeds() { using var context = NewContext(); await SeedPendingAsync(context, 2); var service = NewService(context); var result = await service.RejectAsync(UserWithRoles("Admin"), 1, "no", CancellationToken.None); result.Status.Should().Be(UpliftStatus.Rejected); context.Dispatches.Single().NTEAmount.Should().Be(1000m); } [Fact] public async Task RequestChanges_Admin_WithEmptyTierConfig_Succeeds() { using var context = NewContext(); await SeedPendingAsync(context, 2); var service = NewService(context); var result = await service.RequestChangesAsync(UserWithRoles("Admin"), 1, "more detail", CancellationToken.None); result.Status.Should().Be(UpliftStatus.ChangesRequested); } [Fact] public async Task EvidenceDownload_Admin_WithEmptyTierConfig_ReturnsFile() { using var context = NewContext(); var dispatch = await SeedPendingAsync(context, 2); context.VendorCompletionDocuments.Add(new VendorCompletionDocument { VendorId = dispatch.VendorId, DispatchId = dispatch.Id, WorkOrderId = dispatch.WorkOrderId!.Value, OriginalFileName = "invoice.pdf", StoredFileName = "evidence.bin", ContentType = "application/pdf", SizeBytes = 4, ScanStatus = "Passed", ReviewStatus = "Approved", Purpose = "UpliftEvidence", Version = 1 }); context.DispatchUpliftRequests.Single().EvidenceDocumentId = 1; await context.SaveChangesAsync(); var service = NewService(context); var result = await service.GetEvidenceForDownloadAsync(UserWithRoles("Admin"), 1, CancellationToken.None); result.Outcome.Should().Be(VendorDocumentDownloadOutcome.Ok); result.FileName.Should().Be("invoice.pdf"); } [Theory] [InlineData("Dispatcher")] [InlineData("Scheduler")] public async Task NonAdminRole_WithEmptyTierConfig_IsStillDenied(string role) { using var context = NewContext(); await SeedPendingAsync(context, 1); var service = NewService(context); var user = UserWithRoles(role); service.CanApprove(user, 1).Should().BeFalse(); service.CanApprove(user, 2).Should().BeFalse(); var queue = await service.ListAsync(user, UpliftStatus.Pending, null, 1, 25, CancellationToken.None); queue.Items.Should().ContainSingle().Which.CanDecide.Should().BeFalse(); var act = () => service.ApproveAsync(user, 1, "ok", CancellationToken.None); await act.Should().ThrowAsync(); context.Dispatches.Single().NTEAmount.Should().Be(1000m); context.DispatchUpliftRequests.Single().Status.Should().Be(UpliftStatus.Pending); } [Fact] public void Tier1OnlyRole_ApprovesTier1_ButNotTier2() { using var context = NewContext(); var service = NewService(context, new ApprovalsOptions { Tier1Roles = new[] { "Approver" } }); var user = UserWithRoles("Approver"); service.CanApprove(user, 1).Should().BeTrue(); service.CanApprove(user, 2).Should().BeFalse(); } private sealed class NoopDocumentStorage : IVendorDocumentStoragePort { public Task SaveAsync(int vendorId, int dispatchId, string storedFileName, Stream content, CancellationToken cancellationToken) => Task.CompletedTask; public Stream OpenRead(int vendorId, int dispatchId, string storedFileName) => new MemoryStream(); public void Delete(int vendorId, int dispatchId, string storedFileName) { } } }