build-flow.api/tests/BuildFlow.File.Tests/DeleteFileCommandHandlerTests.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

172 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.Files.Commands;
using File.Application.Features.Files.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>
/// 删除文件命令处理器测试。核心回归:删除必须同时清理 S3 对象和 FileMeta 记录,
/// 否则文件夹视图(读 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>构造一条 FileMeta 记录。</summary>
private static FileMeta 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 有一条匹配的 FileMeta
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:FileMeta 记录已删(核心回归点——之前漏删导致文件夹视图仍显示)
var remaining = await db.Files.ToListAsync();
remaining.Should().BeEmpty("删除后 FileMeta 记录必须同步清理");
}
[Fact]
public async Task Delete_无FileMeta记录_仍删S3对象_不报错()
{
// Arrange:DB 无匹配记录(如根目录直传未落库、或已被其他途径清理)
var (db, s3, mediator) = CreateDeps(nameof(Delete_无FileMeta记录_仍删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 存在多条 FileMeta(历史数据或并发落库)
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 的所有 FileMeta 都应被清理");
}
[Fact]
public async Task Delete_不影响其他文件的FileMeta()
{
// Arrange:删 A 文件,B 文件应不受影响
var (db, s3, mediator) = CreateDeps(nameof(Delete_不影响其他文件的FileMeta));
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:FileMeta 仍在(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);
}
}