- 将通用 Entities 命名空间拆分为具体业务实体子目录 - 更新所有实体类型引用为具体的 Entity 后缀类型 - 将 ModelType、ModelProvider、GroupMemberRole 等枚举重命名为 Enum 后缀 - 重构 AiModel 相关配置和映射逻辑适配新的实体结构 - 更新测试代码中的实体类型引用和枚举使用 - 调整应用层处理器中的实体创建和查询逻辑 - 修改 appsettings.json 中的基础设施主机配置为动态占位符模式
144 lines
5.7 KiB
C#
144 lines
5.7 KiB
C#
using BuildFlow.Shared.Domain.Common;
|
||
using BuildFlow.Shared.Infrastructure;
|
||
using File.Application.Features.Files.Handlers;
|
||
using File.Application.Features.Files.Queries;
|
||
using File.Domain.Entities.FileMeta;
|
||
using File.Domain.Entities.FileSetting;
|
||
using File.Domain.Entities.Folder;
|
||
using File.Domain.Entities.MultipartUploadSession;
|
||
using File.Domain.Entities.ShareLink;
|
||
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>
|
||
/// 秒传预检测试。验证查重三维度(同用户/跨用户/防碰撞)+ S3 存在性校验(孤儿清理)。
|
||
/// </summary>
|
||
public class CheckFileQueryHandlerTests
|
||
{
|
||
private const string OwnerId = "user-001";
|
||
private const string Sha = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
|
||
|
||
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>();
|
||
// 默认对象存在;个别测试覆盖 false
|
||
s3.Setup(x => x.ObjectExistsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(true);
|
||
|
||
var options = new DbContextOptionsBuilder<FileDbContext>()
|
||
.UseInMemoryDatabase(dbName)
|
||
.AddInterceptors(new AuditInterceptor(user.Object))
|
||
.Options;
|
||
return (new FileDbContext(options), s3, user);
|
||
}
|
||
|
||
private static FileMetaEntity NewFile(string owner, string hash, long size, string name = "a.txt")
|
||
=> new()
|
||
{
|
||
Name = name,
|
||
S3Key = Guid.NewGuid().ToString(),
|
||
S3Bucket = "files",
|
||
Size = size,
|
||
ContentType = "text/plain",
|
||
ContentHash = hash,
|
||
OwnerId = owner,
|
||
};
|
||
|
||
[Fact]
|
||
public async Task Check_同用户同哈希同大小_S3对象存在_命中秒传()
|
||
{
|
||
var (db, s3, user) = CreateDeps(nameof(Check_同用户同哈希同大小_S3对象存在_命中秒传));
|
||
db.Files.Add(NewFile(OwnerId, Sha, 100));
|
||
await db.SaveChangesAsync();
|
||
|
||
var handler = new CheckFileQueryHandler(db, s3.Object, user.Object);
|
||
|
||
var result = await handler.Handle(new CheckFileQuery(Sha, 100), CancellationToken.None);
|
||
|
||
result.Exists.Should().BeTrue();
|
||
result.File.Should().NotBeNull();
|
||
result.File!.Size.Should().Be(100);
|
||
result.File.OwnerId.Should().Be(OwnerId);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Check_无匹配_返回未命中()
|
||
{
|
||
var (db, s3, user) = CreateDeps(nameof(Check_无匹配_返回未命中));
|
||
var handler = new CheckFileQueryHandler(db, s3.Object, user.Object);
|
||
|
||
var result = await handler.Handle(new CheckFileQuery(Sha, 100), CancellationToken.None);
|
||
|
||
result.Exists.Should().BeFalse();
|
||
result.File.Should().BeNull();
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Check_他人文件不命中_所有权隔离()
|
||
{
|
||
var (db, s3, user) = CreateDeps(nameof(Check_他人文件不命中_所有权隔离));
|
||
db.Files.Add(NewFile("another-user", Sha, 100));
|
||
await db.SaveChangesAsync();
|
||
var handler = new CheckFileQueryHandler(db, s3.Object, user.Object);
|
||
|
||
var result = await handler.Handle(new CheckFileQuery(Sha, 100), CancellationToken.None);
|
||
|
||
result.Exists.Should().BeFalse("他人文件不应被秒传命中(所有权隔离)");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Check_大小不匹配不命中_防哈希碰撞()
|
||
{
|
||
var (db, s3, user) = CreateDeps(nameof(Check_大小不匹配不命中_防哈希碰撞));
|
||
db.Files.Add(NewFile(OwnerId, Sha, 100));
|
||
await db.SaveChangesAsync();
|
||
var handler = new CheckFileQueryHandler(db, s3.Object, user.Object);
|
||
|
||
var result = await handler.Handle(new CheckFileQuery(Sha, 999), CancellationToken.None);
|
||
|
||
result.Exists.Should().BeFalse("大小不同不应命中(防哈希碰撞)");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Check_文件无哈希不命中()
|
||
{
|
||
var (db, s3, user) = CreateDeps(nameof(Check_文件无哈希不命中));
|
||
db.Files.Add(NewFile(OwnerId, hash: null!, 100));
|
||
await db.SaveChangesAsync();
|
||
var handler = new CheckFileQueryHandler(db, s3.Object, user.Object);
|
||
|
||
var result = await handler.Handle(new CheckFileQuery(Sha, 100), CancellationToken.None);
|
||
|
||
result.Exists.Should().BeFalse("无哈希的老文件不参与秒传查重");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Check_S3对象已丢失_清理孤儿FileMeta_返回未命中()
|
||
{
|
||
// 缺陷E回归:FileMetaEntity 存在但 S3 对象被外部删除,应清理孤儿并返回未命中(而非误报秒传成功)
|
||
var (db, s3, user) = CreateDeps(nameof(Check_S3对象已丢失_清理孤儿FileMeta_返回未命中));
|
||
db.Files.Add(NewFile(OwnerId, Sha, 100));
|
||
await db.SaveChangesAsync();
|
||
s3.Setup(x => x.ObjectExistsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(false); // S3 对象不存在
|
||
var handler = new CheckFileQueryHandler(db, s3.Object, user.Object);
|
||
|
||
var result = await handler.Handle(new CheckFileQuery(Sha, 100), CancellationToken.None);
|
||
|
||
result.Exists.Should().BeFalse("S3 对象丢失不应误报秒传成功");
|
||
// 孤儿 FileMetaEntity 应被清理
|
||
var remaining = await db.Files.ToListAsync();
|
||
remaining.Should().BeEmpty("孤儿 FileMetaEntity 应被清理");
|
||
}
|
||
}
|