diff --git a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs index 22b10de..6133102 100644 --- a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs @@ -132,6 +132,13 @@ public class WorkOrderDetailServiceTests NewValue = "1", CreatedAt = DateTime.UtcNow }); + context.Users.Add(new ApplicationUser + { + Id = "user-1", + UserName = "alice", + FirstName = "Alice", + LastName = "Dispatcher" + }); context.Comments.Add(new Comments { WorkerOrderId = 1, @@ -151,11 +158,75 @@ public class WorkOrderDetailServiceTests Assert.Equal("HVAC PM Completion", detail.Completion.Template!.Name); Assert.Single(detail.Comments); Assert.Equal("user-1", detail.Comments[0].AuthorId); + Assert.Equal("Alice Dispatcher", detail.Comments[0].AuthorName); Assert.Equal("Called vendor", detail.Comments[0].Text); Assert.Single(detail.Audit); Assert.Equal("system", detail.Audit[0].Type); Assert.Equal("WeekRolled", detail.Audit[0].Action); } + + [Fact] + public async Task GetAudit_ResolvesAssignToUserIdsToDisplayNames() + { + var (context, service) = CreateSut(); + const string oldUserId = "a1000001-0001-4000-8000-000000000002"; + const string newUserId = "b5e356d5-d926-4f92-8673-ee13cddeff0f"; + + context.Users.AddRange( + new ApplicationUser + { + Id = oldUserId, + UserName = "alice", + FirstName = "Alice", + LastName = "Dispatcher" + }, + new ApplicationUser + { + Id = newUserId, + UserName = "bob", + FirstName = "Bob", + LastName = "Tech" + }); + context.workOrders.Add(new WorkOrder + { + Id = 1, + InternalWONumber = "00000000001", + LifecycleStatus = LifecycleStatus.Pending, + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }); + context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + { + WorkOrderId = 1, + UserId = "actor-1", + EventType = "Manual", + Action = "AssignmentChanged", + FieldName = "AssignTo", + OldValue = oldUserId, + NewValue = newUserId, + CreatedAt = DateTime.UtcNow + }); + context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog + { + WorkOrderId = 1, + UserId = "actor-1", + EventType = "Manual", + Action = "AssignmentChanged", + FieldName = "AssignTo", + OldValue = newUserId, + NewValue = "", + CreatedAt = DateTime.UtcNow.AddMinutes(1) + }); + await context.SaveChangesAsync(); + + var audit = await service.GetAuditAsync(1); + + Assert.NotNull(audit); + Assert.Equal(2, audit!.Count); + var unassigned = audit.Single(a => a.NewValue == "Unassigned"); + Assert.Equal("Bob Tech", unassigned.OldValue); + var reassigned = audit.Single(a => a.NewValue == "Bob Tech"); + Assert.Equal("Alice Dispatcher", reassigned.OldValue); + } } public class WorkOrderCompletionServiceTests @@ -307,6 +378,13 @@ public class WorkOrderCommentServiceTests .Options; var context = new ApplicationDbContext(options); context.workOrders.Add(new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled }); + context.Users.Add(new ApplicationUser + { + Id = "user-abc", + UserName = "bob", + FirstName = "Bob", + LastName = "Tech" + }); await context.SaveChangesAsync(); var detailData = new WorkOrderDetailDataService(context); @@ -317,9 +395,180 @@ public class WorkOrderCommentServiceTests var result = await service.AddCommentAsync(1, new WorkOrderCommentCreateDto { Text = "Note" }, "user-abc"); Assert.Equal("user-abc", result.AuthorId); + Assert.Equal("Bob Tech", result.AuthorName); Assert.Equal("Note", result.Text); Assert.False(string.IsNullOrWhiteSpace(result.Time)); } + + [Fact] + public async Task GetComments_FallsBackToCommenterWhenUserIdMissing() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var context = new ApplicationDbContext(options); + context.workOrders.Add(new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled }); + context.Comments.Add(new Comments + { + WorkerOrderId = 1, + Commenter = "Legacy Sync Author", + Commenttext = "Synced note", + CreatedDate = DateTime.UtcNow + }); + await context.SaveChangesAsync(); + + var detailData = new WorkOrderDetailDataService(context); + var commentData = new CommentDataService(context); + var userData = new UserDataService(context); + var service = new WorkOrderCommentService(detailData, commentData, userData); + + var comments = await service.GetCommentsAsync(1); + + Assert.NotNull(comments); + Assert.Single(comments!); + Assert.Null(comments[0].AuthorId); + Assert.Equal("Legacy Sync Author", comments[0].AuthorName); + Assert.Equal("Synced note", comments[0].Text); + } + + private static async Task<(ApplicationDbContext Context, WorkOrderCommentService Service, Comments Comment)> SeedEditableCommentAsync( + LifecycleStatus status = LifecycleStatus.Scheduled, + string authorId = "user-author", + string commentType = "General", + string recordType = "WorkOrder", + int workOrderId = 1) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var context = new ApplicationDbContext(options); + context.workOrders.Add(new WorkOrder { Id = workOrderId, LifecycleStatus = status }); + context.Users.Add(new ApplicationUser + { + Id = authorId, + UserName = "author", + FirstName = "Alice", + LastName = "Dispatcher" + }); + var comment = new Comments + { + WorkerOrderId = workOrderId, + UserId = authorId, + Commenttext = "Original", + CommentType = commentType, + RecordType = recordType, + CreatedDate = DateTime.UtcNow + }; + context.Comments.Add(comment); + await context.SaveChangesAsync(); + + var service = new WorkOrderCommentService( + new WorkOrderDetailDataService(context), + new CommentDataService(context), + new UserDataService(context)); + return (context, service, comment); + } + + [Fact] + public async Task UpdateComment_Author_UpdatesText() + { + var (_, service, comment) = await SeedEditableCommentAsync(); + + var result = await service.UpdateCommentAsync( + 1, + comment.Id, + new WorkOrderCommentCreateDto { Text = " Updated note " }, + "user-author", + isAdmin: false); + + Assert.Equal("Updated note", result.Text); + Assert.Equal("user-author", result.AuthorId); + Assert.Equal("Alice Dispatcher", result.AuthorName); + } + + [Fact] + public async Task UpdateComment_Admin_CanEditOthers() + { + var (_, service, comment) = await SeedEditableCommentAsync(); + + var result = await service.UpdateCommentAsync( + 1, + comment.Id, + new WorkOrderCommentCreateDto { Text = "Admin edit" }, + "admin-user", + isAdmin: true); + + Assert.Equal("Admin edit", result.Text); + Assert.Equal("user-author", result.AuthorId); + } + + [Fact] + public async Task UpdateComment_NonAuthorNonAdmin_ThrowsForbidden() + { + var (_, service, comment) = await SeedEditableCommentAsync(); + + var ex = await Assert.ThrowsAsync(() => + service.UpdateCommentAsync( + 1, + comment.Id, + new WorkOrderCommentCreateDto { Text = "Nope" }, + "other-user", + isAdmin: false)); + + Assert.Equal("Forbidden", ex.Code); + } + + [Fact] + public async Task UpdateComment_LegacyVendorComment_ThrowsNotEditable() + { + var (_, service, comment) = await SeedEditableCommentAsync( + commentType: "vendor", + recordType: "WorkOrder"); + + var ex = await Assert.ThrowsAsync(() => + service.UpdateCommentAsync( + 1, + comment.Id, + new WorkOrderCommentCreateDto { Text = "Nope" }, + "user-author", + isAdmin: false)); + + Assert.Equal("NotEditable", ex.Code); + } + + [Fact] + public async Task UpdateComment_ReadOnlyWorkOrder_Throws() + { + var (_, service, comment) = await SeedEditableCommentAsync(LifecycleStatus.Canceled); + + var ex = await Assert.ThrowsAsync(() => + service.UpdateCommentAsync( + 1, + comment.Id, + new WorkOrderCommentCreateDto { Text = "Nope" }, + "user-author", + isAdmin: false)); + + Assert.Equal("ReadOnly", ex.Code); + } + + [Fact] + public async Task UpdateComment_WrongWorkOrder_ThrowsNotFound() + { + var (context, service, comment) = await SeedEditableCommentAsync(workOrderId: 1); + context.workOrders.Add(new WorkOrder { Id = 2, LifecycleStatus = LifecycleStatus.Scheduled }); + await context.SaveChangesAsync(); + + var ex = await Assert.ThrowsAsync(() => + service.UpdateCommentAsync( + 2, + comment.Id, + new WorkOrderCommentCreateDto { Text = "Nope" }, + "user-author", + isAdmin: false)); + + Assert.Equal("NotFound", ex.Code); + } } public class WorkOrderMediaServiceTests