using BuildFlow.Shared.Domain.Common;
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;
///
/// 直传上传处理器测试。验证缺陷 C 修复:直传完成后落 FileMetaEntity,使直传路径参与秒传。
///
public class UploadFileCommandHandlerTests
{
private const string OwnerId = "user-001";
private const string Sha = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
/// 测试用 bucket 解析器,固定返回 "files"。
private static IBucketResolver BucketResolver(string bucket = "files")
{
var mock = new Mock();
mock.Setup(r => r.GetDefaultBucketAsync(It.IsAny())).ReturnsAsync(bucket);
return mock.Object;
}
private static (FileDbContext db, Mock s3, Mock user) CreateDeps(string dbName)
{
var user = new Mock();
user.SetupGet(u => u.UserId).Returns(OwnerId);
user.SetupGet(u => u.IPAddress).Returns("127.0.0.1");
var s3 = new Mock();
var options = new DbContextOptionsBuilder()
.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().Object);
var result = await handler.Handle(
new UploadFileCommand("obj-key", data, "application/octet-stream", 3, Sha),
CancellationToken.None);
result.ObjectKey.Should().Be("obj-key");
s3.Verify(x => x.UploadFileAsync("files", "obj-key", It.IsAny(), "application/octet-stream", It.IsAny()), Times.Once);
// FileMetaEntity 应被创建,含 ContentHash,FolderId=null(根目录散落)
var meta = await db.Files.SingleAsync();
meta.S3Key.Should().Be("obj-key");
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().Object);
var act = () => handler.Handle(
new UploadFileCommand("big", new MemoryStream(), "application/octet-stream", 501 * 1024 * 1024),
CancellationToken.None);
await act.Should().ThrowAsync();
s3.Verify(x => x.UploadFileAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Upload_同对象已存在FileMeta_不重复落库()
{
// 幂等:同一 (bucket, key) 已有 FileMetaEntity,再次上传不重复 Add
var (db, s3, user) = CreateDeps(nameof(Upload_同对象已存在FileMeta_不重复落库));
db.Files.Add(new FileMetaEntity
{
Name = "obj-key", S3Key = "obj-key", S3Bucket = "files",
Size = 3, ContentType = "application/octet-stream", ContentHash = Sha, OwnerId = OwnerId,
});
await db.SaveChangesAsync();
var handler = new UploadFileCommandHandler(s3.Object, db, user.Object, BucketResolver(), new Mock().Object);
await handler.Handle(
new UploadFileCommand("obj-key", new MemoryStream([1, 2, 3]), "application/octet-stream", 3, Sha),
CancellationToken.None);
// 仍只有一条记录
var count = await db.Files.CountAsync();
count.Should().Be(1);
}
}