using File.Application.Common;
using File.Application.Features.Files.Handlers;
using File.Application.Features.Files.Queries;
using File.Infrastructure.Storage;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace BuildFlow.File.Tests;
///
/// 4 个文件查询 Handler 测试(均为纯委托/组装,无异常分支,仅覆盖成功路径):
/// GetFileContentQueryHandler / GetFilePreviewQueryHandler / GetOfficePreviewQueryHandler / ListFilesQueryHandler。
///
public class FileQueryHandlersTests
{
private static Mock Bucket(string name = "files")
{
var bucket = new Mock();
bucket.Setup(x => x.GetDefaultBucketAsync(It.IsAny())).ReturnsAsync(name);
return bucket;
}
#region GetFileContentQueryHandler
[Fact]
public async Task GetFileContent_返回S3文本内容()
{
var s3 = new Mock();
s3.Setup(x => x.GetFileContentAsync("files", "k1", It.IsAny(), It.IsAny()))
.ReturnsAsync("hello world");
var handler = new GetFileContentQueryHandler(s3.Object, Bucket().Object);
var result = await handler.Handle(new GetFileContentQuery("k1"), CancellationToken.None);
result.Content.Should().Be("hello world");
s3.Verify(x => x.GetFileContentAsync("files", "k1", It.IsAny(), It.IsAny()), Times.Once);
}
#endregion
#region GetFilePreviewQueryHandler
[Fact]
public async Task GetFilePreview_返回24h预签名URL()
{
var s3 = new Mock();
s3.Setup(x => x.GeneratePresignedUrlAsync(
"files", "k2", It.IsAny(), It.IsAny()))
.ReturnsAsync("https://presigned.example.com/k2");
var handler = new GetFilePreviewQueryHandler(s3.Object, Bucket().Object);
var result = await handler.Handle(new GetFilePreviewQuery("k2"), CancellationToken.None);
result.PresignedUrl.Should().Be("https://presigned.example.com/k2");
}
#endregion
#region GetOfficePreviewQueryHandler
[Fact]
public async Task GetOfficePreview_返回KKFileView预览地址()
{
var kk = new Mock>();
kk.Setup(x => x.CurrentValue).Returns(new KkFileViewOptions
{
BaseUrl = "http://kk.example.com",
BackendBaseUrl = "http://backend.example.com",
InternalKey = "secret-key",
});
var handler = new GetOfficePreviewQueryHandler(kk.Object);
var result = await handler.Handle(new GetOfficePreviewQuery("docs/report.pdf"), CancellationToken.None);
result.PreviewUrl.Should().Contain("http://kk.example.com/onlinePreview?url=");
result.PreviewUrl.Should().NotBeNullOrEmpty();
}
#endregion
#region ListFilesQueryHandler
[Fact]
public async Task ListFiles_映射S3对象列表并返回()
{
var s3 = new Mock();
s3.Setup(x => x.ListObjectsAsync(
"files", "prefix/", It.IsAny(), null, It.IsAny()))
.ReturnsAsync(new ListFilesResult(
new List { new("prefix/a.txt", 10, new DateTime(2026, 1, 1), "etag-1") },
null));
var handler = new ListFilesQueryHandler(s3.Object, Bucket().Object);
var result = await handler.Handle(
new ListFilesQuery("prefix/", null, null), CancellationToken.None);
result.Files.Should().HaveCount(1);
result.Files[0].Key.Should().Be("prefix/a.txt");
result.Files[0].Size.Should().Be(10);
result.NextContinuationToken.Should().BeNull();
}
[Fact]
public async Task ListFiles_未指定MaxKeys_使用默认分页上限()
{
var s3 = new Mock();
s3.Setup(x => x.ListObjectsAsync(
"files", null, It.IsAny(), null, It.IsAny()))
.ReturnsAsync(new ListFilesResult(new List(), null));
var handler = new ListFilesQueryHandler(s3.Object, Bucket().Object);
await handler.Handle(new ListFilesQuery(null, null, null), CancellationToken.None);
// 默认 100 条;断言 S3 收到的是默认上限(<=1000 上限内)
s3.Verify(x => x.ListObjectsAsync(
"files", null, It.Is(v => v > 0 && v <= 1000), null, It.IsAny()), Times.Once);
}
#endregion
}