mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
406 lines
15 KiB
C#
406 lines
15 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 WorkOrderDocStatusPatchTests
|
|
{
|
|
private static (ApplicationDbContext Context, WorkOrderBoardUpdateService Service) CreateSut()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
var boardData = new WorkOrderBoardDataService(context);
|
|
var fieldLocks = new WorkOrderFieldLockService(context);
|
|
var audit = new WorkOrderAuditService(context, fieldLocks);
|
|
var service = new WorkOrderBoardUpdateService(context, boardData, audit);
|
|
return (context, service);
|
|
}
|
|
|
|
private static string ToVersion(WorkOrder workOrder)
|
|
=> Convert.ToBase64String(workOrder.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 });
|
|
|
|
[Theory]
|
|
[InlineData("Yes", DocStatus.Yes)]
|
|
[InlineData("No", DocStatus.No)]
|
|
[InlineData("NotNeeded", DocStatus.NotNeeded)]
|
|
public async Task PatchField_DocStatus_SetsValue(string value, DocStatus expected)
|
|
{
|
|
var (context, service) = CreateSut();
|
|
var wo = new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
};
|
|
context.workOrders.Add(wo);
|
|
await context.SaveChangesAsync();
|
|
|
|
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
|
{
|
|
Field = WorkOrderBoardFieldNames.DocStatus,
|
|
Value = value,
|
|
WorkOrderVersion = ToVersion(wo)
|
|
}, "actor-1");
|
|
|
|
Assert.Equal(expected, result.DocStatus);
|
|
var audit = await context.WorkOrderAuditLogs.SingleAsync();
|
|
Assert.Equal("DocStatus", audit.FieldName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PatchField_DocStatus_ReadOnlyWorkOrder_Throws()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
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.DocStatus,
|
|
Value = "Yes",
|
|
WorkOrderVersion = ToVersion(wo)
|
|
}, "actor-1"));
|
|
|
|
Assert.Equal("CanceledReadOnly", ex.Code);
|
|
}
|
|
}
|
|
|
|
public class WorkOrderDetailServiceTests
|
|
{
|
|
private static (ApplicationDbContext Context, WorkOrderDetailService Service) CreateSut()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
var boardData = new WorkOrderBoardDataService(context);
|
|
var boardService = new WorkOrderBoardService(boardData);
|
|
var detailData = new WorkOrderDetailDataService(context);
|
|
var templateData = new CompletionDocTemplateDataService(context);
|
|
var userData = new UserDataService(context);
|
|
var service = new WorkOrderDetailService(boardService, detailData, templateData, userData);
|
|
return (context, service);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDetail_ReturnsDerivedFieldsAndFeAuditMapping()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
var wo = new WorkOrder
|
|
{
|
|
Id = 1,
|
|
InternalWONumber = "00000000001",
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
ScheduledDate = DateTime.UtcNow.AddDays(-2),
|
|
Trade = "HVAC PM",
|
|
WorkOrderType = WorkOrderType.PM,
|
|
DocStatus = DocStatus.No,
|
|
Description = "Leak",
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
};
|
|
context.workOrders.Add(wo);
|
|
context.CompletionDocTemplates.Add(new CompletionDocTemplate
|
|
{
|
|
Name = "HVAC PM Completion",
|
|
ServiceKey = "HVAC PM",
|
|
WorkOrderType = WorkOrderType.PM,
|
|
TemplateUrl = "https://example.com/hvac.pdf",
|
|
IsActive = true
|
|
});
|
|
context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
|
{
|
|
WorkOrderId = 1,
|
|
EventType = "System",
|
|
Action = "WeekRolled",
|
|
FieldName = "CarriedOver",
|
|
OldValue = "0",
|
|
NewValue = "1",
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
context.Comments.Add(new Comments
|
|
{
|
|
WorkerOrderId = 1,
|
|
UserId = "user-1",
|
|
Commenttext = "Called vendor",
|
|
CreatedDate = new DateTime(2026, 6, 1, 12, 0, 0, DateTimeKind.Utc)
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var detail = await service.GetDetailAsync(1);
|
|
|
|
Assert.NotNull(detail);
|
|
Assert.True(detail!.Info.IsPastDue);
|
|
Assert.Equal("HVAC PM", detail.Info.Trade);
|
|
Assert.Equal("Leak", detail.Info.Description);
|
|
Assert.NotNull(detail.Completion.Template);
|
|
Assert.Equal("HVAC PM Completion", detail.Completion.Template!.Name);
|
|
Assert.Single(detail.Comments);
|
|
Assert.Equal("user-1", detail.Comments[0].AuthorId);
|
|
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);
|
|
}
|
|
}
|
|
|
|
public class WorkOrderCompletionServiceTests
|
|
{
|
|
private static (ApplicationDbContext Context, WorkOrderCompletionService Service) CreateSut()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
var templateData = new CompletionDocTemplateDataService(context);
|
|
var detailData = new WorkOrderDetailDataService(context);
|
|
var fieldLocks = new WorkOrderFieldLockService(context);
|
|
var audit = new WorkOrderAuditService(context, fieldLocks);
|
|
var service = new WorkOrderCompletionService(context, templateData, detailData, audit);
|
|
return (context, service);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadCompletionDoc_SetsDocStatusYesAndSignOff()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
var wo = new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
Trade = "HVAC PM",
|
|
DocStatus = DocStatus.No,
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
};
|
|
context.workOrders.Add(wo);
|
|
await context.SaveChangesAsync();
|
|
|
|
var result = await service.UploadCompletionDocAsync(
|
|
1,
|
|
new WorkOrderCompletionDocUploadDto
|
|
{
|
|
SignOffName = "Jane Dispatcher",
|
|
WorkOrderVersion = Convert.ToBase64String(wo.RowVersion!)
|
|
},
|
|
"https://example.com/signed.pdf",
|
|
"actor-1");
|
|
|
|
Assert.Equal(DocStatus.Yes, result.DocStatus);
|
|
Assert.Equal("https://example.com/signed.pdf", result.SignOffAttachment);
|
|
Assert.Equal("Jane Dispatcher", result.SignOffName);
|
|
|
|
var updated = await context.workOrders.FindAsync(1);
|
|
Assert.Equal(DocStatus.Yes, updated!.DocStatus);
|
|
Assert.Equal(2, await context.WorkOrderAuditLogs.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadCompletionDoc_SecondUpload_DoesNotDuplicateFieldLocks()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
var wo = new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
DocStatus = DocStatus.No,
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
};
|
|
context.workOrders.Add(wo);
|
|
await context.SaveChangesAsync();
|
|
|
|
var version = Convert.ToBase64String(wo.RowVersion!);
|
|
await service.UploadCompletionDocAsync(
|
|
1,
|
|
new WorkOrderCompletionDocUploadDto { WorkOrderVersion = version },
|
|
"https://example.com/first.pdf",
|
|
"actor-1");
|
|
|
|
// Simulate a new request scope: clear local tracker then reload
|
|
context.ChangeTracker.Clear();
|
|
var reloaded = await context.workOrders.SingleAsync(w => w.Id == 1);
|
|
// InMemory does not bump rowversion; keep same bytes for the second call
|
|
reloaded.RowVersion ??= new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 };
|
|
|
|
await service.UploadCompletionDocAsync(
|
|
1,
|
|
new WorkOrderCompletionDocUploadDto
|
|
{
|
|
WorkOrderVersion = Convert.ToBase64String(reloaded.RowVersion!)
|
|
},
|
|
"https://example.com/second.pdf",
|
|
"actor-1");
|
|
|
|
var locks = await context.WorkOrderFieldLocks.ToListAsync();
|
|
Assert.Equal(1, locks.Count(l => l.FieldName == "SignOffAttachment"));
|
|
Assert.Equal(1, locks.Count(l => l.FieldName == "DocStatus"));
|
|
Assert.Equal("https://example.com/second.pdf", reloaded.SignOffAttachment);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadCompletionDoc_MissingVersion_Throws()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.UploadCompletionDocAsync(
|
|
1,
|
|
new WorkOrderCompletionDocUploadDto(),
|
|
"https://example.com/signed.pdf",
|
|
"actor-1"));
|
|
|
|
Assert.Equal("WorkOrderVersionRequired", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadCompletionDoc_ReadOnly_Throws()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Completed
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.UploadCompletionDocAsync(
|
|
1,
|
|
new WorkOrderCompletionDocUploadDto
|
|
{
|
|
WorkOrderVersion = Convert.ToBase64String(new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 })
|
|
},
|
|
"https://example.com/signed.pdf",
|
|
"actor-1"));
|
|
|
|
Assert.Equal("ReadOnly", ex.Code);
|
|
}
|
|
}
|
|
|
|
public class WorkOrderCommentServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task AddComment_ReturnsFeSchema()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
context.workOrders.Add(new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled });
|
|
await context.SaveChangesAsync();
|
|
|
|
var detailData = new WorkOrderDetailDataService(context);
|
|
var commentData = new CommentDataService(context);
|
|
var service = new WorkOrderCommentService(detailData, commentData);
|
|
|
|
var result = await service.AddCommentAsync(1, new WorkOrderCommentCreateDto { Text = "Note" }, "user-abc");
|
|
|
|
Assert.Equal("user-abc", result.AuthorId);
|
|
Assert.Equal("Note", result.Text);
|
|
Assert.False(string.IsNullOrWhiteSpace(result.Time));
|
|
}
|
|
}
|
|
|
|
public class WorkOrderMediaServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task GetMedia_IncludesLegacyBeforeAfterAndExtra()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
BeforPhotoAttachment = "https://example.com/before.jpg",
|
|
AfterPhotoAttachment = "https://example.com/after.jpg"
|
|
});
|
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
|
{
|
|
WorkorderId = 1,
|
|
Attachments = "https://example.com/extra.pdf",
|
|
Category = WorkOrderMediaCategory.Extra
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var detailData = new WorkOrderDetailDataService(context);
|
|
var service = new WorkOrderMediaService(context, detailData);
|
|
var media = await service.GetMediaAsync(1);
|
|
|
|
Assert.NotNull(media);
|
|
Assert.Equal(3, media!.Count);
|
|
Assert.Contains(media, m => m.Category == WorkOrderMediaCategory.Before);
|
|
Assert.Contains(media, m => m.Category == WorkOrderMediaCategory.After);
|
|
Assert.Contains(media, m => m.Category == WorkOrderMediaCategory.Extra);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteMedia_ReadOnlyWorkOrder_Throws()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Completed
|
|
});
|
|
context.workOrderAttachments.Add(new WorkOrderAttachments
|
|
{
|
|
Id = 10,
|
|
WorkorderId = 1,
|
|
Attachments = "https://example.com/extra.pdf",
|
|
Category = WorkOrderMediaCategory.Extra
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = new WorkOrderMediaService(context, new WorkOrderDetailDataService(context));
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.DeleteMediaAsync(1, 10, "actor-1"));
|
|
|
|
Assert.Equal("ReadOnly", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddMedia_CompletionCategory_ThrowsUseCompletionDocEndpoint()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = new WorkOrderMediaService(context, new WorkOrderDetailDataService(context));
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.AddMediaAsync(1, WorkOrderMediaCategory.Completion, "https://example.com/doc.pdf", "actor-1"));
|
|
|
|
Assert.Equal("UseCompletionDocEndpoint", ex.Code);
|
|
}
|
|
}
|