mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-29 07:16:33 +08:00
216 lines
7.3 KiB
C#
216 lines
7.3 KiB
C#
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<UploadAdmissionDecision>(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<UploadAdmissionDecision>(cancellationToken));
|
|
using var cancellationTokenSource = new CancellationTokenSource();
|
|
await cancellationTokenSource.CancelAsync();
|
|
|
|
await Should.ThrowAsync<OperationCanceledException>(async () =>
|
|
await UploadFileAdmissionService.EvaluateAsync(
|
|
Guid.NewGuid(),
|
|
UploadInputSource.DirectoryPicker,
|
|
File("file.txt"),
|
|
[],
|
|
policy,
|
|
cancellationTokenSource.Token));
|
|
}
|
|
|
|
private static Task<UploadFileAdmissionResult> EvaluateAsync(
|
|
UploadFileInfo file,
|
|
IReadOnlyList<FilePickerFileType>? 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<UploadAdmissionContext, CancellationToken, ValueTask<UploadAdmissionDecision>> _evaluate;
|
|
|
|
internal DelegateAdmissionPolicy(
|
|
Func<UploadAdmissionContext, CancellationToken, ValueTask<UploadAdmissionDecision>> evaluate)
|
|
{
|
|
_evaluate = evaluate;
|
|
}
|
|
|
|
public ValueTask<UploadAdmissionDecision> EvaluateAsync(
|
|
UploadAdmissionContext context,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return _evaluate(context, cancellationToken);
|
|
}
|
|
}
|
|
}
|