mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
206 lines
7.8 KiB
C#
206 lines
7.8 KiB
C#
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<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
return new ApplicationDbContext(options);
|
|
}
|
|
|
|
private static ClaimsPrincipal UserWithRoles(params string[] roles)
|
|
{
|
|
var claims = new List<Claim> { 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<Dispatch> 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<UpliftForbiddenException>();
|
|
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) { }
|
|
}
|
|
}
|