- 将通用 Entities 命名空间拆分为具体业务实体子目录 - 更新所有实体类型引用为具体的 Entity 后缀类型 - 将 ModelType、ModelProvider、GroupMemberRole 等枚举重命名为 Enum 后缀 - 重构 AiModel 相关配置和映射逻辑适配新的实体结构 - 更新测试代码中的实体类型引用和枚举使用 - 调整应用层处理器中的实体创建和查询逻辑 - 修改 appsettings.json 中的基础设施主机配置为动态占位符模式
177 lines
7.5 KiB
C#
177 lines
7.5 KiB
C#
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.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 对象和 FileMetaEntity 记录,
|
||
/// 否则文件夹视图(读 DB)仍显示已失效文件,或 S3 残留孤儿对象。
|
||
/// </summary>
|
||
public class DeleteFileCommandHandlerTests
|
||
{
|
||
private const string Bucket = "build-flow";
|
||
private const string Key = "multipart/abc123";
|
||
|
||
/// <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;
|
||
}
|
||
|
||
/// <summary>构造测试依赖:InMemory DB(挂审计拦截器自动填充非空字段)+ Mock S3 + Mock Mediator。</summary>
|
||
private static (FileDbContext db, Mock<IS3StorageService> s3, Mock<MediatR.IMediator> mediator) CreateDeps(string dbName)
|
||
{
|
||
var user = new Mock<ICurrentUserContext>();
|
||
user.SetupGet(u => u.UserId).Returns("user-001");
|
||
user.SetupGet(u => u.IPAddress).Returns("127.0.0.1");
|
||
var s3 = new Mock<IS3StorageService>();
|
||
var mediator = new Mock<MediatR.IMediator>();
|
||
var options = new DbContextOptionsBuilder<FileDbContext>()
|
||
.UseInMemoryDatabase(dbName)
|
||
.AddInterceptors(new AuditInterceptor(user.Object))
|
||
.Options;
|
||
return (new FileDbContext(options), s3, mediator);
|
||
}
|
||
|
||
/// <summary>构造一条 FileMetaEntity 记录。</summary>
|
||
private static FileMetaEntity NewMeta(string bucket, string key, string owner = "user-001")
|
||
=> new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Name = key,
|
||
S3Key = key,
|
||
S3Bucket = bucket,
|
||
Size = 1024,
|
||
ContentType = "text/plain",
|
||
OwnerId = owner,
|
||
};
|
||
|
||
[Fact]
|
||
public async Task Delete_存在FileMeta_同时删S3和数据库记录()
|
||
{
|
||
// Arrange:DB 有一条匹配的 FileMetaEntity
|
||
var (db, s3, mediator) = CreateDeps(nameof(Delete_存在FileMeta_同时删S3和数据库记录));
|
||
db.Files.Add(NewMeta(Bucket, Key));
|
||
await db.SaveChangesAsync();
|
||
|
||
var handler = new DeleteFileCommandHandler(s3.Object, db, BucketResolver(), mediator.Object);
|
||
|
||
// Act
|
||
await handler.Handle(new DeleteFileCommand(Key), CancellationToken.None);
|
||
|
||
// Assert:S3 对象已删
|
||
s3.Verify(x => x.DeleteFileAsync(Bucket, Key, It.IsAny<CancellationToken>()), Times.Once);
|
||
// Assert:FileMetaEntity 记录已删(核心回归点——之前漏删导致文件夹视图仍显示)
|
||
var remaining = await db.Files.ToListAsync();
|
||
remaining.Should().BeEmpty("删除后 FileMetaEntity 记录必须同步清理");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Delete_无FileMetaEntity记录_仍删S3对象_不报错()
|
||
{
|
||
// Arrange:DB 无匹配记录(如根目录直传未落库、或已被其他途径清理)
|
||
var (db, s3, mediator) = CreateDeps(nameof(Delete_无FileMetaEntity记录_仍删S3对象_不报错));
|
||
var handler = new DeleteFileCommandHandler(s3.Object, db, BucketResolver(), mediator.Object);
|
||
|
||
// Act
|
||
await handler.Handle(new DeleteFileCommand(Key), CancellationToken.None);
|
||
|
||
// Assert:S3 仍尝试删除(幂等,删不存在的对象也返回成功)
|
||
s3.Verify(x => x.DeleteFileAsync(Bucket, Key, It.IsAny<CancellationToken>()), Times.Once);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Delete_多条同bucket同key的FileMeta_全部清理()
|
||
{
|
||
// Arrange:极端场景——同 bucket+key 存在多条 FileMetaEntity(历史数据或并发落库)
|
||
var (db, s3, mediator) = CreateDeps(nameof(Delete_多条同bucket同key的FileMeta_全部清理));
|
||
db.Files.Add(NewMeta(Bucket, Key, "user-001"));
|
||
db.Files.Add(NewMeta(Bucket, Key, "user-002"));
|
||
await db.SaveChangesAsync();
|
||
|
||
var handler = new DeleteFileCommandHandler(s3.Object, db, BucketResolver(), mediator.Object);
|
||
|
||
// Act
|
||
await handler.Handle(new DeleteFileCommand(Key), CancellationToken.None);
|
||
|
||
// Assert:两条记录都被清理
|
||
var remaining = await db.Files.Where(f => f.S3Bucket == Bucket && f.S3Key == Key).ToListAsync();
|
||
remaining.Should().BeEmpty("同 bucket+key 的所有 FileMetaEntity 都应被清理");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Delete_不影响其他文件的FileMetaEntity()
|
||
{
|
||
// Arrange:删 A 文件,B 文件应不受影响
|
||
var (db, s3, mediator) = CreateDeps(nameof(Delete_不影响其他文件的FileMetaEntity));
|
||
db.Files.Add(NewMeta(Bucket, "multipart/aaa"));
|
||
db.Files.Add(NewMeta(Bucket, "multipart/bbb"));
|
||
await db.SaveChangesAsync();
|
||
|
||
var handler = new DeleteFileCommandHandler(s3.Object, db, BucketResolver(), mediator.Object);
|
||
|
||
// Act:只删 aaa
|
||
await handler.Handle(new DeleteFileCommand("multipart/aaa"), CancellationToken.None);
|
||
|
||
// Assert:aaa 没了,bbb 还在
|
||
var remaining = await db.Files.ToListAsync();
|
||
remaining.Should().HaveCount(1);
|
||
remaining[0].S3Key.Should().Be("multipart/bbb");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Delete_S3删除失败_抛异常不删数据库()
|
||
{
|
||
// Arrange:S3 删除抛异常(网络故障/权限不足等)
|
||
var (db, s3, mediator) = CreateDeps(nameof(Delete_S3删除失败_抛异常不删数据库));
|
||
db.Files.Add(NewMeta(Bucket, Key));
|
||
await db.SaveChangesAsync();
|
||
s3.Setup(x => x.DeleteFileAsync(Bucket, Key, It.IsAny<CancellationToken>()))
|
||
.ThrowsAsync(new Exception("S3 连接失败"));
|
||
|
||
var handler = new DeleteFileCommandHandler(s3.Object, db, BucketResolver(), mediator.Object);
|
||
|
||
// Act
|
||
var act = () => handler.Handle(new DeleteFileCommand(Key), CancellationToken.None);
|
||
|
||
// Assert:异常向上抛
|
||
await act.Should().ThrowAsync<Exception>().WithMessage("S3 连接失败");
|
||
// Assert:FileMetaEntity 仍在(S3 删失败时不删库,避免库删了但对象还在的孤儿)
|
||
var remaining = await db.Files.ToListAsync();
|
||
remaining.Should().HaveCount(1, "S3 删除失败时不应清理数据库记录");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Delete_发布FileDeletedEvent()
|
||
{
|
||
// Arrange
|
||
var (db, s3, mediator) = CreateDeps(nameof(Delete_发布FileDeletedEvent));
|
||
var handler = new DeleteFileCommandHandler(s3.Object, db, BucketResolver(), mediator.Object);
|
||
|
||
// Act
|
||
await handler.Handle(new DeleteFileCommand(Key), CancellationToken.None);
|
||
|
||
// Assert:事件已发布(供其他模块响应,如清理缓存)
|
||
mediator.Verify(
|
||
x => x.Publish(It.IsAny<global::File.Application.Events.FileDeletedEvent>(), It.IsAny<CancellationToken>()),
|
||
Times.Once);
|
||
}
|
||
}
|