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