153 lines
5.7 KiB
C#
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");
|
|
}
|
|
}
|