build-flow.api/tests/BuildFlow.File.Tests/InstantUploadTests.cs
xiangning 8f85a2d3c8 refactor(file): 移除bucket参数并使用默认桶解析器
- 移除AbortMultipartCommand中的BucketName参数
- 移除CompleteMultipartCommand中的BucketName参数
- 移除DeleteFileCommand中的BucketName参数
- 移除DownloadFileQuery中的BucketName参数
- 移除GetFileContentQuery中的BucketName参数
- 移除GetFilePreviewQuery中的BucketName参数
- 移除GetOfficePreviewQuery中的BucketName参数
- 移除InitMultipartCommand中的BucketName参数
- 在各处理器中注入IBucketResolver并获取默认桶
- 更新所有相关端点和请求模型移除bucket参数
- 修改测试代码适配参数变更
- 添加IM离线消息到平台通知中心的跨模块集成实现
2026-07-03 13:32:08 +08:00

153 lines
7.1 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;
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 完成后落 FileMeta(缺陷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 FileMeta
{
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 FileMeta
{
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 FileMeta
{
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核心回归:分片完成必须落 FileMeta,否则同文件再次分片上传永远命中不了秒传。
var (db, s3, user) = CreateDeps(nameof(Complete_成功后落FileMeta_使后续分片秒传可命中));
// 预置一个 Active 会话(带 ContentHash)
db.UploadSessions.Add(new MultipartUploadSession
{
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");
// FileMeta 应被创建
var meta = await db.Files.SingleOrDefaultAsync();
meta.Should().NotBeNull("分片完成后应落 FileMeta 供秒传查重");
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("FileMeta 已落库,再次分片上传同文件应秒传");
}
}