build-flow.api/tests/BuildFlow.File.Tests/DeleteFileCommandHandlerTests.cs
xiangning 3b13099378 refactor(domain): 重构领域实体结构并更新枚举命名
- 将通用 Entities 命名空间拆分为具体业务实体子目录
- 更新所有实体类型引用为具体的 Entity 后缀类型
- 将 ModelType、ModelProvider、GroupMemberRole 等枚举重命名为 Enum 后缀
- 重构 AiModel 相关配置和映射逻辑适配新的实体结构
- 更新测试代码中的实体类型引用和枚举使用
- 调整应用层处理器中的实体创建和查询逻辑
- 修改 appsettings.json 中的基础设施主机配置为动态占位符模式
2026-07-19 14:02:32 +08:00

177 lines
7.5 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.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);
}
}