mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33:13 +00:00
138 lines
6.3 KiB
C#
138 lines
6.3 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Moq;
|
|
using SeaHaven.DataServices.Dto;
|
|
using SeaHaven.DataServices.Exceptions;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.Configuration;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Exceptions;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Interfaces;
|
|
using System.Security.Claims;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
public sealed class WorkOrderUpliftServiceConflictTests
|
|
{
|
|
[Fact]
|
|
public async Task CreateAsync_DifferentWorkOrdersSharingDispatch_MapsConcurrentInsertConflict()
|
|
{
|
|
// Dispatch 10 is legitimately shared: work order 1 owns it and work order 2 links to
|
|
// it through DispatchWorkOrders, so the uplift dispatch resolves to it for both.
|
|
var sharedDispatch = new Dispatch
|
|
{
|
|
Id = 10,
|
|
WorkOrderId = 1,
|
|
Status = "Scheduled",
|
|
NTEAmount = 1000m,
|
|
DispatchWorkOrders = new List<DispatchWorkOrder>
|
|
{
|
|
new() { DispatchId = 10, WorkOrderId = 2 },
|
|
},
|
|
};
|
|
var upliftData = new Mock<IUpliftDataService>();
|
|
upliftData.Setup(x => x.GetUpliftDispatchForWorkOrderAsync(
|
|
It.IsIn(1, 2),
|
|
10,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(sharedDispatch);
|
|
var bothAtInsert = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var savesArrived = 0;
|
|
var winner = 0;
|
|
|
|
upliftData.Setup(x => x.ExecuteWorkOrderMutationAsync(
|
|
It.IsAny<int>(),
|
|
It.IsAny<Func<CancellationToken, Task<WorkOrderUpliftDto?>>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<int, Func<CancellationToken, Task<WorkOrderUpliftDto?>>, CancellationToken>(
|
|
(_, work, cancellationToken) => work(cancellationToken));
|
|
upliftData.Setup(x => x.HasPendingForWorkOrderAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(false);
|
|
upliftData.Setup(x => x.HasActiveAsync(10, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(false);
|
|
upliftData.Setup(x => x.SumAutoApprovedAmountForWorkOrderAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(0m);
|
|
upliftData.Setup(x => x.StageAsync(It.IsAny<DispatchUpliftRequest>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
upliftData.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
.Returns(async () =>
|
|
{
|
|
if (Interlocked.Increment(ref savesArrived) == 2)
|
|
bothAtInsert.TrySetResult();
|
|
|
|
await bothAtInsert.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
if (Interlocked.Exchange(ref winner, 1) != 0)
|
|
throw new UpliftDispatchConflictException();
|
|
});
|
|
|
|
var detailData = new Mock<IWorkOrderDetailDataService>();
|
|
detailData.Setup(x => x.ExistsAsync(It.IsAny<int>(), It.IsAny<CancellationToken>(), It.IsAny<int?>()))
|
|
.ReturnsAsync(true);
|
|
detailData.Setup(x => x.GetWorkOrderForMediaAsync(It.IsAny<int>(), It.IsAny<CancellationToken>(), It.IsAny<int?>()))
|
|
.ReturnsAsync((int workOrderId, CancellationToken _, int? _) => new WorkOrder
|
|
{
|
|
Id = workOrderId,
|
|
PrimaryDispatchId = 10,
|
|
WorkOrderType = WorkOrderType.PM,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
});
|
|
|
|
var dispatchData = new Mock<IDispatchDataService>();
|
|
dispatchData.Setup(x => x.GetByIdAsync(10))
|
|
.ReturnsAsync(sharedDispatch);
|
|
dispatchData.Setup(x => x.StageAuditLogAsync(It.IsAny<WorkOrderAuditLog>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var accountResolver = new Mock<IWorkOrderAccountResolver>();
|
|
accountResolver.Setup(x => x.ResolveAccountFilter(It.IsAny<ClaimsPrincipal>())).Returns((int?)null);
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(x => x.GetDisplayNamesByIdsAsync(It.IsAny<IEnumerable<string>>()))
|
|
.ReturnsAsync(new Dictionary<string, string>());
|
|
// SH-327: the requester must hold RequestUplifts before the create reaches the insert.
|
|
var permissionData = new Mock<ITeamPermissionOverrideDataService>();
|
|
permissionData.Setup(x => x.GetUserAsync("dispatcher-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new TeamPermissionUserData { UserId = "dispatcher-1", RoleName = "Dispatcher" });
|
|
|
|
var service = new WorkOrderUpliftService(
|
|
upliftData.Object,
|
|
dispatchData.Object,
|
|
detailData.Object,
|
|
accountResolver.Object,
|
|
userData.Object,
|
|
permissionData.Object,
|
|
new TeamPermissionPolicy(),
|
|
TimeProvider.System,
|
|
Microsoft.Extensions.Options.Options.Create(new ApprovalsOptions
|
|
{
|
|
UpliftTier1MaxUsd = 2500m,
|
|
Tier1Roles = new[] { "Approver" },
|
|
Tier2Roles = new[] { "Manager" },
|
|
}));
|
|
var user = new ClaimsPrincipal(new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.NameIdentifier, "dispatcher-1") },
|
|
"test"));
|
|
|
|
static async Task<(WorkOrderUpliftDto? Result, Exception? Error)> Capture(
|
|
Func<Task<WorkOrderUpliftDto?>> create)
|
|
{
|
|
try
|
|
{
|
|
return (await create(), null);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
return (null, exception);
|
|
}
|
|
}
|
|
|
|
var outcomes = await Task.WhenAll(
|
|
Capture(() => service.CreateAsync(1, new CreateWorkOrderUpliftRequestDto { Amount = 500m }, user, CancellationToken.None)),
|
|
Capture(() => service.CreateAsync(2, new CreateWorkOrderUpliftRequestDto { Amount = 500m }, user, CancellationToken.None)));
|
|
|
|
Assert.Single(outcomes, outcome => outcome.Result != null);
|
|
Assert.IsType<UpliftConflictException>(Assert.Single(outcomes, outcome => outcome.Error != null).Error);
|
|
upliftData.Verify(x => x.HasActiveAsync(10, It.IsAny<CancellationToken>()), Times.Exactly(2));
|
|
}
|
|
}
|