build-flow.api/tests/BuildFlow.File.Tests/InstantUploadTests.cs
xiangning 6c110b579a refactor(dependencies): 移除未使用的域实体引用
- 从文件模块的多部分上传处理器中移除未使用的实体引用
- 从即时通讯模块的群组成员处理器中移除未使用的实体引用
- 从RAG模块的AI模型配置中移除未使用的实体引用
- 从监控模块的告警规则配置中移除未使用的实体引用
- 从平台模块的角色权限处理器中移除未使用的实体引用
- 从脚本模块的执行日志实体引用中进行清理
- 从多个基础设施配置类中移除未使用的依赖项
- 优化测试文件中的导入声明,移除不必要的引用
2026-07-19 14:09:47 +08:00

155 lines
7.2 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.Infrastructure;
using File.Application.Common;
using File.Application.Features.Multipart.Commands;
using File.Application.Features.Multipart.DTOs;
using File.Application.Features.Multipart.Handlers;
using File.Domain.Entities.FileMeta;
using File.Domain.Entities.MultipartUploadSession;
using File.Domain.Values;
using File.Infrastructure.Persistence;
using File.Infrastructure.Storage;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Moq;
using Xunit;
namespace BuildFlow.File.Tests;
/// <summary>
/// 秒传在分片上传路径的端到端测试。覆盖缺陷 2、3 的修复:
/// - InitMultipart 命中同用户同哈希同大小文件 → Instant=true(缺陷3:三元组判定)
/// - CompleteMultipart 完成后落 FileMetaEntity(缺陷2:分片秒传数据来源)
/// 两者串联验证:分片上传完成后,再次 init 同文件能命中秒传。
/// </summary>
public class InstantUploadTests
{
private const string OwnerId = "user-001";
private const string Sha = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
private const string Bucket = "files";
/// <summary>测试用 bucket 解析器,固定返回与 <see cref="Bucket"/> 一致的桶名。</summary>
private static IBucketResolver BucketResolver()
{
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 options = new DbContextOptionsBuilder<FileDbContext>()
.UseInMemoryDatabase(dbName)
.AddInterceptors(new AuditInterceptor(user.Object))
.Options;
return (new FileDbContext(options), new Mock<IS3StorageService>(), user);
}
[Fact]
public async Task Init_同用户同哈希同大小文件存在_返回Instant秒传()
{
var (db, s3, user) = CreateDeps(nameof(Init_同用户同哈希同大小文件存在_返回Instant秒传));
// 预置一条已存在的文件(模拟此前上传完成)
db.Files.Add(new FileMetaEntity
{
Name = "a.bin", S3Key = "k1", S3Bucket = "files",
Size = 1024, ContentType = "application/octet-stream",
ContentHash = Sha, OwnerId = OwnerId,
});
await db.SaveChangesAsync();
var handler = new InitMultipartCommandHandler(s3.Object, db, user.Object, BucketResolver());
var result = await handler.Handle(new InitMultipartCommand("obj", Sha, TotalParts: 1, Size: 1024), CancellationToken.None);
result.Instant.Should().BeTrue("同用户同哈希同大小应命中秒传");
result.UploadId.Should().BeEmpty("秒传不需要 uploadId");
result.Resume.Should().BeFalse();
// 秒传命中不应发起 S3 multipart
s3.Verify(x => x.CreateMultipartUploadAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Init_同哈希但大小不同_不命中秒传_走正常init()
{
// 缺陷3回归:Size 防碰撞。库里 1024,请求 2048,哈希相同也不应误判秒传。
var (db, s3, user) = CreateDeps(nameof(Init_同哈希但大小不同_不命中秒传_走正常init));
db.Files.Add(new FileMetaEntity
{
Name = "a.bin", S3Key = "k1", S3Bucket = "files",
Size = 1024, ContentType = "application/octet-stream",
ContentHash = Sha, OwnerId = OwnerId,
});
await db.SaveChangesAsync();
s3.Setup(x => x.CreateMultipartUploadAsync("files", "obj", default)).ReturnsAsync("uid-1");
var handler = new InitMultipartCommandHandler(s3.Object, db, user.Object, BucketResolver());
var result = await handler.Handle(new InitMultipartCommand("obj", Sha, TotalParts: 1, Size: 2048), CancellationToken.None);
result.Instant.Should().BeFalse("大小不同不应命中秒传");
result.UploadId.Should().Be("uid-1");
}
[Fact]
public async Task Init_他人文件不命中秒传_所有权隔离()
{
var (db, s3, user) = CreateDeps(nameof(Init_他人文件不命中秒传_所有权隔离));
db.Files.Add(new FileMetaEntity
{
Name = "a.bin", S3Key = "k1", S3Bucket = "files",
Size = 1024, ContentType = "application/octet-stream",
ContentHash = Sha, OwnerId = "another-user",
});
await db.SaveChangesAsync();
s3.Setup(x => x.CreateMultipartUploadAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync("uid-1");
var handler = new InitMultipartCommandHandler(s3.Object, db, user.Object, BucketResolver());
var result = await handler.Handle(new InitMultipartCommand("obj", Sha, TotalParts: 1, Size: 1024), CancellationToken.None);
result.Instant.Should().BeFalse("他人文件不参与当前用户秒传");
}
[Fact]
public async Task Complete_成功后落FileMeta_使后续分片秒传可命中()
{
// 缺陷2核心回归:分片完成必须落 FileMetaEntity,否则同文件再次分片上传永远命中不了秒传。
var (db, s3, user) = CreateDeps(nameof(Complete_成功后落FileMeta_使后续分片秒传可命中));
// 预置一个 Active 会话(带 ContentHash)
db.UploadSessions.Add(new MultipartUploadSessionEntity
{
BucketName = "files", ObjectKey = "obj-key", UploadId = "uid-1",
ContentHash = Sha, OwnerId = OwnerId, Status = UploadSessionStatus.Active,
});
await db.SaveChangesAsync();
var parts = new List<CompletedPartDto>
{
new(1, "etag-1"), new(2, "etag-2"),
};
var handler = new CompleteMultipartCommandHandler(s3.Object, db, user.Object, BucketResolver());
var result = await handler.Handle(new CompleteMultipartCommand("obj-key", "uid-1", parts, FileName: "a.bin", Size: 1024), CancellationToken.None);
result.Location.Should().Be("files/obj-key");
// FileMetaEntity 应被创建
var meta = await db.Files.SingleOrDefaultAsync();
meta.Should().NotBeNull("分片完成后应落 FileMetaEntity 供秒传查重");
meta!.ContentHash.Should().Be(Sha);
meta.Size.Should().Be(1024);
meta.Name.Should().Be("a.bin");
meta.OwnerId.Should().Be(OwnerId);
// 会话应标记为 Completed
var session = await db.UploadSessions.SingleAsync();
session.Status.Should().Be(UploadSessionStatus.Completed);
// 串联验证:再次 init 同文件现在能命中秒传
var initHandler = new InitMultipartCommandHandler(s3.Object, db, user.Object, BucketResolver());
var initResult = await initHandler.Handle(new InitMultipartCommand("obj2", Sha, TotalParts: 2, Size: 1024), CancellationToken.None);
initResult.Instant.Should().BeTrue("FileMetaEntity 已落库,再次分片上传同文件应秒传");
}
}