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 }