build-flow.api/tests/BuildFlow.File.Tests/UploadFileCommandHandlerTests.cs

120 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}