build-flow.api/tests/BuildFlow.File.Tests/ShareDownloadTests.cs
xiangning 7ecd78550f test: 补充 platform/rag/file 模块单元测试
- platform: 通知与权限模块 handler 测试
- rag: Chat 端到端、知识库 CRUD、嵌入与查询测试
- file: 文件分享下载与文件夹/桶设置测试
2026-07-09 12:52:00 +08:00

153 lines
5.7 KiB
C#

using BuildFlow.Shared.Domain.Common;
using BuildFlow.Shared.Domain.Exceptions;
using BuildFlow.Shared.Infrastructure;
using File.Application.Features.Shares.Handlers;
using File.Application.Features.Shares.Queries;
using File.Domain.Entities;
using File.Domain.Exceptions;
using File.Infrastructure.Persistence;
using File.Infrastructure.Storage;
using FluentAssertions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace BuildFlow.File.Tests;
/// <summary>
/// [SOP-FILE-02] 分享下载端到端测试。
/// 覆盖:正常下载/过期/次数耗尽/密码校验。
/// 飞书文档: https://ecn1rid3e27z.feishu.cn/wiki/GeOwweCmfi1yMLk62iIcOKLInOE
/// </summary>
public class ShareDownloadTests
{
private static (FileDbContext db, Mock<IS3StorageService> s3, Mock<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 options = new DbContextOptionsBuilder<FileDbContext>()
.UseInMemoryDatabase($"share-{dbName}-{Guid.NewGuid():N}")
.AddInterceptors(new AuditInterceptor(user.Object))
.Options;
var db = new FileDbContext(options);
var s3 = new Mock<IS3StorageService>();
s3.Setup(x => x.GeneratePresignedUrlAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("https://presigned.example.com/file");
return (db, s3, new Mock<IMediator>());
}
private static async Task<(FileDbContext db, DownloadShareQueryHandler handler)> SetupWithShareAsync(
string name, ShareLink share)
{
var (db, s3, mediator) = CreateDeps(name);
share.ResourceType = "file"; // 必填(folder/file)
db.ShareLinks.Add(share);
db.Files.Add(new FileMeta { Id = share.ResourceId, Name = "doc.pdf", S3Key = "k", S3Bucket = "b", ContentType = "application/pdf", OwnerId = "user-001" });
await db.SaveChangesAsync();
var handler = new DownloadShareQueryHandler(db, s3.Object, mediator.Object, NullLogger<DownloadShareQueryHandler>.Instance);
return (db, handler);
}
private static string UniqueToken(string seed) => (seed + "padding1234567890123456789012")[..32];
[Fact]
public async Task Download_ValidShare_ReturnsPresignedUrl()
{
var share = new ShareLink { ResourceId = Guid.NewGuid(), Token = UniqueToken("valid") };
var (_, handler) = await SetupWithShareAsync("valid", share);
var result = await handler.Handle(
new DownloadShareQuery(share.Token, null), CancellationToken.None);
result.PresignedUrl.Should().StartWith("https://presigned");
result.FileName.Should().Be("doc.pdf");
}
[Fact]
public async Task Download_Expired_ThrowsShareExpired()
{
var share = new ShareLink
{
ResourceId = Guid.NewGuid(),
Token = UniqueToken("expired"),
ExpiresAt = DateTime.UtcNow.AddMinutes(-10)
};
var (_, handler) = await SetupWithShareAsync("expired", share);
var act = async () => await handler.Handle(
new DownloadShareQuery(share.Token, null), CancellationToken.None);
await act.Should().ThrowAsync<ShareExpiredException>("过期分享应被拒绝");
}
[Fact]
public async Task Download_Exhausted_ThrowsShareExhausted()
{
var share = new ShareLink
{
ResourceId = Guid.NewGuid(),
Token = UniqueToken("exhausted"),
MaxDownloads = 3,
DownloadCount = 3
};
var (_, handler) = await SetupWithShareAsync("exhausted", share);
var act = async () => await handler.Handle(
new DownloadShareQuery(share.Token, null), CancellationToken.None);
await act.Should().ThrowAsync<ShareExhaustedException>("次数耗尽应被拒绝");
}
[Fact]
public async Task Download_ProtectedNoPassword_ThrowsSharePassword()
{
var share = new ShareLink
{
ResourceId = Guid.NewGuid(),
Token = UniqueToken("pwd-missing"),
Password = BCrypt.Net.BCrypt.HashPassword("secret123")
};
var (_, handler) = await SetupWithShareAsync("pwd-missing", share);
var act = async () => await handler.Handle(
new DownloadShareQuery(share.Token, null), CancellationToken.None);
await act.Should().ThrowAsync<SharePasswordException>("无密码访问受保护分享应被拒绝");
}
[Fact]
public async Task Download_ProtectedWrongPassword_ThrowsSharePassword()
{
var share = new ShareLink
{
ResourceId = Guid.NewGuid(),
Token = UniqueToken("pwd-wrong"),
Password = BCrypt.Net.BCrypt.HashPassword("secret123")
};
var (_, handler) = await SetupWithShareAsync("pwd-wrong", share);
var act = async () => await handler.Handle(
new DownloadShareQuery(share.Token, "wrongpassword"), CancellationToken.None);
await act.Should().ThrowAsync<SharePasswordException>("密码错误应被拒绝");
}
[Fact]
public async Task Download_NonExistentToken_ThrowsNotFound()
{
var (db, s3, mediator) = CreateDeps("not-found");
var handler = new DownloadShareQueryHandler(db, s3.Object, mediator.Object, NullLogger<DownloadShareQueryHandler>.Instance);
var act = async () => await handler.Handle(
new DownloadShareQuery(UniqueToken("nope"), null), CancellationToken.None);
await act.Should().ThrowAsync<NotFoundException>("不存在的 token 应返回 NotFound");
}
}