mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
Stamp WorkOrder.AccountId on all create paths and filter board/list/search/detail by server-derived account claims so scoped callers cannot cross accounts. Co-authored-by: Cursor <cursoragent@cursor.com>
304 lines
12 KiB
C#
304 lines
12 KiB
C#
using System.Security.Claims;
|
|
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.Validation;
|
|
using Xunit;
|
|
|
|
namespace SeaHavenIndustries.Tests;
|
|
|
|
public class WorkOrderAccountScopeTests
|
|
{
|
|
private static ApplicationDbContext CreateContext()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
return new ApplicationDbContext(options);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BoardCreate_WithAccountClaim_StampsAccountId()
|
|
{
|
|
await using var context = CreateContext();
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 7, "Scoped Co");
|
|
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
var boardData = new WorkOrderBoardDataService(context);
|
|
var mutationData = new WorkOrderBoardMutationDataService(context);
|
|
var boardService = new WorkOrderBoardService(boardData, resolver);
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var create = new WorkOrderBoardCreateService(
|
|
boardData, mutationData, boardService, audit, new WorkOrderBoardCreateValidation(), resolver);
|
|
|
|
var user = WorkOrderAccountTestHelpers.AccountUser("actor-1", 7, "Dispatcher");
|
|
var row = await create.CreateAsync(
|
|
new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5"
|
|
},
|
|
user,
|
|
"actor-1");
|
|
|
|
var wo = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == row.Id);
|
|
Assert.Equal(7, wo.AccountId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BoardCreate_MissingScope_ThrowsForbidden()
|
|
{
|
|
await using var context = CreateContext();
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
var boardData = new WorkOrderBoardDataService(context);
|
|
var mutationData = new WorkOrderBoardMutationDataService(context);
|
|
var boardService = new WorkOrderBoardService(boardData, resolver);
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var create = new WorkOrderBoardCreateService(
|
|
boardData, mutationData, boardService, audit, new WorkOrderBoardCreateValidation(), resolver);
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
create.CreateAsync(
|
|
new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5"
|
|
},
|
|
WorkOrderAccountTestHelpers.MissingScope(),
|
|
"actor-1"));
|
|
|
|
Assert.Equal("Forbidden", ex.Code);
|
|
Assert.Empty(context.workOrders);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BoardCreate_OrgWide_WithUniqueCustomer_StampsAccountId()
|
|
{
|
|
await using var context = CreateContext();
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 3, "Unique Customer");
|
|
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
var boardData = new WorkOrderBoardDataService(context);
|
|
var mutationData = new WorkOrderBoardMutationDataService(context);
|
|
var boardService = new WorkOrderBoardService(boardData, resolver);
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var create = new WorkOrderBoardCreateService(
|
|
boardData, mutationData, boardService, audit, new WorkOrderBoardCreateValidation(), resolver);
|
|
|
|
var row = await create.CreateAsync(
|
|
new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PO,
|
|
SiteCode = "DAL",
|
|
Customer = "Unique Customer"
|
|
},
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"admin-1");
|
|
|
|
var wo = await context.workOrders.AsNoTracking().SingleAsync(w => w.Id == row.Id);
|
|
Assert.Equal(3, wo.AccountId);
|
|
Assert.Equal("Unique Customer", wo.Customer);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BoardCreate_OrgWide_UnresolvedCustomer_ThrowsAccountUnresolved()
|
|
{
|
|
await using var context = CreateContext();
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
var boardData = new WorkOrderBoardDataService(context);
|
|
var mutationData = new WorkOrderBoardMutationDataService(context);
|
|
var boardService = new WorkOrderBoardService(boardData, resolver);
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var create = new WorkOrderBoardCreateService(
|
|
boardData, mutationData, boardService, audit, new WorkOrderBoardCreateValidation(), resolver);
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
create.CreateAsync(
|
|
new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5"
|
|
},
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"admin-1"));
|
|
|
|
Assert.Equal("AccountUnresolved", ex.Code);
|
|
Assert.Empty(context.workOrders);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Board_ScopedUser_HidesOtherAccountAndNullAccountRows()
|
|
{
|
|
await using var context = CreateContext();
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 1, "A");
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 2, "B");
|
|
|
|
context.workOrders.AddRange(
|
|
new WorkOrder
|
|
{
|
|
Id = 1,
|
|
InternalWONumber = "00000000001",
|
|
AccountId = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
ScheduledDate = new DateTime(2026, 8, 10),
|
|
istemplate = false
|
|
},
|
|
new WorkOrder
|
|
{
|
|
Id = 2,
|
|
InternalWONumber = "00000000002",
|
|
AccountId = 2,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
ScheduledDate = new DateTime(2026, 8, 11),
|
|
istemplate = false
|
|
},
|
|
new WorkOrder
|
|
{
|
|
Id = 3,
|
|
InternalWONumber = "00000000003",
|
|
AccountId = null,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
ScheduledDate = new DateTime(2026, 8, 12),
|
|
istemplate = false
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
var boardService = new WorkOrderBoardService(new WorkOrderBoardDataService(context), resolver);
|
|
|
|
var scoped = await boardService.GetBoardAsync(
|
|
new WorkOrderBoardQueryDto
|
|
{
|
|
WeekStart = new DateOnly(2026, 8, 10),
|
|
WeekEnd = new DateOnly(2026, 8, 14)
|
|
},
|
|
WorkOrderAccountTestHelpers.AccountUser("disp-1", 1, "Dispatcher"),
|
|
"disp-1");
|
|
|
|
Assert.Single(scoped.Scheduled);
|
|
Assert.Equal(1, scoped.Scheduled[0].Id);
|
|
|
|
var orgWide = await boardService.GetBoardAsync(
|
|
new WorkOrderBoardQueryDto
|
|
{
|
|
WeekStart = new DateOnly(2026, 8, 10),
|
|
WeekEnd = new DateOnly(2026, 8, 14)
|
|
},
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"admin-1");
|
|
|
|
Assert.Equal(3, orgWide.Scheduled.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Detail_CrossAccount_ReturnsNull()
|
|
{
|
|
await using var context = CreateContext();
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 1, "A");
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 2, "B");
|
|
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 10,
|
|
InternalWONumber = "00000000010",
|
|
AccountId = 2,
|
|
LifecycleStatus = LifecycleStatus.Incomplete,
|
|
istemplate = false
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
var boardService = new WorkOrderBoardService(new WorkOrderBoardDataService(context), resolver);
|
|
var detail = new WorkOrderDetailService(
|
|
boardService,
|
|
new WorkOrderDetailDataService(context),
|
|
new CompletionDocTemplateDataService(context),
|
|
new UserDataService(context),
|
|
resolver);
|
|
|
|
var result = await detail.GetDetailAsync(
|
|
10,
|
|
WorkOrderAccountTestHelpers.AccountUser("disp-1", 1, "Dispatcher"));
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Ingest_UnresolvedCustomer_SkipsCreate()
|
|
{
|
|
await using var context = CreateContext();
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var ingest = new WorkOrderIngestService(
|
|
new WorkOrderIngestDataService(context),
|
|
new SyncFieldMergePolicy(fieldLocks),
|
|
fieldLocks,
|
|
audit,
|
|
WorkOrderAccountTestHelpers.Resolver(context));
|
|
|
|
var result = await ingest.UpsertBatchAsync(new[]
|
|
{
|
|
new WorkOrderIngestPayloadDto
|
|
{
|
|
ExternalWorkOrderId = "EXT-1",
|
|
Description = "No account",
|
|
Customer = "Unknown Customer"
|
|
}
|
|
});
|
|
|
|
Assert.Equal(1, result.Skipped);
|
|
Assert.Equal(0, result.Created);
|
|
Assert.Empty(context.workOrders);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Ingest_UniqueCustomer_StampsAccountId()
|
|
{
|
|
await using var context = CreateContext();
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 9, "Ingest Customer");
|
|
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var ingest = new WorkOrderIngestService(
|
|
new WorkOrderIngestDataService(context),
|
|
new SyncFieldMergePolicy(fieldLocks),
|
|
fieldLocks,
|
|
audit,
|
|
WorkOrderAccountTestHelpers.Resolver(context));
|
|
|
|
var result = await ingest.UpsertBatchAsync(new[]
|
|
{
|
|
new WorkOrderIngestPayloadDto
|
|
{
|
|
ExternalWorkOrderId = "EXT-9",
|
|
Description = "Has account",
|
|
Customer = "Ingest Customer"
|
|
}
|
|
});
|
|
|
|
Assert.Equal(1, result.Created);
|
|
var wo = Assert.Single(context.workOrders);
|
|
Assert.Equal(9, wo.AccountId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResolveAccountFilter_MissingScope_ThrowsForbidden()
|
|
{
|
|
await using var context = CreateContext();
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
|
|
var ex = Assert.Throws<WorkOrderBoardValidationException>(() =>
|
|
resolver.ResolveAccountFilter(WorkOrderAccountTestHelpers.MissingScope()));
|
|
|
|
Assert.Equal("Forbidden", ex.Code);
|
|
}
|
|
}
|