120 lines
5.5 KiB
C#
120 lines
5.5 KiB
C#
using BuildFlow.Shared.Domain.Common;
|
||
using BuildFlow.Shared.Domain.Errors;
|
||
using BuildFlow.Shared.Infrastructure;
|
||
using File.Application.Common;
|
||
using File.Application.Features.Files.Commands;
|
||
using File.Application.Features.Files.Handlers;
|
||
using File.Domain.Entities.FileMeta;
|
||
using File.Domain.Exceptions;
|
||
using File.Infrastructure.Persistence;
|
||
using File.Infrastructure.Storage;
|
||
using FluentAssertions;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Moq;
|
||
using Xunit;
|
||
|
||
namespace BuildFlow.File.Tests;
|
||
|
||
/// <summary>
|
||
/// 直传上传处理器测试。验证缺陷 C 修复:直传完成后落 FileMetaEntity,使直传路径参与秒传。
|
||
/// </summary>
|
||
public class UploadFileCommandHandlerTests
|
||
{
|
||
private const string OwnerId = "user-001";
|
||
private const string Sha = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
|
||
|
||
/// <summary>测试用 bucket 解析器,固定返回 "files"。</summary>
|
||
private static IBucketResolver BucketResolver(string bucket = "files")
|
||
{
|
||
var mock = new Mock<IBucketResolver>();
|
||
mock.Setup(r => r.GetDefaultBucketAsync(It.IsAny<CancellationToken>())).ReturnsAsync(bucket);
|
||
return mock.Object;
|
||
}
|
||
|
||
private static (FileDbContext db, Mock<IS3StorageService> s3, Mock<ICurrentUserContext> user) CreateDeps(string dbName)
|
||
{
|
||
var user = new Mock<ICurrentUserContext>();
|
||
user.SetupGet(u => u.UserId).Returns(OwnerId);
|
||
user.SetupGet(u => u.IPAddress).Returns("127.0.0.1");
|
||
var s3 = new Mock<IS3StorageService>();
|
||
var options = new DbContextOptionsBuilder<FileDbContext>()
|
||
.UseInMemoryDatabase(dbName)
|
||
.AddInterceptors(new AuditInterceptor(user.Object))
|
||
.Options;
|
||
return (new FileDbContext(options), s3, user);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Upload_成功后落FileMeta_含ContentHash_使直传参与秒传()
|
||
{
|
||
// 缺陷C核心:直传必须落 FileMetaEntity,否则直传的文件永远无法被秒传
|
||
var (db, s3, user) = CreateDeps(nameof(Upload_成功后落FileMeta_含ContentHash_使直传参与秒传));
|
||
var data = new MemoryStream([1, 2, 3]);
|
||
var handler = new UploadFileCommandHandler(s3.Object, db, user.Object, BucketResolver(), new Mock<MediatR.IMediator>().Object);
|
||
|
||
var result = await handler.Handle(
|
||
new UploadFileCommand("obj-key", data, "application/octet-stream", 3, Sha),
|
||
CancellationToken.None);
|
||
|
||
// 服务端生成最终 key:{owner}/{独立段}/{净化文件名};目录前缀被剥掉
|
||
result.ObjectKey.Should().StartWith($"{OwnerId}/").And.EndWith("/obj-key");
|
||
s3.Verify(x => x.UploadFileAsync("files", result.ObjectKey, It.IsAny<Stream>(), "application/octet-stream", It.IsAny<CancellationToken>()), Times.Once);
|
||
|
||
// FileMetaEntity 应被创建,含 ContentHash,FolderId=null(根目录散落)
|
||
var meta = await db.Files.SingleAsync();
|
||
meta.Name.Should().Be("obj-key");
|
||
meta.S3Key.Should().Be(result.ObjectKey);
|
||
meta.S3Bucket.Should().Be("files");
|
||
meta.Size.Should().Be(3);
|
||
meta.ContentHash.Should().Be(Sha);
|
||
meta.FolderId.Should().BeNull();
|
||
meta.OwnerId.Should().Be(OwnerId);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Upload_超过500MiB_抛TooLarge()
|
||
{
|
||
var (db, s3, user) = CreateDeps(nameof(Upload_超过500MiB_抛TooLarge));
|
||
var handler = new UploadFileCommandHandler(s3.Object, db, user.Object, BucketResolver(), new Mock<MediatR.IMediator>().Object);
|
||
|
||
var act = () => handler.Handle(
|
||
new UploadFileCommand("big", new MemoryStream(), "application/octet-stream", 501 * 1024 * 1024),
|
||
CancellationToken.None);
|
||
|
||
await act.Should().ThrowAsync<TooLargeException>();
|
||
s3.Verify(x => x.UploadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Upload_同名两次_最终key不同_不覆盖历史对象()
|
||
{
|
||
var (db, s3, user) = CreateDeps(nameof(Upload_同名两次_最终key不同_不覆盖历史对象));
|
||
var handler = new UploadFileCommandHandler(s3.Object, db, user.Object, BucketResolver(), new Mock<MediatR.IMediator>().Object);
|
||
|
||
var r1 = await handler.Handle(
|
||
new UploadFileCommand("doc.pdf", new MemoryStream([1]), "a", 1, Sha),
|
||
CancellationToken.None);
|
||
var r2 = await handler.Handle(
|
||
new UploadFileCommand("doc.pdf", new MemoryStream([2]), "a", 1, Sha),
|
||
CancellationToken.None);
|
||
|
||
// 每次上传独立段:同名再传不覆盖 S3 上的旧对象
|
||
r1.ObjectKey.Should().NotBe(r2.ObjectKey);
|
||
(await db.Files.CountAsync()).Should().Be(2);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Upload_非法文件名_抛InvalidParam且不触S3()
|
||
{
|
||
var (db, s3, user) = CreateDeps(nameof(Upload_非法文件名_抛InvalidParam且不触S3));
|
||
var handler = new UploadFileCommandHandler(s3.Object, db, user.Object, BucketResolver(), new Mock<MediatR.IMediator>().Object);
|
||
|
||
var act = () => handler.Handle(
|
||
new UploadFileCommand("\u0000evil.txt", new MemoryStream([1]), "a", 1),
|
||
CancellationToken.None);
|
||
|
||
(await act.Should().ThrowAsync<BuildFlow.Shared.Domain.Exceptions.BusinessException>()).Which.Code.Should().Be(ErrorCode.InvalidParam);
|
||
s3.Verify(x => x.UploadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||
}
|
||
}
|