shoc-backend/Api.SeaHavenIndustries.Tests/WorkOrderUpliftServiceConflictTests.cs
2026-09-23 01:26:15 -03:00

113 lines
5.1 KiB
C#

using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Moq;
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()
{
var upliftData = new Mock<IUpliftDataService>();
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(new Dispatch { Id = 10, Status = "Scheduled", NTEAmount = 1000m });
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>());
var service = new WorkOrderUpliftService(
upliftData.Object,
dispatchData.Object,
detailData.Object,
accountResolver.Object,
userData.Object,
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));
}
}