shoc-backend/SeaHavenIndustries.Tests/WorkOrderUploadFileRulesTests.cs
Arthur Bassi 719a5b05fd fix(work-orders): validate uploads before persist and store privately
Board completion/media uploads now check domain state first, use server-controlled filenames with an allowlist, store under App_Data, and serve via authenticated download endpoints. Also harden WeekRolled log arguments against CodeQL log-injection findings.
2026-07-21 09:25:33 -03:00

116 lines
4 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;
using Xunit;
namespace SeaHavenIndustries.Tests;
public class WorkOrderUploadFileRulesTests
{
[Theory]
[InlineData("report.pdf", ".pdf")]
[InlineData(@"C:\temp\photo.PNG", ".png")]
[InlineData("../evil.jpg", ".jpg")]
public void TryGetAllowedExtension_AcceptsSafeTypes(string name, string expectedExt)
{
var ok = WorkOrderUploadFileRules.TryGetAllowedExtension(name, out var ext, out var error);
Assert.True(ok, error);
Assert.Equal(expectedExt, ext);
}
[Theory]
[InlineData("payload.exe")]
[InlineData("noext")]
[InlineData("")]
[InlineData("..")]
public void TryGetAllowedExtension_RejectsUnsafeTypes(string name)
{
var ok = WorkOrderUploadFileRules.TryGetAllowedExtension(name, out _, out var error);
Assert.False(ok);
Assert.False(string.IsNullOrWhiteSpace(error));
}
[Fact]
public void BuildServerFileName_IsGuidPlusExtension_NoClientName()
{
var name = WorkOrderUploadFileRules.BuildServerFileName(".pdf");
Assert.StartsWith(name[..32], name);
Assert.EndsWith(".pdf", name);
Assert.DoesNotContain("..", name);
Assert.DoesNotContain("/", name);
Assert.DoesNotContain("\\", name);
}
}
public class WorkOrderUploadValidationOrderTests
{
private static (ApplicationDbContext Context, WorkOrderCompletionService Completion, WorkOrderMediaService Media) 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 completion = new WorkOrderCompletionService(context, templateData, detailData, audit);
var media = new WorkOrderMediaService(context, detailData);
return (context, completion, media);
}
[Fact]
public async Task EnsureCanUploadCompletionDoc_NotFound_ThrowsBeforeAnyPersist()
{
var (_, completion, _) = CreateSut();
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
completion.EnsureCanUploadCompletionDocAsync(
999,
new WorkOrderCompletionDocUploadDto
{
WorkOrderVersion = Convert.ToBase64String(new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 })
}));
Assert.Equal("NotFound", ex.Code);
}
[Fact]
public async Task EnsureCanUploadCompletionDoc_ReadOnly_Throws()
{
var (context, completion, _) = CreateSut();
context.workOrders.Add(new WorkOrder
{
Id = 1,
LifecycleStatus = LifecycleStatus.Completed,
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
});
await context.SaveChangesAsync();
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
completion.EnsureCanUploadCompletionDocAsync(
1,
new WorkOrderCompletionDocUploadDto
{
WorkOrderVersion = Convert.ToBase64String(new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 })
}));
Assert.Equal("ReadOnly", ex.Code);
}
[Fact]
public async Task EnsureCanAddMedia_NotFound_Throws()
{
var (_, _, media) = CreateSut();
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
media.EnsureCanAddMediaAsync(999, WorkOrderMediaCategory.Before));
Assert.Equal("NotFound", ex.Code);
}
}