using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Helpers; using SeaHaven.Services.Implementation; namespace SeaHavenIndustries.Tests; public class WorkOrderBoardConcurrencyTests { private static ApplicationDbContext CreateContext(string dbName) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(dbName) .Options; return new ApplicationDbContext(options); } private static WorkOrderBoardUpdateService CreateService(ApplicationDbContext context) { var boardData = new WorkOrderBoardDataService(context); var mutationData = new WorkOrderBoardMutationDataService(context); var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks); return new WorkOrderBoardUpdateService(boardData, mutationData, audit); } [Fact] public async Task ParallelVendorAndStatusEdits_OnDifferentAggregates_Succeed() { var dbName = Guid.NewGuid().ToString(); await using var seedContext = CreateContext(dbName); seedContext.Vendors.Add(new Vendor { Id = 3, CompanyName = "Parallel Vendor" }); var dispatch = new Dispatch { Id = 10, VendorId = 3, WorkOrderId = 1, RowVersion = new byte[] { 9, 0, 0, 0, 0, 0, 0, 9 } }; seedContext.Dispatches.Add(dispatch); seedContext.workOrders.Add(new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, ScheduledDate = new DateTime(2027, 6, 25), PrimaryDispatchId = 10, RowVersion = new byte[] { 8, 0, 0, 0, 0, 0, 0, 8 } }); await seedContext.SaveChangesAsync(); await using var woContext = CreateContext(dbName); await using var dispatchContext = CreateContext(dbName); var woService = CreateService(woContext); var dispatchService = CreateService(dispatchContext); var wo = await woContext.workOrders.FindAsync(1); var disp = await dispatchContext.Dispatches.FindAsync(10); var statusTask = woService.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.LifecycleStatus, Value = "Scheduled", WorkOrderVersion = Convert.ToBase64String(wo!.RowVersion!) }, "dispatcher-a"); var vendorTask = dispatchService.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.VendorId, Value = "3", WorkOrderVersion = Convert.ToBase64String(wo.RowVersion!), DispatchVersion = Convert.ToBase64String(disp!.RowVersion!), PrimaryDispatchId = 10 }, "dispatcher-b"); await Task.WhenAll(statusTask, vendorTask); Assert.Equal(LifecycleStatus.Scheduled, statusTask.Result.LifecycleStatus); Assert.Equal(3, vendorTask.Result.VendorId); } [Fact] public async Task StaleWorkOrderVersion_ThrowsConcurrencyWithCurrentState() { var dbName = Guid.NewGuid().ToString(); await using var context1 = CreateContext(dbName); context1.workOrders.Add(new WorkOrder { Id = 1, SiteCode = "A", LifecycleStatus = LifecycleStatus.Scheduled, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }); await context1.SaveChangesAsync(); await using var context2 = CreateContext(dbName); var wo2 = await context2.workOrders.FirstAsync(); wo2.SiteCode = "B"; await context2.SaveChangesAsync(); await using var patchContext = CreateContext(dbName); var service = CreateService(patchContext); var staleVersion = Convert.ToBase64String(new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.SiteCode, Value = "C", WorkOrderVersion = staleVersion }, "actor-1")); Assert.NotNull(ex.CurrentState); Assert.Equal("B", ex.CurrentState!.SiteCode); } [Fact] public async Task RescheduleAuditCount_MatchesChangedFields() { var dbName = Guid.NewGuid().ToString(); await using var context = CreateContext(dbName); context.workOrders.Add(new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, ScheduledDate = new DateTime(2026, 6, 20), RescheduleCount = 0, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }); await context.SaveChangesAsync(); var service = CreateService(context); var wo = await context.workOrders.FindAsync(1); await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ScheduledDate, Value = "2026-06-25", WorkOrderVersion = Convert.ToBase64String(wo!.RowVersion!) }, "actor-1"); var audits = await context.WorkOrderAuditLogs.ToListAsync(); Assert.Equal(3, audits.Count); Assert.Contains(audits, a => a.FieldName == "IsAddOn"); Assert.Contains(audits, a => a.FieldName == "ScheduledDate"); Assert.Contains(audits, a => a.FieldName == "RescheduleCount"); } [Fact] public async Task SecondEditWithStaleVersion_FailsAfterFirstEdit() { var dbName = Guid.NewGuid().ToString(); await using var seed = CreateContext(dbName); seed.workOrders.Add(new WorkOrder { Id = 1, SiteCode = "A", LifecycleStatus = LifecycleStatus.Scheduled, RowVersion = new byte[] { 4, 0, 0, 0, 0, 0, 0, 4 } }); await seed.SaveChangesAsync(); var originalVersion = Convert.ToBase64String(new byte[] { 4, 0, 0, 0, 0, 0, 0, 4 }); await using (var ctx1 = CreateContext(dbName)) { var svc1 = CreateService(ctx1); await svc1.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.SiteCode, Value = "B", WorkOrderVersion = originalVersion }, "user-1"); } await using var ctx2 = CreateContext(dbName); var svc2 = CreateService(ctx2); await Assert.ThrowsAsync(() => svc2.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.SiteCode, Value = "C", WorkOrderVersion = originalVersion }, "user-2")); } }