shoc-backend/SeaHavenIndustries.Tests/WorkOrderPocServiceTests.cs

641 lines
25 KiB
C#
Raw Normal View History

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;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
namespace SeaHavenIndustries.Tests;
public class WorkOrderPocServiceTests
{
[Fact]
public async Task ManualOverridePersistsIsAuditedAndLocks()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100");
await context.SaveChangesAsync();
var service = CreatePocService(context);
var row = await service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", "Call first", workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == workOrder.Id);
Assert.Equal("Manual POC", persisted.PocName);
Assert.Equal("312-555-0001", persisted.PocPhone);
Assert.Equal("Call first", persisted.PocNotes);
Assert.Equal("Manual POC", row.PocName);
Assert.Equal("312-555-0001", row.PocPhone);
Assert.True(row.PocCustomized);
var auditName = await context.WorkOrderAuditLogs.SingleAsync(a =>
a.WorkOrderId == workOrder.Id && a.FieldName == "PocName");
Assert.Equal("Alice Site", auditName.OldValue);
Assert.Equal("Manual POC", auditName.NewValue);
Assert.Equal("FieldChanged", auditName.Action);
Assert.True(await context.WorkOrderFieldLocks.AnyAsync(l =>
l.WorkOrderId == workOrder.Id && l.FieldName == "PocName"));
Assert.True(await context.WorkOrderFieldLocks.AnyAsync(l =>
l.WorkOrderId == workOrder.Id && l.FieldName == "PocPhone"));
}
[Fact]
public async Task ManualOverrideSurvivesCompleteAndLaterSiteChanges()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
workOrder.LifecycleStatus = LifecycleStatus.Scheduled;
workOrder.ScheduledDate = DateTime.UtcNow.Date.AddDays(1);
await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100");
await context.SaveChangesAsync();
var service = CreatePocService(context);
await service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
var updateService = CreateUpdateService(context);
var completed = await updateService.PatchFieldAsync(
workOrder.Id,
Patch(WorkOrderBoardFieldNames.LifecycleStatus, "Complete", workOrder),
"actor-1");
Assert.Equal("Manual POC", completed.PocName);
var siteContact = await context.Contacts.SingleAsync(c => c.FirstName == "Alice");
siteContact.FirstName = "Changed";
siteContact.PhoneNumber = "999-555-0100";
await context.SaveChangesAsync();
var row = await CreateBoardService(context).GetBoardRowAsync(
workOrder.Id, WorkOrderAccountTestHelpers.AccountUser());
Assert.Equal("Manual POC", row!.PocName);
Assert.Equal("312-555-0001", row.PocPhone);
}
[Fact]
public async Task SiteContactUpdateDoesNotOverwriteManualOverride()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
var siteContact = await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100");
await context.SaveChangesAsync();
var service = CreatePocService(context);
await service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
siteContact.FirstName = "Rotated";
siteContact.PhoneNumber = "999-555-0100";
await context.SaveChangesAsync();
var row = await CreateBoardService(context).GetBoardRowAsync(
workOrder.Id, WorkOrderAccountTestHelpers.AccountUser());
Assert.Equal("Manual POC", row!.PocName);
Assert.Equal("312-555-0001", row.PocPhone);
Assert.True(row.PocCustomized);
}
[Fact]
public async Task UnlockedWorkOrderFollowsSiteContact()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
var primary = await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100", order: 0);
await SeedSiteContactAsync(context, firstName: "Bob", lastName: "Backup", phone: "312-555-0200", order: 1);
await context.SaveChangesAsync();
var boardService = CreateBoardService(context);
var before = await boardService.GetBoardRowAsync(workOrder.Id, WorkOrderAccountTestHelpers.AccountUser());
Assert.Equal("Alice Site", before!.PocName);
Assert.Equal("312-555-0100", before.PocPhone);
Assert.False(before.PocCustomized);
primary.PhoneNumber = "312-555-0999";
await context.SaveChangesAsync();
var after = await boardService.GetBoardRowAsync(workOrder.Id, WorkOrderAccountTestHelpers.AccountUser());
Assert.Equal("Alice Site", after!.PocName);
Assert.Equal("312-555-0999", after.PocPhone);
Assert.False(after.PocCustomized);
}
[Fact]
public async Task EditEqualToSiteContactDoesNotLock()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100");
await context.SaveChangesAsync();
var service = CreatePocService(context);
await service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
var row = await service.UpdatePocAsync(
workOrder.Id,
Poc("Alice Site", "312-555-0100", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == workOrder.Id);
Assert.Null(persisted.PocName);
Assert.Null(persisted.PocPhone);
Assert.Equal("Alice Site", row.PocName);
Assert.Equal("312-555-0100", row.PocPhone);
Assert.False(row.PocCustomized);
var auditName = await context.WorkOrderAuditLogs.Where(a =>
a.WorkOrderId == workOrder.Id && a.FieldName == "PocName")
.OrderByDescending(a => a.Id)
.FirstAsync();
Assert.Equal("Manual POC", auditName.OldValue);
Assert.Equal("Alice Site", auditName.NewValue);
}
[Fact]
public async Task ClearingBothFieldsFollowsSiteAgain()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100");
await context.SaveChangesAsync();
var service = CreatePocService(context);
await service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", "keep notes", workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
var row = await service.UpdatePocAsync(
workOrder.Id,
Poc(null, null, null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == workOrder.Id);
Assert.Null(persisted.PocName);
Assert.Null(persisted.PocPhone);
Assert.Equal("Alice Site", row.PocName);
Assert.False(row.PocCustomized);
}
[Fact]
public async Task CrossTenantUpdateIsRejected()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context, accountId: 2);
await context.SaveChangesAsync();
var service = CreatePocService(context);
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(accountId: 1),
"actor-1"));
Assert.Equal("NotFound", ex.Code);
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == workOrder.Id);
Assert.Null(persisted.PocName);
Assert.Empty(await context.WorkOrderAuditLogs.ToListAsync());
}
[Fact]
public async Task MissingAccountScopeIsForbidden()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
await context.SaveChangesAsync();
var service = CreatePocService(context);
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", null, workOrder),
WorkOrderAccountTestHelpers.MissingScope(),
"actor-1"));
Assert.Equal("Forbidden", ex.Code);
}
[Fact]
public async Task CompletedWorkOrderIsReadOnly()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
workOrder.LifecycleStatus = LifecycleStatus.Completed;
workOrder.CompletedDate = DateTime.UtcNow;
await context.SaveChangesAsync();
var service = CreatePocService(context);
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1"));
Assert.Equal("ReadOnly", ex.Code);
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == workOrder.Id);
Assert.Null(persisted.PocName);
}
[Fact]
public async Task CanceledWorkOrderIsReadOnly()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
workOrder.LifecycleStatus = LifecycleStatus.Canceled;
await context.SaveChangesAsync();
var service = CreatePocService(context);
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
service.UpdatePocAsync(
workOrder.Id,
Poc("Manual POC", "312-555-0001", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1"));
Assert.Equal("CanceledReadOnly", ex.Code);
}
[Fact]
public async Task StaleVersionThrowsConflict()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
await context.SaveChangesAsync();
var service = CreatePocService(context);
var stale = new WorkOrderPocUpdateRequestDto
{
PocName = "Manual POC",
PocPhone = "312-555-0001",
WorkOrderVersion = Convert.ToBase64String(new byte[] { 9, 9, 9, 9 })
};
await Assert.ThrowsAsync<WorkOrderBoardConcurrencyException>(() =>
service.UpdatePocAsync(workOrder.Id, stale, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == workOrder.Id);
Assert.Null(persisted.PocName);
}
[Fact]
public async Task CompletionFreezesSiteContactWhenNoOverrideOrLink()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
workOrder.LifecycleStatus = LifecycleStatus.Scheduled;
workOrder.ScheduledDate = DateTime.UtcNow.Date.AddDays(1);
await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100");
await context.SaveChangesAsync();
var updateService = CreateUpdateService(context);
var completed = await updateService.PatchFieldAsync(
workOrder.Id,
Patch(WorkOrderBoardFieldNames.LifecycleStatus, "Complete", workOrder),
"actor-1");
Assert.Equal(LifecycleStatus.Completed, completed.LifecycleStatus);
Assert.Equal("Alice Site", completed.PocName);
Assert.Equal("312-555-0100", completed.PocPhone);
var contact = await context.Contacts.SingleAsync();
contact.PhoneNumber = "999-555-0100";
await context.SaveChangesAsync();
var row = await CreateBoardService(context).GetBoardRowAsync(
workOrder.Id, WorkOrderAccountTestHelpers.AccountUser());
Assert.Equal("Alice Site", row!.PocName);
Assert.Equal("312-555-0100", row.PocPhone);
}
[Fact]
public async Task BoardCreateWithPocEqualToSiteContactFollowsSite()
{
await using var context = CreateContext();
WorkOrderAccountTestHelpers.SeedBoardCreateScope(context);
context.Contacts.Add(new Contacts
{
FirstName = "Alice",
LastName = "Site",
PhoneNumber = "312-555-0100",
LocationId = 1,
SiteContactOrder = 0
});
await context.SaveChangesAsync();
var service = CreateCreateService(context);
var row = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.PM,
SiteCode = "BK5",
LocationId = 1,
PocName = "Alice Site",
PocPhone = "312-555-0100"
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == row.Id);
Assert.Null(persisted.PocName);
Assert.Null(persisted.PocPhone);
Assert.Equal("Alice Site", row.PocName);
Assert.False(row.PocCustomized);
}
[Fact]
public async Task BoardCreateWithPocAwayFromSiteContactIsStored()
{
await using var context = CreateContext();
WorkOrderAccountTestHelpers.SeedBoardCreateScope(context);
context.Contacts.Add(new Contacts
{
FirstName = "Alice",
LastName = "Site",
PhoneNumber = "312-555-0100",
LocationId = 1,
SiteContactOrder = 0
});
await context.SaveChangesAsync();
var service = CreateCreateService(context);
var row = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.PM,
SiteCode = "BK5",
LocationId = 1,
PocName = "Manual POC",
PocPhone = "312-555-0001"
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == row.Id);
Assert.Equal("Manual POC", persisted.PocName);
Assert.Equal("312-555-0001", persisted.PocPhone);
Assert.True(row.PocCustomized);
}
[Fact]
public async Task EditToSecondSiteContactStoresOverride()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100", order: 0);
await SeedSiteContactAsync(context, firstName: "Bob", lastName: "Backup", phone: "312-555-0200", order: 1);
await context.SaveChangesAsync();
// The WO follows the Site primary (Alice). Editing to the second live Site
// contact (Bob) is a change away from what the board shows and must be
// stored and locked, not silently cleared back to Alice.
var service = CreatePocService(context);
var row = await service.UpdatePocAsync(
workOrder.Id,
Poc("Bob Backup", "312-555-0200", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == workOrder.Id);
Assert.Equal("Bob Backup", persisted.PocName);
Assert.Equal("312-555-0200", persisted.PocPhone);
Assert.Equal("Bob Backup", row.PocName);
Assert.Equal("312-555-0200", row.PocPhone);
Assert.True(row.PocCustomized);
var auditName = await context.WorkOrderAuditLogs.SingleAsync(a =>
a.WorkOrderId == workOrder.Id && a.FieldName == "PocName");
Assert.Equal("Alice Site", auditName.OldValue);
Assert.Equal("Bob Backup", auditName.NewValue);
Assert.True(await context.WorkOrderFieldLocks.AnyAsync(l =>
l.WorkOrderId == workOrder.Id && l.FieldName == "PocName"));
Assert.True(await context.WorkOrderFieldLocks.AnyAsync(l =>
l.WorkOrderId == workOrder.Id && l.FieldName == "PocPhone"));
}
[Fact]
public async Task EditToSitePrimaryWhenLinkedContactDiffersStoresOverride()
{
await using var context = CreateContext();
await SeedScopeAsync(context);
var workOrder = NewWorkOrder(context);
await SeedSiteContactAsync(context, firstName: "Alice", lastName: "Site", phone: "312-555-0100", order: 0);
var linked = new Contacts
{
FirstName = "Carol",
LastName = "Linked",
PhoneNumber = "312-555-0300"
};
context.Contacts.Add(linked);
await context.SaveChangesAsync();
context.WorkOrderContacts.Add(new WorkOrderContacts
{
WorkorderId = workOrder.Id,
ContactId = linked.Id
});
await context.SaveChangesAsync();
// The WO follows its linked contact (Carol). Typing the Site's primary
// (Alice) is a change away from what the board shows and must be stored,
// not cleared back to Carol.
var service = CreatePocService(context);
var row = await service.UpdatePocAsync(
workOrder.Id,
Poc("Alice Site", "312-555-0100", null, workOrder),
WorkOrderAccountTestHelpers.AccountUser(),
"actor-1");
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == workOrder.Id);
Assert.Equal("Alice Site", persisted.PocName);
Assert.Equal("312-555-0100", persisted.PocPhone);
Assert.Equal("Alice Site", row.PocName);
Assert.Equal("312-555-0100", row.PocPhone);
Assert.True(row.PocCustomized);
var auditName = await context.WorkOrderAuditLogs.SingleAsync(a =>
a.WorkOrderId == workOrder.Id && a.FieldName == "PocName");
Assert.Equal("Carol Linked", auditName.OldValue);
Assert.Equal("Alice Site", auditName.NewValue);
}
[Fact]
public async Task BoardCreateWithPocEqualToSecondSiteContactIsStored()
{
await using var context = CreateContext();
WorkOrderAccountTestHelpers.SeedBoardCreateScope(context);
context.Contacts.Add(new Contacts
{
FirstName = "Alice",
LastName = "Site",
PhoneNumber = "312-555-0100",
LocationId = 1,
SiteContactOrder = 0
});
context.Contacts.Add(new Contacts
{
FirstName = "Bob",
LastName = "Backup",
PhoneNumber = "312-555-0200",
LocationId = 1,
SiteContactOrder = 1
});
await context.SaveChangesAsync();
// Create-time POC equal to the second Site contact (Bob) is not the contact
// the WO would follow (the primary, Alice), so it must be stored.
var service = CreateCreateService(context);
var row = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.PM,
SiteCode = "BK5",
LocationId = 1,
PocName = "Bob Backup",
PocPhone = "312-555-0200"
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
var persisted = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == row.Id);
Assert.Equal("Bob Backup", persisted.PocName);
Assert.Equal("312-555-0200", persisted.PocPhone);
Assert.True(row.PocCustomized);
}
private static ApplicationDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ApplicationDbContext(options);
}
private static async Task SeedScopeAsync(ApplicationDbContext context)
{
WorkOrderAccountTestHelpers.SeedBoardCreateScope(context);
await context.SaveChangesAsync();
}
private static WorkOrder NewWorkOrder(ApplicationDbContext context, int accountId = 1)
{
var location = context.Locations.Local.Single(l => l.Id == 1);
var workOrder = new WorkOrder
{
Id = 1,
InternalWONumber = "00000012345",
SiteCode = "BK5",
LocationId = location.Id,
Locations = location,
AccountId = accountId,
LifecycleStatus = LifecycleStatus.Incomplete,
RowVersion = Version()
};
context.workOrders.Add(workOrder);
return workOrder;
}
private static async Task<Contacts> SeedSiteContactAsync(
ApplicationDbContext context,
string firstName,
string lastName,
string phone,
int order = 0)
{
var contact = new Contacts
{
FirstName = firstName,
LastName = lastName,
PhoneNumber = phone,
LocationId = 1,
SiteContactOrder = order
};
context.Contacts.Add(contact);
await context.SaveChangesAsync();
return contact;
}
private static WorkOrderPocService CreatePocService(ApplicationDbContext context)
=> new(
new WorkOrderPocDataService(context),
new WorkOrderBoardMutationDataService(context),
CreateBoardService(context),
new WorkOrderAuditService(
new WorkOrderAuditDataService(context),
new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context))),
WorkOrderAccountTestHelpers.Resolver(context));
private static WorkOrderBoardService CreateBoardService(ApplicationDbContext context)
=> new(
new WorkOrderBoardDataService(context),
WorkOrderAccountTestHelpers.Resolver(context));
private static WorkOrderBoardUpdateService CreateUpdateService(ApplicationDbContext context)
{
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
return WorkOrderAccountTestHelpers.BoardUpdateService(
context,
new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks));
}
private static WorkOrderBoardCreateService CreateCreateService(ApplicationDbContext context)
=> new(
new WorkOrderBoardDataService(context),
new WorkOrderBoardMutationDataService(context),
CreateBoardService(context),
new WorkOrderAuditService(
new WorkOrderAuditDataService(context),
new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context))),
new WorkOrderBoardCreateValidation(),
WorkOrderAccountTestHelpers.Resolver(context),
new WorkOrderPocDataService(context));
private static WorkOrderPocUpdateRequestDto Poc(
string? name,
string? phone,
string? notes,
WorkOrder workOrder)
=> new()
{
PocName = name,
PocPhone = phone,
PocNotes = notes,
WorkOrderVersion = Convert.ToBase64String(workOrder.RowVersion ?? Version())
};
private static WorkOrderBoardPatchRequestDto Patch(
string field,
string value,
WorkOrder workOrder)
=> new()
{
Field = field,
Value = value,
WorkOrderVersion = Convert.ToBase64String(workOrder.RowVersion ?? Version())
};
private static byte[] Version() => new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 };
}