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() .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() .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(() => 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(() => 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() .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() .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() .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(() => 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(() => 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(() => 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(() => service.AddMediaAsync( 1, WorkOrderMediaCategory.Extra, "https://example.com/extra.pdf", WorkOrderAccountTestHelpers.OrgWideAdmin(), "actor-1")); Assert.Equal("ReadOnly", ex.Code); } }