mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
Merge branch 'dev' into fix/ab/sh-261-vendor-list-contact
This commit is contained in:
commit
5343931a62
11 changed files with 482 additions and 29 deletions
|
|
@ -66,7 +66,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
WorkOrderMediaFileRules.EnsureAllowed(file);
|
||||
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
await _workOrderMediaService.EnsureCanMutateMediaAsync(id, User, actorId, cancellationToken);
|
||||
await _workOrderMediaService.EnsureCanMutateMediaAsync(id, User, actorId, cancellationToken, category);
|
||||
|
||||
fileUrl = await _fileStorage.SaveFileAsync(file);
|
||||
var media = await _workOrderMediaService.AddMediaAsync(id, category, fileUrl, User, actorId, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,33 @@ namespace SeaHaven.Services.Helpers
|
|||
|
||||
public static bool IsReadOnly(LifecycleStatus? status) => ReadOnlyStatuses.Contains(status);
|
||||
|
||||
public static bool IsFullyLocked(LifecycleStatus? status)
|
||||
=> status == LifecycleStatus.Canceled;
|
||||
|
||||
public static bool IsCoreLocked(LifecycleStatus? status)
|
||||
=> status == LifecycleStatus.Completed;
|
||||
|
||||
public static bool CanPatchBoardField(LifecycleStatus? status, string field)
|
||||
{
|
||||
if (IsFullyLocked(status))
|
||||
return false;
|
||||
if (!IsCoreLocked(status))
|
||||
return true;
|
||||
return field.Equals(WorkOrderBoardFieldNames.FlagColor, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool CanMutateComments(LifecycleStatus? status)
|
||||
=> !IsFullyLocked(status);
|
||||
|
||||
public static bool CanMutateExtraMedia(LifecycleStatus? status, WorkOrderMediaCategory? category)
|
||||
{
|
||||
if (IsFullyLocked(status))
|
||||
return false;
|
||||
if (!IsCoreLocked(status))
|
||||
return true;
|
||||
return (category ?? WorkOrderMediaCategory.Extra) == WorkOrderMediaCategory.Extra;
|
||||
}
|
||||
|
||||
public static bool ShouldBlockStatusChangeWhenPastDue(string field, bool isPastDue)
|
||||
=> field.Equals(WorkOrderBoardFieldNames.LifecycleStatus, StringComparison.OrdinalIgnoreCase) && isPastDue;
|
||||
|
||||
|
|
|
|||
|
|
@ -59,8 +59,13 @@ namespace SeaHaven.Services.Implementation
|
|||
throw new WorkOrderBoardConcurrencyException(staleState);
|
||||
}
|
||||
|
||||
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
|
||||
throw new WorkOrderBoardValidationException("CanceledReadOnly", "Work order is read-only in its current status.");
|
||||
if (!WorkOrderBoardMutationRules.CanPatchBoardField(workOrder.LifecycleStatus, canonicalField))
|
||||
{
|
||||
var code = WorkOrderBoardMutationRules.IsFullyLocked(workOrder.LifecycleStatus)
|
||||
? "CanceledReadOnly"
|
||||
: "ReadOnly";
|
||||
throw new WorkOrderBoardValidationException(code, "Work order is read-only in its current status.");
|
||||
}
|
||||
|
||||
var isPastDue = WorkOrderDerivedFields.IsPastDue(workOrder.ScheduledDate, workOrder.LifecycleStatus);
|
||||
if (WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(canonicalField, isPastDue))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Security.Claims;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Exceptions;
|
||||
|
|
@ -48,9 +49,13 @@ namespace SeaHaven.Services.Implementation
|
|||
string? documentUrl = null)
|
||||
{
|
||||
var accountId = _accountResolver.ResolveAccountFilter(user);
|
||||
if (!await _detailData.ExistsAsync(workOrderId, CancellationToken.None, accountId))
|
||||
var workOrder = await _detailData.GetWorkOrderForMediaAsync(
|
||||
workOrderId, CancellationToken.None, accountId);
|
||||
if (workOrder == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
EnsureCommentMutationAllowed(workOrder.LifecycleStatus);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Text))
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", "Comment text is required.");
|
||||
|
||||
|
|
@ -83,8 +88,7 @@ namespace SeaHaven.Services.Implementation
|
|||
if (workOrder == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
|
||||
|
||||
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
EnsureCommentMutationAllowed(workOrder.LifecycleStatus);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Text))
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", "Comment text is required.");
|
||||
|
|
@ -111,6 +115,12 @@ namespace SeaHaven.Services.Implementation
|
|||
return WorkOrderCommentProjection.ToDto(comment, authorNames);
|
||||
}
|
||||
|
||||
private static void EnsureCommentMutationAllowed(LifecycleStatus? status)
|
||||
{
|
||||
if (!WorkOrderBoardMutationRules.CanMutateComments(status))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
}
|
||||
|
||||
private static bool IsBoardEditableComment(Comments comment)
|
||||
=> string.Equals(comment.CommentType, "General", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(comment.RecordType, "WorkOrder", StringComparison.OrdinalIgnoreCase);
|
||||
|
|
|
|||
|
|
@ -52,11 +52,13 @@ namespace SeaHaven.Services.Implementation
|
|||
int workOrderId,
|
||||
ClaimsPrincipal user,
|
||||
string? actorId,
|
||||
CancellationToken cancellationToken = default)
|
||||
CancellationToken cancellationToken = default,
|
||||
WorkOrderMediaCategory? category = null)
|
||||
{
|
||||
WorkOrderMediaAuthorization.EnsureCanMutate(user, actorId);
|
||||
// AsNoTracking pre-check so the subsequent AddMediaAsync load is not stale-cached.
|
||||
await GetMutableWorkOrderForAuthAsync(workOrderId, user, actorId!, cancellationToken);
|
||||
var workOrder = await GetMutableWorkOrderForAuthAsync(workOrderId, user, actorId!, cancellationToken);
|
||||
EnsureMediaMutationAllowed(workOrder.LifecycleStatus, category ?? WorkOrderMediaCategory.Extra);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderMediaFileDto> AddMediaAsync(
|
||||
|
|
@ -71,6 +73,7 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
var resolvedCategory = category ?? WorkOrderMediaCategory.Extra;
|
||||
var workOrder = await GetMutableWorkOrderAsync(workOrderId, user, actorId!, cancellationToken);
|
||||
EnsureMediaMutationAllowed(workOrder.LifecycleStatus, resolvedCategory);
|
||||
|
||||
if (resolvedCategory == WorkOrderMediaCategory.Completion)
|
||||
{
|
||||
|
|
@ -167,7 +170,10 @@ namespace SeaHaven.Services.Implementation
|
|||
if (attachment == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Media not found.");
|
||||
|
||||
var priorCategory = (attachment.Category ?? WorkOrderMediaCategory.Extra).ToString();
|
||||
var currentCategory = attachment.Category ?? WorkOrderMediaCategory.Extra;
|
||||
EnsureMediaMutationAllowed(workOrder.LifecycleStatus, currentCategory);
|
||||
EnsureMediaMutationAllowed(workOrder.LifecycleStatus, category);
|
||||
var priorCategory = currentCategory.ToString();
|
||||
|
||||
if (category == WorkOrderMediaCategory.Before || category == WorkOrderMediaCategory.After)
|
||||
{
|
||||
|
|
@ -238,7 +244,9 @@ namespace SeaHaven.Services.Implementation
|
|||
if (attachment == null)
|
||||
throw new WorkOrderBoardValidationException("NotFound", "Media not found.");
|
||||
|
||||
var priorCategory = (attachment.Category ?? WorkOrderMediaCategory.Extra).ToString();
|
||||
var currentCategory = attachment.Category ?? WorkOrderMediaCategory.Extra;
|
||||
EnsureMediaMutationAllowed(workOrder.LifecycleStatus, currentCategory);
|
||||
var priorCategory = currentCategory.ToString();
|
||||
attachment.IsDeleted = true;
|
||||
attachment.DeletionTime = DateTime.UtcNow;
|
||||
attachment.DeleterUserId = actorId;
|
||||
|
|
@ -266,9 +274,6 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
WorkOrderMediaAuthorization.EnsureWorkOrderInCallerScope(user, actorId, workOrder);
|
||||
|
||||
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
|
||||
return workOrder;
|
||||
}
|
||||
|
||||
|
|
@ -286,12 +291,17 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
WorkOrderMediaAuthorization.EnsureWorkOrderInCallerScope(user, actorId, workOrder);
|
||||
|
||||
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
|
||||
return workOrder;
|
||||
}
|
||||
|
||||
private static void EnsureMediaMutationAllowed(
|
||||
LifecycleStatus? status,
|
||||
WorkOrderMediaCategory category)
|
||||
{
|
||||
if (!WorkOrderBoardMutationRules.CanMutateExtraMedia(status, category))
|
||||
throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Account filter for data queries. Null means org-wide (skip ApplyAccountScope).
|
||||
/// Call only after EnsureCan* has verified scope is not Missing.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ namespace SeaHaven.Services.Interfaces
|
|||
int workOrderId,
|
||||
ClaimsPrincipal user,
|
||||
string? actorId,
|
||||
CancellationToken cancellationToken = default);
|
||||
CancellationToken cancellationToken = default,
|
||||
WorkOrderMediaCategory? category = null);
|
||||
|
||||
Task<WorkOrderMediaFileDto> AddMediaAsync(
|
||||
int workOrderId,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,43 @@ public class WorkOrderBoardMutationRulesTests
|
|||
scheduleWeekOnly: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPatchBoardField_AllowsOnlyFlagColorWhenCompleted()
|
||||
{
|
||||
Assert.True(WorkOrderBoardMutationRules.CanPatchBoardField(
|
||||
LifecycleStatus.Completed, WorkOrderBoardFieldNames.FlagColor));
|
||||
Assert.False(WorkOrderBoardMutationRules.CanPatchBoardField(
|
||||
LifecycleStatus.Completed, WorkOrderBoardFieldNames.LifecycleStatus));
|
||||
Assert.False(WorkOrderBoardMutationRules.CanPatchBoardField(
|
||||
LifecycleStatus.Completed, WorkOrderBoardFieldNames.SiteCode));
|
||||
Assert.False(WorkOrderBoardMutationRules.CanPatchBoardField(
|
||||
LifecycleStatus.Canceled, WorkOrderBoardFieldNames.FlagColor));
|
||||
Assert.True(WorkOrderBoardMutationRules.CanPatchBoardField(
|
||||
LifecycleStatus.Scheduled, WorkOrderBoardFieldNames.AssignTo));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMutateComments_FalseOnlyWhenCanceled()
|
||||
{
|
||||
Assert.True(WorkOrderBoardMutationRules.CanMutateComments(LifecycleStatus.Completed));
|
||||
Assert.True(WorkOrderBoardMutationRules.CanMutateComments(LifecycleStatus.Scheduled));
|
||||
Assert.False(WorkOrderBoardMutationRules.CanMutateComments(LifecycleStatus.Canceled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMutateExtraMedia_AllowsExtraOnCompleted()
|
||||
{
|
||||
Assert.True(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
||||
LifecycleStatus.Completed, WorkOrderMediaCategory.Extra));
|
||||
Assert.True(WorkOrderBoardMutationRules.CanMutateExtraMedia(LifecycleStatus.Completed, null));
|
||||
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
||||
LifecycleStatus.Completed, WorkOrderMediaCategory.Before));
|
||||
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
||||
LifecycleStatus.Completed, WorkOrderMediaCategory.After));
|
||||
Assert.False(WorkOrderBoardMutationRules.CanMutateExtraMedia(
|
||||
LifecycleStatus.Canceled, WorkOrderMediaCategory.Extra));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsReschedule_WhenPreviousDateExistsAndChanges()
|
||||
{
|
||||
|
|
|
|||
363
SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs
Normal file
363
SeaHavenIndustries.Tests/WorkOrderCompletedSelectiveLockTests.cs
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
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);
|
||||
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_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_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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1017,7 +1017,7 @@ public class WorkOrderMediaServiceTests
|
|||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.Completed,
|
||||
LifecycleStatus = LifecycleStatus.Canceled,
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
});
|
||||
context.workOrderAttachments.Add(new WorkOrderAttachments
|
||||
|
|
@ -1566,7 +1566,7 @@ public class WorkOrderMediaServiceTests
|
|||
await service.EnsureCanMutateMediaAsync(1, AuthenticatedUser(), "actor-1");
|
||||
|
||||
var wo = await context.workOrders.SingleAsync(w => w.Id == 1);
|
||||
wo.LifecycleStatus = LifecycleStatus.Completed;
|
||||
wo.LifecycleStatus = LifecycleStatus.Canceled;
|
||||
await context.SaveChangesAsync();
|
||||
context.ChangeTracker.Clear();
|
||||
|
||||
|
|
|
|||
16
infra/cdk/package-lock.json
generated
16
infra/cdk/package-lock.json
generated
|
|
@ -8,12 +8,12 @@
|
|||
"name": "shoc-backend-cdk",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"aws-cdk-lib": "2.265.0",
|
||||
"aws-cdk-lib": "2.266.0",
|
||||
"constructs": "10.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "26.2.0",
|
||||
"aws-cdk": "2.1137.0",
|
||||
"aws-cdk": "2.1138.0",
|
||||
"typescript": "7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -419,9 +419,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1137.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1137.0.tgz",
|
||||
"integrity": "sha512-SRy2df/5cqGvq8ncph1m93rWEh3RGbvP6uwElw+W0x135vZDzTpfbuq2L8oAHwK2yDt3RNS5sb7Phwb7xqQP2g==",
|
||||
"version": "2.1138.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1138.0.tgz",
|
||||
"integrity": "sha512-gZ5F8rmh+qc7ZNWsbaXYoV+p7jSYynRRg70s7FAn3VmzRaSkTE31ijpQHYroxCbDEtKSzgN63ORR/WuZmvXAwA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
|
@ -432,9 +432,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib": {
|
||||
"version": "2.265.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.265.0.tgz",
|
||||
"integrity": "sha512-Ur96d/ZW+gqAW4XtqKQp//D9O/jQvyRoXhAqoHGybpV03qjckGIQcLcGkLV68+jv03TX/rOXHCsa2IEFcCXa4w==",
|
||||
"version": "2.266.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.266.0.tgz",
|
||||
"integrity": "sha512-sBQU42pEc9ud3yeVU2En2euQRUhCg63eaJIPEVpBtE5aPhJjN3d9MkzQ9eYGLVXP3fnohUE54BiVOdUrkEDUbg==",
|
||||
"bundleDependencies": [
|
||||
"@aws/cloudformation-validate",
|
||||
"@balena/dockerignore",
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@
|
|||
"deploy": "npm run build && cdk deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-cdk-lib": "2.265.0",
|
||||
"aws-cdk-lib": "2.266.0",
|
||||
"constructs": "10.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "26.2.0",
|
||||
"aws-cdk": "2.1137.0",
|
||||
"aws-cdk": "2.1138.0",
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue