mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
The 10-photo / 3-video cap was a check-then-insert with no lock on both upload surfaces, so two overlapping uploads could both take the last slot. The board media upload and the vendor portal upload now run count, insert and save inside ExecuteWorkOrderMutationAsync. GetMediaContent maps .heic to image/heic.
409 lines
14 KiB
C#
409 lines
14 KiB
C#
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 WorkOrderCompletedSelectiveLockTests
|
|
{
|
|
private static string ToVersion(WorkOrder workOrder)
|
|
=> Convert.ToBase64String(workOrder.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 });
|
|
|
|
private static (ApplicationDbContext Context, WorkOrderBoardUpdateService Service) CreateBoardSut()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var service = new WorkOrderBoardUpdateService(
|
|
new WorkOrderBoardDataService(context),
|
|
new WorkOrderBoardMutationDataService(context),
|
|
audit);
|
|
return (context, service);
|
|
}
|
|
|
|
private static (ApplicationDbContext Context, WorkOrderMediaService Service) CreateMediaSut()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var service = new WorkOrderMediaService(
|
|
new WorkOrderMediaDataService(context),
|
|
new WorkOrderDetailDataService(context),
|
|
audit,
|
|
new TestFileStoragePort(),
|
|
new UpliftDataService(context));
|
|
return (context, service);
|
|
}
|
|
|
|
private static WorkOrder CompletedWorkOrder()
|
|
=> new()
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Completed,
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
};
|
|
|
|
[Fact]
|
|
public async Task PatchField_Completed_AllowsFlagColor()
|
|
{
|
|
var (context, service) = CreateBoardSut();
|
|
var wo = CompletedWorkOrder();
|
|
context.workOrders.Add(wo);
|
|
await context.SaveChangesAsync();
|
|
|
|
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
|
{
|
|
Field = WorkOrderBoardFieldNames.FlagColor,
|
|
Value = "#ef4444",
|
|
WorkOrderVersion = ToVersion(wo)
|
|
}, "actor-1");
|
|
|
|
Assert.Equal("#EF4444", result.FlagColor);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(WorkOrderBoardFieldNames.LifecycleStatus, "Scheduled")]
|
|
[InlineData(WorkOrderBoardFieldNames.SiteCode, "BK5")]
|
|
[InlineData(WorkOrderBoardFieldNames.AssignTo, "dispatcher-2")]
|
|
public async Task PatchField_Completed_BlocksCoreFields(string field, string value)
|
|
{
|
|
var (context, service) = CreateBoardSut();
|
|
var wo = CompletedWorkOrder();
|
|
context.workOrders.Add(wo);
|
|
await context.SaveChangesAsync();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
|
{
|
|
Field = field,
|
|
Value = value,
|
|
WorkOrderVersion = ToVersion(wo)
|
|
}, "actor-1"));
|
|
|
|
Assert.Equal("ReadOnly", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PatchField_Canceled_BlocksFlagColor()
|
|
{
|
|
var (context, service) = CreateBoardSut();
|
|
var wo = new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Canceled,
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
};
|
|
context.workOrders.Add(wo);
|
|
await context.SaveChangesAsync();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
|
{
|
|
Field = WorkOrderBoardFieldNames.FlagColor,
|
|
Value = "#ef4444",
|
|
WorkOrderVersion = ToVersion(wo)
|
|
}, "actor-1"));
|
|
|
|
Assert.Equal("CanceledReadOnly", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateComment_Completed_UpdatesText()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
context.workOrders.Add(CompletedWorkOrder());
|
|
context.Users.Add(new ApplicationUser
|
|
{
|
|
Id = "user-author",
|
|
UserName = "author",
|
|
FirstName = "Alice",
|
|
LastName = "Dispatcher"
|
|
});
|
|
var comment = new Comments
|
|
{
|
|
WorkerOrderId = 1,
|
|
UserId = "user-author",
|
|
Commenttext = "Original",
|
|
CommentType = "General",
|
|
RecordType = "WorkOrder",
|
|
CreatedDate = DateTime.UtcNow
|
|
};
|
|
context.Comments.Add(comment);
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = new WorkOrderCommentService(
|
|
new WorkOrderDetailDataService(context),
|
|
new CommentDataService(context),
|
|
new UserDataService(context),
|
|
WorkOrderAccountTestHelpers.Resolver(context));
|
|
|
|
var result = await service.UpdateCommentAsync(
|
|
1,
|
|
comment.Id,
|
|
new WorkOrderCommentCreateDto { Text = "Follow-up" },
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin("user-author"),
|
|
"user-author");
|
|
|
|
Assert.Equal("Follow-up", result.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddComment_Completed_AddsText()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
context.workOrders.Add(CompletedWorkOrder());
|
|
context.Users.Add(new ApplicationUser
|
|
{
|
|
Id = "user-author",
|
|
UserName = "author",
|
|
FirstName = "Alice",
|
|
LastName = "Dispatcher"
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = new WorkOrderCommentService(
|
|
new WorkOrderDetailDataService(context),
|
|
new CommentDataService(context),
|
|
new UserDataService(context),
|
|
WorkOrderAccountTestHelpers.Resolver(context));
|
|
|
|
var result = await service.AddCommentAsync(
|
|
1,
|
|
new WorkOrderCommentCreateDto { Text = "Follow-up" },
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin("user-author"),
|
|
"user-author");
|
|
|
|
Assert.Equal("Follow-up", result.Text);
|
|
Assert.Equal("user-author", result.AuthorId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddComment_Canceled_ThrowsReadOnly()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Canceled,
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = new WorkOrderCommentService(
|
|
new WorkOrderDetailDataService(context),
|
|
new CommentDataService(context),
|
|
new UserDataService(context),
|
|
WorkOrderAccountTestHelpers.Resolver(context));
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.AddCommentAsync(
|
|
1,
|
|
new WorkOrderCommentCreateDto { Text = "Should not persist" },
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin("user-author"),
|
|
"user-author"));
|
|
|
|
Assert.Equal("ReadOnly", ex.Code);
|
|
Assert.Empty(context.Comments);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddMedia_Completed_AllowsExtra()
|
|
{
|
|
var (context, service) = CreateMediaSut();
|
|
context.workOrders.Add(CompletedWorkOrder());
|
|
await context.SaveChangesAsync();
|
|
|
|
var media = await service.AddMediaAsync(
|
|
1,
|
|
WorkOrderMediaCategory.Extra,
|
|
"https://example.com/extra.pdf",
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"actor-1");
|
|
|
|
Assert.True(media.Id > 0);
|
|
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddMedia_Completed_AllowsAveta()
|
|
{
|
|
var (context, service) = CreateMediaSut();
|
|
context.workOrders.Add(CompletedWorkOrder());
|
|
await context.SaveChangesAsync();
|
|
|
|
var media = await service.AddMediaAsync(
|
|
1,
|
|
WorkOrderMediaCategory.Aveta,
|
|
"https://example.com/aveta.pdf",
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"actor-1");
|
|
|
|
Assert.True(media.Id > 0);
|
|
Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddMedia_Completed_BlocksBefore()
|
|
{
|
|
var (context, service) = CreateMediaSut();
|
|
context.workOrders.Add(CompletedWorkOrder());
|
|
await context.SaveChangesAsync();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.AddMediaAsync(
|
|
1,
|
|
WorkOrderMediaCategory.Before,
|
|
"https://example.com/before.jpg",
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"actor-1"));
|
|
|
|
Assert.Equal("ReadOnly", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateMediaCategory_Completed_KeepsExtra()
|
|
{
|
|
var (context, service) = CreateMediaSut();
|
|
var wo = CompletedWorkOrder();
|
|
context.workOrders.Add(wo);
|
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
|
{
|
|
Id = 10,
|
|
WorkorderId = 1,
|
|
Attachments = "https://example.com/extra.pdf",
|
|
Category = WorkOrderMediaCategory.Extra
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var media = await service.UpdateMediaCategoryAsync(
|
|
1,
|
|
10,
|
|
WorkOrderMediaCategory.Extra,
|
|
ToVersion(wo),
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"actor-1");
|
|
|
|
Assert.Equal(WorkOrderMediaCategory.Extra, media.Category);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateMediaCategory_Completed_AllowsAveta()
|
|
{
|
|
var (context, service) = CreateMediaSut();
|
|
var wo = CompletedWorkOrder();
|
|
context.workOrders.Add(wo);
|
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
|
{
|
|
Id = 10,
|
|
WorkorderId = 1,
|
|
Attachments = "https://example.com/extra.pdf",
|
|
Category = WorkOrderMediaCategory.Extra
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var media = await service.UpdateMediaCategoryAsync(
|
|
1,
|
|
10,
|
|
WorkOrderMediaCategory.Aveta,
|
|
ToVersion(wo),
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"actor-1");
|
|
|
|
Assert.Equal(WorkOrderMediaCategory.Aveta, media.Category);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateMediaCategory_Completed_BlocksMoveToBefore()
|
|
{
|
|
var (context, service) = CreateMediaSut();
|
|
var wo = CompletedWorkOrder();
|
|
context.workOrders.Add(wo);
|
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
|
{
|
|
Id = 10,
|
|
WorkorderId = 1,
|
|
Attachments = "https://example.com/extra.pdf",
|
|
Category = WorkOrderMediaCategory.Extra
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.UpdateMediaCategoryAsync(
|
|
1,
|
|
10,
|
|
WorkOrderMediaCategory.Before,
|
|
ToVersion(wo),
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"actor-1"));
|
|
|
|
Assert.Equal("ReadOnly", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteMedia_Completed_AllowsExtra()
|
|
{
|
|
var (context, service) = CreateMediaSut();
|
|
var wo = CompletedWorkOrder();
|
|
context.workOrders.Add(wo);
|
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
|
{
|
|
Id = 10,
|
|
WorkorderId = 1,
|
|
Attachments = "https://example.com/extra.pdf",
|
|
Category = WorkOrderMediaCategory.Extra
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
await service.DeleteMediaAsync(
|
|
1,
|
|
10,
|
|
ToVersion(wo),
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"actor-1");
|
|
|
|
Assert.True(context.workOrderAttachments.Single().IsDeleted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddMedia_Canceled_BlocksExtra()
|
|
{
|
|
var (context, service) = CreateMediaSut();
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Canceled,
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.AddMediaAsync(
|
|
1,
|
|
WorkOrderMediaCategory.Extra,
|
|
"https://example.com/extra.pdf",
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
"actor-1"));
|
|
|
|
Assert.Equal("ReadOnly", ex.Code);
|
|
}
|
|
}
|