From d073a503d1b05801ec18d7def9b0f7d360143dad Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 31 Jul 2026 11:58:22 -0300 Subject: [PATCH] feat(work-orders): board completedDate + media categorize contract Expose completedDate on PATCH /workorders/{id}/board so CompDoc can leave legacy EditWorkorder. Allow optional media category on upload, PATCH category afterward, and enforce JPG/PNG/MP4/MOV allowlist (SH-116). --- .../WorkOrderRouteContractTests.cs | 5 +- .../Controllers/WorkOrderMediaController.cs | 26 +++++- .../Helpers/WorkOrderBoardProjection.cs | 2 + .../Interfaces/WorkOrderBoardModels.cs | 1 + SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs | 1 + SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs | 1 - .../Helpers/WorkOrderBoardFieldNames.cs | 3 + .../Helpers/WorkOrderMediaFileRules.cs | 51 ++++++++++++ .../Implementation/WorkOrderBoardService.cs | 1 + .../WorkOrderBoardUpdateService.cs | 1 + .../Implementation/WorkOrderMediaService.cs | 83 +++++++++++++++++-- .../Interfaces/IWorkOrderMediaService.cs | 11 ++- .../WorkOrderBoardUpdateServiceTests.cs | 55 ++++++++++++ .../WorkOrderPhase6Tests.cs | 52 ++++++++++++ 14 files changed, 280 insertions(+), 13 deletions(-) create mode 100644 SeaHaven.Services/Helpers/WorkOrderMediaFileRules.cs diff --git a/Api.SeaHavenIndustries.Tests/WorkOrderRouteContractTests.cs b/Api.SeaHavenIndustries.Tests/WorkOrderRouteContractTests.cs index dd1d0c5..5870f16 100644 --- a/Api.SeaHavenIndustries.Tests/WorkOrderRouteContractTests.cs +++ b/Api.SeaHavenIndustries.Tests/WorkOrderRouteContractTests.cs @@ -38,8 +38,8 @@ public class WorkOrderRouteContractTests /// Baseline public endpoint set (verb + action-relative route) that the original single /// WorkOrderController exposed, plus the author-only board-comment edit endpoint (SH-122). /// Every action is reachable under both api/WorkOrder and api/workorders; that base-route - /// duplication is collapsed here, so this is the distinct action-relative contract. 46 routes - /// come from 44 actions (Editworkorder and GetWorkorderById each bind two routes). + /// duplication is collapsed here, so this is the distinct action-relative contract. 47 routes + /// come from 45 actions (Editworkorder and GetWorkorderById each bind two routes). /// private static readonly HashSet ExpectedWorkOrderEndpoints = new(StringComparer.Ordinal) { @@ -68,6 +68,7 @@ public class WorkOrderRouteContractTests "GET {id:int}/media", "PATCH {id:int}/board", "PATCH {id:int}/comments/{commentId:int}", + "PATCH {id:int}/media/{mediaId:int}", "POST AddChecklistItem", "POST AddComment", "POST AddCommentJson", diff --git a/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs b/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs index 2664d65..73e5a69 100644 --- a/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs +++ b/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; +using SeaHaven.Services.Helpers; using SeaHaven.Services.Interfaces; using System.Security.Claims; @@ -40,7 +41,7 @@ namespace Api.SeaHavenIndustries.Controllers [RequestSizeLimit(30_000_000)] public async Task AddMedia( int id, - [FromForm] WorkOrderMediaCategory category, + [FromForm] WorkOrderMediaCategory? category, [FromForm] IFormFile file) { if (file == null || file.Length == 0) @@ -48,6 +49,7 @@ namespace Api.SeaHavenIndustries.Controllers try { + WorkOrderMediaFileRules.EnsureAllowed(file); var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier); var fileUrl = await _fileStorage.SaveFileAsync(file); var media = await _workOrderMediaService.AddMediaAsync(id, category, fileUrl, actorId); @@ -63,6 +65,28 @@ namespace Api.SeaHavenIndustries.Controllers } } + [HttpPatch("{id:int}/media/{mediaId:int}")] + public async Task UpdateMediaCategory( + int id, + int mediaId, + [FromForm] WorkOrderMediaCategory category) + { + try + { + var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var media = await _workOrderMediaService.UpdateMediaCategoryAsync(id, mediaId, category, actorId); + return Ok(media); + } + catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound") + { + return NotFound(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message }); + } + catch (WorkOrderBoardValidationException ex) + { + return UnprocessableEntity(new WorkOrderBoardValidationErrorDto { Code = ex.Code, Message = ex.Message }); + } + } + [HttpDelete("{id:int}/media/{mediaId:int}")] public async Task DeleteMedia(int id, int mediaId) { diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs index 80d18df..190bb8f 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs @@ -66,6 +66,7 @@ namespace SeaHaven.DataServices.Helpers w.ServiceNotes, w.ExtraServices, w.DocStatus, + w.CompletedDate, w.FlagColor, w.PrimaryDispatchId, w.RowVersion, @@ -116,6 +117,7 @@ namespace SeaHaven.DataServices.Helpers w.ServiceNotes, w.ExtraServices, w.DocStatus, + w.CompletedDate, w.FlagColor, w.PrimaryDispatchId, w.RowVersion, diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs index 51ca0d7..71ad944 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs @@ -46,6 +46,7 @@ namespace SeaHaven.DataServices.Interfaces string? ServiceNotes, string? ExtraServices, DocStatus? DocStatus, + DateTime? CompletedDate, string? FlagColor, int? PrimaryDispatchId, byte[]? RowVersion, diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs index adba573..21e4fff 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs @@ -56,6 +56,7 @@ namespace SeaHaven.Services.DTOs public string? ServiceNotes { get; set; } public List? ExtraServices { get; set; } public DocStatus? DocStatus { get; set; } + public DateTime? CompletedDate { get; set; } /// Board flag color (#RRGGBB). Null = no flag. Distinct from Color (dispatcher avatar). public string? FlagColor { get; set; } public int? PrimaryDispatchId { get; set; } diff --git a/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs index 105e112..8f2c692 100644 --- a/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs @@ -19,7 +19,6 @@ namespace SeaHaven.Services.DTOs public string? SubTrade { get; set; } public DateOnly? OriginalWeek { get; set; } public DateOnly? OriginalDate { get; set; } - public DateTime? CompletedDate { get; set; } public string? VendorNotes { get; set; } } diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs b/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs index a76a7fe..ce7b7ae 100644 --- a/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs +++ b/SeaHaven.Services/Helpers/WorkOrderBoardFieldNames.cs @@ -15,6 +15,7 @@ namespace SeaHaven.Services.Helpers public const string ApptDate = "apptDate"; public const string ApptTime = "apptTime"; public const string DocStatus = "docStatus"; + public const string CompletedDate = "completedDate"; public const string Pm = "pm"; public const string ServiceNotes = "serviceNotes"; public const string ExtraServices = "extraServices"; @@ -43,6 +44,7 @@ namespace SeaHaven.Services.Helpers ApptDate, ApptTime, DocStatus, + CompletedDate, Pm, ServiceNotes, ExtraServices, @@ -83,6 +85,7 @@ namespace SeaHaven.Services.Helpers ApptDate => "ApptDate", ApptTime => "ApptTime", DocStatus => "DocStatus", + CompletedDate => "CompletedDate", Pm => "Trade", ServiceNotes => "ServiceNotes", ExtraServices => "ExtraServices", diff --git a/SeaHaven.Services/Helpers/WorkOrderMediaFileRules.cs b/SeaHaven.Services/Helpers/WorkOrderMediaFileRules.cs new file mode 100644 index 0000000..e500edc --- /dev/null +++ b/SeaHaven.Services/Helpers/WorkOrderMediaFileRules.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Http; + +namespace SeaHaven.Services.Helpers +{ + /// SH-116 media type allowlist for board work-order uploads. + public static class WorkOrderMediaFileRules + { + private static readonly HashSet AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase) + { + "image/jpeg", + "image/png", + "video/mp4", + "video/quicktime" + }; + + private static readonly HashSet AllowedExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".jpg", + ".jpeg", + ".png", + ".mp4", + ".mov" + }; + + public static bool IsAllowed(IFormFile file) + { + if (file == null) + return false; + + var contentTypeOk = !string.IsNullOrWhiteSpace(file.ContentType) + && AllowedContentTypes.Contains(file.ContentType.Trim()); + + var extension = Path.GetExtension(file.FileName ?? ""); + var extensionOk = !string.IsNullOrWhiteSpace(extension) + && AllowedExtensions.Contains(extension); + + // Accept when either MIME or extension matches the advertised contract (JPG/PNG/MP4/MOV). + return contentTypeOk || extensionOk; + } + + public static void EnsureAllowed(IFormFile file) + { + if (!IsAllowed(file)) + { + throw new Exceptions.WorkOrderBoardValidationException( + "UnsupportedMediaType", + "Supported media types are JPG, PNG, MP4, and MOV."); + } + } + } +} diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index f6fdb1f..aa4b4dd 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -113,6 +113,7 @@ namespace SeaHaven.Services.Implementation ServiceNotes = row.ServiceNotes, ExtraServices = ParseExtraServices(row.ExtraServices), DocStatus = row.DocStatus, + CompletedDate = row.CompletedDate, FlagColor = row.FlagColor, PrimaryDispatchId = row.PrimaryDispatchId, RowVersion = row.RowVersion, diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs index bdab70a..3e8c108 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs @@ -153,6 +153,7 @@ namespace SeaHaven.Services.Implementation WorkOrderBoardFieldNames.ApptDate => new List { ApplyApptDate(dispatch!, value, auditField) }, WorkOrderBoardFieldNames.ApptTime => ApplyApptTime(workOrder, dispatch!, value), WorkOrderBoardFieldNames.DocStatus => new List { ApplyDocStatus(workOrder, value, auditField) }, + WorkOrderBoardFieldNames.CompletedDate => new List { ApplyDateField(value, auditField, v => workOrder.CompletedDate = v, () => workOrder.CompletedDate) }, WorkOrderBoardFieldNames.Pm => new List { ApplyStringField(value, auditField, v => workOrder.Trade = v, () => workOrder.Trade) }, WorkOrderBoardFieldNames.ServiceNotes => new List { ApplyServiceNotes(workOrder, value, auditField) }, WorkOrderBoardFieldNames.ExtraServices => new List { ApplyExtraServices(workOrder, value, auditField) }, diff --git a/SeaHaven.Services/Implementation/WorkOrderMediaService.cs b/SeaHaven.Services/Implementation/WorkOrderMediaService.cs index 81c2614..098d5d7 100644 --- a/SeaHaven.Services/Implementation/WorkOrderMediaService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderMediaService.cs @@ -34,10 +34,12 @@ namespace SeaHaven.Services.Implementation public async Task AddMediaAsync( int workOrderId, - WorkOrderMediaCategory category, + WorkOrderMediaCategory? category, string fileUrl, string? actorId) { + var resolvedCategory = category ?? WorkOrderMediaCategory.Extra; + if (!await _detailData.ExistsAsync(workOrderId)) throw new WorkOrderBoardValidationException("NotFound", "Work order not found."); @@ -48,34 +50,34 @@ namespace SeaHaven.Services.Implementation if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus)) throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status."); - if (category == WorkOrderMediaCategory.Completion) + if (resolvedCategory == WorkOrderMediaCategory.Completion) { throw new WorkOrderBoardValidationException( "UseCompletionDocEndpoint", "Completion documents must be uploaded via POST /api/workorders/{id}/completion-doc."); } - if (category == WorkOrderMediaCategory.Before) + if (resolvedCategory == WorkOrderMediaCategory.Before) { workOrder.BeforPhotoAttachment = fileUrl; await _mediaData.SaveAsync(CancellationToken.None); return new WorkOrderMediaFileDto { Id = -1, - Category = category, + Category = resolvedCategory, Url = fileUrl, IsLegacy = true }; } - if (category == WorkOrderMediaCategory.After) + if (resolvedCategory == WorkOrderMediaCategory.After) { workOrder.AfterPhotoAttachment = fileUrl; await _mediaData.SaveAsync(CancellationToken.None); return new WorkOrderMediaFileDto { Id = -2, - Category = category, + Category = resolvedCategory, Url = fileUrl, IsLegacy = true }; @@ -85,7 +87,7 @@ namespace SeaHaven.Services.Implementation { WorkorderId = workOrderId, Attachments = fileUrl, - Category = category, + Category = category.HasValue ? resolvedCategory : null, CreatedDate = DateTime.UtcNow, createdby = actorId }; @@ -96,13 +98,78 @@ namespace SeaHaven.Services.Implementation return new WorkOrderMediaFileDto { Id = attachment.Id, - Category = category, + Category = attachment.Category ?? WorkOrderMediaCategory.Extra, Url = fileUrl, UploadedAt = attachment.CreatedDate?.ToUniversalTime().ToString("o"), IsLegacy = false }; } + public async Task UpdateMediaCategoryAsync( + int workOrderId, + int mediaId, + WorkOrderMediaCategory category, + string? actorId) + { + if (mediaId <= 0) + throw new WorkOrderBoardValidationException("InvalidMedia", "Legacy media cannot be categorized via this endpoint."); + + if (category == WorkOrderMediaCategory.Completion) + { + throw new WorkOrderBoardValidationException( + "UseCompletionDocEndpoint", + "Completion documents must be uploaded via POST /api/workorders/{id}/completion-doc."); + } + + if (!await _detailData.ExistsAsync(workOrderId)) + throw new WorkOrderBoardValidationException("NotFound", "Work order not found."); + + var workOrder = await _mediaData.GetTrackedWorkOrderAsync(workOrderId, CancellationToken.None); + 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."); + + var attachment = await _mediaData.GetTrackedAttachmentAsync(mediaId, workOrderId, CancellationToken.None); + if (attachment == null) + throw new WorkOrderBoardValidationException("NotFound", "Media not found."); + + if (category == WorkOrderMediaCategory.Before || category == WorkOrderMediaCategory.After) + { + var url = attachment.Attachments ?? ""; + if (category == WorkOrderMediaCategory.Before) + workOrder.BeforPhotoAttachment = url; + else + workOrder.AfterPhotoAttachment = url; + + attachment.IsDeleted = true; + attachment.DeletionTime = DateTime.UtcNow; + attachment.DeleterUserId = actorId; + await _mediaData.SaveAsync(CancellationToken.None); + + return new WorkOrderMediaFileDto + { + Id = category == WorkOrderMediaCategory.Before ? -1 : -2, + Category = category, + Url = url, + IsLegacy = true + }; + } + + attachment.Category = category; + await _mediaData.SaveAsync(CancellationToken.None); + + return new WorkOrderMediaFileDto + { + Id = attachment.Id, + Category = attachment.Category ?? WorkOrderMediaCategory.Extra, + Url = attachment.Attachments ?? "", + UploadedAt = attachment.CreatedDate?.ToUniversalTime().ToString("o"), + IsLegacy = false + }; + } + public async Task DeleteMediaAsync(int workOrderId, int mediaId, string? actorId) { if (mediaId <= 0) diff --git a/SeaHaven.Services/Interfaces/IWorkOrderMediaService.cs b/SeaHaven.Services/Interfaces/IWorkOrderMediaService.cs index 015cc83..a40b422 100644 --- a/SeaHaven.Services/Interfaces/IWorkOrderMediaService.cs +++ b/SeaHaven.Services/Interfaces/IWorkOrderMediaService.cs @@ -6,7 +6,16 @@ namespace SeaHaven.Services.Interfaces public interface IWorkOrderMediaService { Task?> GetMediaAsync(int workOrderId); - Task AddMediaAsync(int workOrderId, WorkOrderMediaCategory category, string fileUrl, string? actorId); + Task AddMediaAsync( + int workOrderId, + WorkOrderMediaCategory? category, + string fileUrl, + string? actorId); + Task UpdateMediaCategoryAsync( + int workOrderId, + int mediaId, + WorkOrderMediaCategory category, + string? actorId); Task DeleteMediaAsync(int workOrderId, int mediaId, string? actorId); } } diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs index 883aeae..b775ee6 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs @@ -798,4 +798,59 @@ public class WorkOrderBoardUpdateServiceTests Assert.Equal("VendorNotFound", ex.Code); } + + [Fact] + public async Task PatchField_UpdatesCompletedDate() + { + 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); + context.WorkOrderContacts.Add(new WorkOrderContacts { Id = 1, WorkorderId = 1, ContactId = 50 }); + context.workOrderCategories.Add(new WorkOrderCategories { Id = 1, WorkorderId = 1, CategoryId = 60 }); + await context.SaveChangesAsync(); + + var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.CompletedDate, + Value = "2026-07-20", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + Assert.Equal(new DateTime(2026, 7, 20), result.CompletedDate); + Assert.Equal(new DateTime(2026, 7, 20), context.workOrders.Single().CompletedDate); + Assert.Equal(1, await context.WorkOrderContacts.CountAsync()); + Assert.Equal(1, await context.workOrderCategories.CountAsync()); + Assert.Equal(50, context.WorkOrderContacts.Single().ContactId); + Assert.Equal(60, context.workOrderCategories.Single().CategoryId); + } + + [Fact] + public async Task PatchField_ClearsCompletedDate_WhenValueEmpty() + { + var (context, service) = CreateSut(); + var wo = new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Scheduled, + CompletedDate = new DateTime(2026, 7, 1), + 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.CompletedDate, + Value = "", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + Assert.Null(result.CompletedDate); + Assert.Null(context.workOrders.Single().CompletedDate); + } } diff --git a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs index e30e651..8029516 100644 --- a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs @@ -650,4 +650,56 @@ public class WorkOrderMediaServiceTests Assert.Equal("UseCompletionDocEndpoint", ex.Code); } + + [Fact] + public async Task AddMedia_WithoutCategory_PersistsAsUncategorizedExtra() + { + var options = new DbContextOptionsBuilder() + .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(new WorkOrderMediaDataService(context), new WorkOrderDetailDataService(context)); + var media = await service.AddMediaAsync(1, null, "https://example.com/photo.jpg", "actor-1"); + + Assert.True(media.Id > 0); + Assert.Equal(WorkOrderMediaCategory.Extra, media.Category); + Assert.Null(context.workOrderAttachments.Single().Category); + } + + [Fact] + public async Task UpdateMediaCategory_SetsBeforeFromExtraAttachment() + { + 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.workOrderAttachments.Add(new WorkOrderAttachments + { + Id = 10, + WorkorderId = 1, + Attachments = "https://example.com/photo.jpg", + Category = null + }); + await context.SaveChangesAsync(); + + var service = new WorkOrderMediaService(new WorkOrderMediaDataService(context), new WorkOrderDetailDataService(context)); + var media = await service.UpdateMediaCategoryAsync(1, 10, WorkOrderMediaCategory.Before, "actor-1"); + + Assert.Equal(-1, media.Id); + Assert.Equal(WorkOrderMediaCategory.Before, media.Category); + Assert.Equal("https://example.com/photo.jpg", context.workOrders.Single().BeforPhotoAttachment); + Assert.True(context.workOrderAttachments.Single().IsDeleted); + } }