using AtomUI.Controls; using Avalonia.Platform.Storage; using Shouldly; using Xunit; namespace AtomUI.Desktop.Controls.Tests.Upload; public class UploadFileAdmissionServiceTests { [Fact] public async Task Null_Or_Empty_File_Types_Accept_The_File() { var file = File("readme.txt"); var nullResult = await EvaluateAsync(file, null); var emptyResult = await EvaluateAsync(file, []); nullResult.IsAccepted.ShouldBeTrue(); emptyResult.IsAccepted.ShouldBeTrue(); } [Theory] [InlineData("image.PNG", "*.png")] [InlineData("archive.tar.gz", "*.tar.gz")] [InlineData("readme", "*.*")] public async Task Patterns_Match_File_Names_Case_Insensitively(string fileName, string pattern) { var result = await EvaluateAsync( File(fileName), [new FilePickerFileType("allowed") { Patterns = [pattern] }]); result.IsAccepted.ShouldBeTrue(); } [Fact] public async Task Any_File_Type_Or_Pattern_Can_Accept_The_File() { var result = await EvaluateAsync( File("image.png"), [ new FilePickerFileType("text") { Patterns = ["*.txt"] }, new FilePickerFileType("images") { Patterns = ["*.jpg", "*.png"] } ]); result.IsAccepted.ShouldBeTrue(); } [Theory] [InlineData("image/png", "image/png")] [InlineData("image/png", "image/*")] [InlineData("APPLICATION/PDF", "application/pdf")] public async Task Reliable_Content_Type_Supports_Exact_And_Wildcard_Mime_Matches( string contentType, string allowedMime) { var result = await EvaluateAsync( File("content.bin", contentType), [new FilePickerFileType("mime") { MimeTypes = [allowedMime] }]); result.IsAccepted.ShouldBeTrue(); } [Fact] public async Task Missing_Content_Type_Does_Not_Match_Mime_Only_Rules() { var result = await EvaluateAsync( File("content.bin"), [new FilePickerFileType("mime") { MimeTypes = ["application/octet-stream"] }]); result.IsAccepted.ShouldBeFalse(); result.Rejection.ShouldNotBeNull(); result.Rejection.Reason.ShouldBe(UploadRejectionReason.FileTypeNotAllowed); } [Fact] public async Task Apple_Uti_Only_Rules_Do_Not_Match_Without_Reliable_Uti_Metadata() { var result = await EvaluateAsync( File("image.png"), [new FilePickerFileType("uti") { AppleUniformTypeIdentifiers = ["public.png"] }]); result.IsAccepted.ShouldBeFalse(); } [Fact] public async Task Admission_Policy_Rejection_Preserves_Code_And_Message() { var policy = new DelegateAdmissionPolicy((_, _) => ValueTask.FromResult(UploadAdmissionDecision.Reject("virus", "Rejected by scanner."))); var result = await UploadFileAdmissionService.EvaluateAsync( Guid.NewGuid(), UploadInputSource.DragDrop, File("unsafe.exe"), [], policy, TestContext.Current.CancellationToken); result.IsAccepted.ShouldBeFalse(); result.Rejection.ShouldNotBeNull(); result.Rejection.Reason.ShouldBe(UploadRejectionReason.AdmissionRejected); result.Rejection.RejectionCode.ShouldBe("virus"); result.Rejection.Message.ShouldBe("Rejected by scanner."); } [Fact] public async Task Admission_Policy_Receives_Batch_Source_And_File() { UploadAdmissionContext? observed = null; var policy = new DelegateAdmissionPolicy((context, _) => { observed = context; return ValueTask.FromResult(UploadAdmissionDecision.Accept()); }); var batchId = Guid.NewGuid(); var file = File("safe.txt"); var result = await UploadFileAdmissionService.EvaluateAsync( batchId, UploadInputSource.Programmatic, file, [], policy, TestContext.Current.CancellationToken); result.IsAccepted.ShouldBeTrue(); observed.ShouldNotBeNull(); observed.BatchId.ShouldBe(batchId); observed.Source.ShouldBe(UploadInputSource.Programmatic); observed.File.ShouldBeSameAs(file); } [Fact] public async Task Admission_Policy_Exception_Becomes_A_Per_Item_Rejection() { var exception = new InvalidOperationException("scanner unavailable"); var policy = new DelegateAdmissionPolicy((_, _) => ValueTask.FromException(exception)); var result = await UploadFileAdmissionService.EvaluateAsync( Guid.NewGuid(), UploadInputSource.FilePicker, File("file.txt"), [], policy, TestContext.Current.CancellationToken); result.IsAccepted.ShouldBeFalse(); result.Rejection.ShouldNotBeNull(); result.Rejection.Reason.ShouldBe(UploadRejectionReason.AdmissionPolicyFailed); result.Rejection.RejectionCode.ShouldBeNull(); result.Rejection.Message.ShouldBeNull(); } [Fact] public async Task Admission_Policy_Cancellation_Propagates() { var policy = new DelegateAdmissionPolicy((_, cancellationToken) => ValueTask.FromCanceled(cancellationToken)); using var cancellationTokenSource = new CancellationTokenSource(); await cancellationTokenSource.CancelAsync(); await Should.ThrowAsync(async () => await UploadFileAdmissionService.EvaluateAsync( Guid.NewGuid(), UploadInputSource.DirectoryPicker, File("file.txt"), [], policy, cancellationTokenSource.Token)); } private static Task EvaluateAsync( UploadFileInfo file, IReadOnlyList? allowedFileTypes) { var rules = allowedFileTypes?.Select(fileType => new UploadFileTypeRule( fileType.Patterns?.ToArray() ?? [], fileType.MimeTypes?.ToArray() ?? [])).ToArray() ?? []; return UploadFileAdmissionService.EvaluateAsync( Guid.NewGuid(), UploadInputSource.Programmatic, file, rules, null, TestContext.Current.CancellationToken).AsTask(); } private static UploadFileInfo File(string name, string? contentType = null) { return new UploadFileInfo( name, new UploadTestFileSource(), new Uri($"file:///tmp/{name}"), 3, contentType); } private sealed class DelegateAdmissionPolicy : IUploadAdmissionPolicy { private readonly Func> _evaluate; internal DelegateAdmissionPolicy( Func> evaluate) { _evaluate = evaluate; } public ValueTask EvaluateAsync( UploadAdmissionContext context, CancellationToken cancellationToken = default) { return _evaluate(context, cancellationToken); } } }