mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
Seed resolvable Customer accounts and mock account resolution so create-path CI tests match fail-closed account scope. Co-authored-by: Cursor <cursoragent@cursor.com>
291 lines
13 KiB
C#
291 lines
13 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Microsoft.Extensions.Options;
|
|
using Moq;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.Configuration;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Interfaces;
|
|
using SeaHaven.Services.Validation;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
internal static class TestAsyncEnumerableHelper
|
|
{
|
|
public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IEnumerable<T> source)
|
|
{
|
|
foreach (var item in source)
|
|
yield return item;
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public class SyncServiceBehaviorTests
|
|
{
|
|
[Fact]
|
|
public async Task SyncWorkOrders_CreatesNewAndUpdatesExisting()
|
|
{
|
|
var data = new Mock<ISyncDataService>();
|
|
var external = new Mock<ISyncExternalSource>();
|
|
|
|
var existingWO = new WorkOrder { Id = 5, ExternalWorkOrderId = "EXT-1", WorkerOrderTitle = "Old" };
|
|
|
|
data.Setup(d => d.GetLastInternalWONumberAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync("10000005");
|
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync("EXT-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(existingWO);
|
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync("EXT-2", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((WorkOrder?)null);
|
|
data.Setup(d => d.ResolveLocationAsync(It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((int?)null);
|
|
data.Setup(d => d.GetLocationCountAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(3);
|
|
|
|
var items = new List<Dictionary<string, string?>>
|
|
{
|
|
new() { ["work_order_id"] = "EXT-1", ["description"] = "Updated Title", ["wo_status"] = "in_progress" },
|
|
new() { ["work_order_id"] = "EXT-2", ["description"] = "New WO", ["wo_status"] = "new", ["severity"] = "3" }
|
|
};
|
|
|
|
external.Setup(e => e.ScanWorkOrdersAsync(It.IsAny<CancellationToken>()))
|
|
.Returns(items.ToAsyncEnumerable());
|
|
|
|
var accountResolver = new Mock<IWorkOrderAccountResolver>();
|
|
accountResolver
|
|
.Setup(r => r.TryResolveFromCustomerAsync(It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(1);
|
|
var service = new SyncService(data.Object, external.Object, accountResolver.Object);
|
|
var result = await service.SyncWorkOrdersAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(2, result.Synced);
|
|
Assert.Equal(1, result.Created);
|
|
Assert.Equal(1, result.Updated);
|
|
Assert.Equal(3, result.LocationsCreated);
|
|
Assert.Equal("Updated Title", existingWO.WorkerOrderTitle);
|
|
Assert.Equal("In Progress", existingWO.Status);
|
|
data.Verify(d => d.EnqueueWorkOrder(It.Is<WorkOrder>(w => w.ExternalWorkOrderId == "EXT-2" && w.AccountId == 1)), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SyncWorkOrders_MapsStatusAndSeverity()
|
|
{
|
|
var data = new Mock<ISyncDataService>();
|
|
var external = new Mock<ISyncExternalSource>();
|
|
|
|
data.Setup(d => d.GetLastInternalWONumberAsync(It.IsAny<CancellationToken>())).ReturnsAsync((string?)null);
|
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync((WorkOrder?)null);
|
|
data.Setup(d => d.ResolveLocationAsync(It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>())).ReturnsAsync((int?)null);
|
|
data.Setup(d => d.GetLocationCountAsync(It.IsAny<CancellationToken>())).ReturnsAsync(0);
|
|
|
|
var items = new List<Dictionary<string, string?>>
|
|
{
|
|
new() { ["work_order_id"] = "W1", ["wo_status"] = "completed", ["severity"] = "2", ["customer"] = "Acme" }
|
|
};
|
|
external.Setup(e => e.ScanWorkOrdersAsync(It.IsAny<CancellationToken>())).Returns(items.ToAsyncEnumerable());
|
|
|
|
var accountResolver = new Mock<IWorkOrderAccountResolver>();
|
|
accountResolver
|
|
.Setup(r => r.TryResolveFromCustomerAsync("Acme", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(42);
|
|
await new SyncService(data.Object, external.Object, accountResolver.Object).SyncWorkOrdersAsync(CancellationToken.None);
|
|
|
|
data.Verify(d => d.EnqueueWorkOrder(It.Is<WorkOrder>(w =>
|
|
w.Status == "Done" && w.Priority == "Sev 2" && w.Severity == "2" && w.AccountId == 42)), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SyncComments_SkipsExistingAndOrphans()
|
|
{
|
|
var data = new Mock<ISyncDataService>();
|
|
var external = new Mock<ISyncExternalSource>();
|
|
var wo = new WorkOrder { Id = 10, ExternalWorkOrderId = "WO-1" };
|
|
|
|
data.Setup(d => d.CommentExistsByExternalIdAsync("C-EXISTS", It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
|
data.Setup(d => d.CommentExistsByExternalIdAsync("C-NEW", It.IsAny<CancellationToken>())).ReturnsAsync(false);
|
|
data.Setup(d => d.CommentExistsByExternalIdAsync("C-ORPHAN", It.IsAny<CancellationToken>())).ReturnsAsync(false);
|
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync("WO-1", It.IsAny<CancellationToken>())).ReturnsAsync(wo);
|
|
data.Setup(d => d.GetWorkOrderByExternalIdAsync("WO-MISSING", It.IsAny<CancellationToken>())).ReturnsAsync((WorkOrder?)null);
|
|
|
|
var items = new List<Dictionary<string, string?>>
|
|
{
|
|
new() { ["comment_id"] = "C-EXISTS", ["work_order_id"] = "WO-1" },
|
|
new() { ["comment_id"] = "C-NEW", ["work_order_id"] = "WO-1", ["text"] = "Hello" },
|
|
new() { ["comment_id"] = "C-ORPHAN", ["work_order_id"] = "WO-MISSING" }
|
|
};
|
|
external.Setup(e => e.ScanCommentsAsync(It.IsAny<CancellationToken>())).Returns(items.ToAsyncEnumerable());
|
|
|
|
var result = await new SyncService(data.Object, external.Object, Mock.Of<IWorkOrderAccountResolver>()).SyncCommentsAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(2, result.Synced);
|
|
Assert.Equal(1, result.Created);
|
|
Assert.Equal(2, result.Skipped);
|
|
data.Verify(d => d.EnqueueComment(It.Is<Comments>(c => c.ExternalCommentId == "C-NEW")), Times.Once);
|
|
}
|
|
}
|
|
|
|
public class WorkOrderDispatchServiceBehaviorTests
|
|
{
|
|
[Fact]
|
|
public async Task VerifyDispatch_ThrowsWhenChecklistIncomplete()
|
|
{
|
|
var dispatchData = CreateVerifyDispatchMocks(
|
|
out var dispatch,
|
|
incompleteCount: 1,
|
|
checklistComplete: false,
|
|
hasCustomerSignoff: true,
|
|
hasVendorSignoff: true);
|
|
dispatch.Id = 1;
|
|
|
|
var service = CreateDispatchService(dispatchData);
|
|
var ex = await Assert.ThrowsAsync<DispatchVerificationException>(() => service.VerifyDispatchAsync(1, "user1"));
|
|
Assert.Contains("1 checklist item(s) incomplete", ex.Missing);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyDispatch_ThrowsWhenVendorSignoffMissing()
|
|
{
|
|
var dispatchData = CreateVerifyDispatchMocks(
|
|
out var dispatch,
|
|
incompleteCount: 0,
|
|
checklistComplete: true,
|
|
hasCustomerSignoff: true,
|
|
hasVendorSignoff: false);
|
|
dispatch.Id = 1;
|
|
|
|
var service = CreateDispatchService(dispatchData);
|
|
var ex = await Assert.ThrowsAsync<DispatchVerificationException>(() => service.VerifyDispatchAsync(1, "user1"));
|
|
Assert.Contains("Missing Vendor sign-off", ex.Missing);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyDispatch_SucceedsWhenAllComplete()
|
|
{
|
|
var dispatchData = CreateVerifyDispatchMocks(
|
|
out var dispatch,
|
|
incompleteCount: 0,
|
|
checklistComplete: true,
|
|
hasCustomerSignoff: true,
|
|
hasVendorSignoff: true);
|
|
dispatch.Id = 1;
|
|
|
|
var userMock = new Mock<IUserDataService>();
|
|
userMock.Setup(u => u.GetByIdAsync("user1")).ReturnsAsync(new ApplicationUser { FirstName = "Jane", LastName = "Doe" });
|
|
|
|
var service = CreateDispatchService(dispatchData, userMock: userMock);
|
|
var result = await service.VerifyDispatchAsync(1, "user1");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("Dispatch verified", result!.Message);
|
|
Assert.Equal("Verified", dispatch.Status);
|
|
Assert.Equal("Jane Doe", dispatch.VerifiedBy);
|
|
}
|
|
|
|
private static Mock<IDispatchDataService> CreateVerifyDispatchMocks(
|
|
out Dispatch dispatch,
|
|
int incompleteCount,
|
|
bool checklistComplete,
|
|
bool hasCustomerSignoff,
|
|
bool hasVendorSignoff)
|
|
{
|
|
dispatch = new Dispatch { Id = 1, DispatchNumber = "DSP-00001", WorkOrderId = 10 };
|
|
var mock = new Mock<IDispatchDataService>();
|
|
mock.Setup(d => d.GetByIdAsync(1)).ReturnsAsync(dispatch);
|
|
|
|
var checklist = checklistComplete
|
|
? new List<DispatchChecklistItem> { new() { IsCompleted = true } }
|
|
: new List<DispatchChecklistItem> { new() { IsCompleted = false } };
|
|
mock.Setup(d => d.GetChecklistItemsAsync(1)).ReturnsAsync(checklist);
|
|
|
|
var signoffs = new List<DispatchSignoff>();
|
|
if (hasCustomerSignoff) signoffs.Add(new DispatchSignoff { SignoffType = "Customer" });
|
|
if (hasVendorSignoff) signoffs.Add(new DispatchSignoff { SignoffType = "Vendor" });
|
|
mock.Setup(d => d.GetSignoffsAsync(1)).ReturnsAsync(signoffs);
|
|
|
|
return mock;
|
|
}
|
|
|
|
private static WorkOrderDispatchService CreateDispatchService(
|
|
Mock<IDispatchDataService>? dispatchDataMock = null,
|
|
Mock<IUserDataService>? userMock = null)
|
|
{
|
|
return new WorkOrderDispatchService(
|
|
Mock.Of<IWorkOrderDataService>(),
|
|
(dispatchDataMock ?? new Mock<IDispatchDataService>()).Object,
|
|
Mock.Of<IVendorDataService>(),
|
|
(userMock ?? new Mock<IUserDataService>()).Object,
|
|
Mock.Of<ITaskListTemplateDataService>(),
|
|
Mock.Of<ICommentDataService>(),
|
|
Mock.Of<IDispatchEmailPort>(),
|
|
Mock.Of<IVendorTokenPort>(),
|
|
Microsoft.Extensions.Options.Options.Create(new FrontendOptions()));
|
|
}
|
|
}
|
|
|
|
public class WorkOrderServiceBehaviorTests
|
|
{
|
|
[Fact]
|
|
public async Task ChangeStatus_AddsAuditWhenStatusDiffers()
|
|
{
|
|
var wo = new WorkOrder { Id = 5, Status = "Open" };
|
|
var woData = new Mock<IWorkOrderDataService>();
|
|
woData.Setup(d => d.GetByIdAsync(5)).ReturnsAsync(wo);
|
|
|
|
var service = CreateWorkOrderService(woData);
|
|
var result = await service.ChangeStatusAsync(5, "In Progress", "user1");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("In Progress", wo.Status);
|
|
woData.Verify(d => d.AddAuditLogAsync(It.Is<WorkOrderAuditLog>(a =>
|
|
a.FieldName == "Status" && a.OldValue == "Open" && a.NewValue == "In Progress")), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ChangeStatus_DoesNotAuditWhenStatusSame()
|
|
{
|
|
var wo = new WorkOrder { Id = 5, Status = "Open" };
|
|
var woData = new Mock<IWorkOrderDataService>();
|
|
woData.Setup(d => d.GetByIdAsync(5)).ReturnsAsync(wo);
|
|
|
|
await CreateWorkOrderService(woData).ChangeStatusAsync(5, "Open", "user1");
|
|
|
|
woData.Verify(d => d.AddAuditLogAsync(It.IsAny<WorkOrderAuditLog>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteWorkOrderCascade_UnlinksQuotesAndDeletesComments()
|
|
{
|
|
var woData = new Mock<IWorkOrderDataService>();
|
|
woData.Setup(d => d.ExistsAsync(5)).ReturnsAsync(true);
|
|
|
|
var commentData = new Mock<ICommentDataService>();
|
|
commentData.Setup(c => c.GetByWorkOrderIdAsync(5))
|
|
.ReturnsAsync(new List<Comments> { new() { Id = 1 }, new() { Id = 2 } });
|
|
|
|
var quotesData = new Mock<IQuotesDataService>();
|
|
|
|
var result = await CreateWorkOrderService(woData, commentData, quotesData).DeleteWorkOrderCascadeAsync(5, "user1");
|
|
|
|
Assert.True(result);
|
|
quotesData.Verify(q => q.UnlinkFromWorkOrderAsync(5), Times.Once);
|
|
commentData.Verify(c => c.DeleteAsync(1), Times.Once);
|
|
commentData.Verify(c => c.DeleteAsync(2), Times.Once);
|
|
}
|
|
|
|
private static WorkOrderService CreateWorkOrderService(
|
|
Mock<IWorkOrderDataService>? woData = null,
|
|
Mock<ICommentDataService>? commentData = null,
|
|
Mock<IQuotesDataService>? quotesData = null)
|
|
{
|
|
return new WorkOrderService(
|
|
(woData ?? new Mock<IWorkOrderDataService>()).Object,
|
|
(commentData ?? new Mock<ICommentDataService>()).Object,
|
|
Mock.Of<IUserDataService>(),
|
|
(quotesData ?? new Mock<IQuotesDataService>()).Object,
|
|
Mock.Of<IFileStoragePort>(),
|
|
Mock.Of<ICreateWorkOrderValidation>(),
|
|
Mock.Of<IUpdateWorkOrderValidation>(),
|
|
Mock.Of<IWorkOrderAccountResolver>());
|
|
}
|
|
}
|