404 lines
14 KiB
C#
404 lines
14 KiB
C#
using FluentAssertions;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using NSubstitute.ExceptionExtensions;
|
|
using StorageService.API.Application.Commands;
|
|
using StorageService.Domain.AggregatesModel.FileAggregate;
|
|
using StorageService.Domain.AggregatesModel.QuotaAggregate;
|
|
using StorageService.Infrastructure.Configuration;
|
|
using StorageService.Infrastructure.Storage;
|
|
using Xunit;
|
|
|
|
namespace StorageService.UnitTests.Handlers;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for SignUploadCommandHandler.
|
|
/// VI: Unit tests cho SignUploadCommandHandler.
|
|
/// </summary>
|
|
public class SignUploadCommandHandlerTests
|
|
{
|
|
private readonly IQuotaRepository _quotaRepository;
|
|
private readonly IStorageProviderFactory _storageProviderFactory;
|
|
private readonly IStorageProvider _storageProvider;
|
|
private readonly IOptions<StorageSettings> _settings;
|
|
private readonly ILogger<SignUploadCommandHandler> _logger;
|
|
private readonly SignUploadCommandHandler _handler;
|
|
|
|
private const string TestUserId = "user-123";
|
|
private const string TestFileName = "document.pdf";
|
|
private const string TestContentType = "application/pdf";
|
|
private const long TestFileSize = 1024 * 1024; // 1MB
|
|
private const long MaxFileSize = 100 * 1024 * 1024; // 100MB
|
|
private const long MaxQuotaBytes = 1024 * 1024 * 1024; // 1GB
|
|
|
|
public SignUploadCommandHandlerTests()
|
|
{
|
|
_quotaRepository = Substitute.For<IQuotaRepository>();
|
|
_storageProviderFactory = Substitute.For<IStorageProviderFactory>();
|
|
_storageProvider = Substitute.For<IStorageProvider>();
|
|
_logger = Substitute.For<ILogger<SignUploadCommandHandler>>();
|
|
|
|
_settings = Options.Create(new StorageSettings
|
|
{
|
|
MaxFileSizeBytes = MaxFileSize,
|
|
DefaultBucket = "storage-bucket",
|
|
PreSignedUrlExpirationSeconds = 3600
|
|
});
|
|
|
|
_storageProviderFactory.GetProvider().Returns(_storageProvider);
|
|
|
|
_handler = new SignUploadCommandHandler(
|
|
_quotaRepository,
|
|
_storageProviderFactory,
|
|
_settings,
|
|
_logger);
|
|
}
|
|
|
|
#region Happy Path Tests
|
|
|
|
[Fact]
|
|
public async Task Handle_ValidRequest_ReturnsSuccessWithPresignedUrl()
|
|
{
|
|
// Arrange
|
|
var command = CreateValidCommand();
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
var expectedUrl = "https://minio.example.com/bucket/object?X-Amz-Signature=...";
|
|
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns(expectedUrl);
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
result.UploadUrl.Should().Be(expectedUrl);
|
|
result.ObjectKey.Should().NotBeNullOrEmpty();
|
|
result.ExpiresAt.Should().BeAfter(DateTime.UtcNow);
|
|
result.Error.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_ValidRequest_EnsuresBucketExists()
|
|
{
|
|
// Arrange
|
|
var command = CreateValidCommand();
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("https://example.com/upload");
|
|
|
|
// Act
|
|
await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
await _storageProvider.Received(1).EnsureBucketExistsAsync(
|
|
"storage-bucket", Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region File Size Validation Tests
|
|
|
|
[Fact]
|
|
public async Task Handle_ExceedsMaxFileSize_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var command = new SignUploadCommand(
|
|
TestUserId,
|
|
TestFileName,
|
|
TestContentType,
|
|
MaxFileSize + 1);
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Error.Should().Contain("exceeds maximum");
|
|
result.UploadUrl.Should().BeNull();
|
|
result.ObjectKey.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_FileSizeAtLimit_ReturnsSuccess()
|
|
{
|
|
// Arrange
|
|
var command = new SignUploadCommand(
|
|
TestUserId,
|
|
TestFileName,
|
|
TestContentType,
|
|
MaxFileSize);
|
|
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("https://example.com/upload");
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Quota Validation Tests
|
|
|
|
[Fact]
|
|
public async Task Handle_QuotaExceeded_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var command = CreateValidCommand();
|
|
// Create quota with no space left
|
|
var quota = new UserStorageQuota(TestUserId, maxStorageBytes: TestFileSize - 1);
|
|
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Error.Should().Contain("quota");
|
|
result.UploadUrl.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_ValidQuota_ChecksQuotaRepository()
|
|
{
|
|
// Arrange
|
|
var command = CreateValidCommand();
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("https://example.com/upload");
|
|
|
|
// Act
|
|
await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
await _quotaRepository.Received(1).GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Object Key Generation Tests
|
|
|
|
[Fact]
|
|
public async Task Handle_PublicAccessLevel_GeneratesPublicPrefix()
|
|
{
|
|
// Arrange
|
|
var command = new SignUploadCommand(
|
|
TestUserId,
|
|
TestFileName,
|
|
TestContentType,
|
|
TestFileSize,
|
|
FileAccessLevel.Public);
|
|
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("https://example.com/upload");
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.ObjectKey.Should().StartWith("public/");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_PrivateAccessLevel_GeneratesPrivatePrefix()
|
|
{
|
|
// Arrange
|
|
var command = new SignUploadCommand(
|
|
TestUserId,
|
|
TestFileName,
|
|
TestContentType,
|
|
TestFileSize,
|
|
FileAccessLevel.Private);
|
|
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("https://example.com/upload");
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.ObjectKey.Should().StartWith("private/");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_SharedAccessLevel_GeneratesSharedPrefix()
|
|
{
|
|
// Arrange
|
|
var command = new SignUploadCommand(
|
|
TestUserId,
|
|
TestFileName,
|
|
TestContentType,
|
|
TestFileSize,
|
|
FileAccessLevel.Shared);
|
|
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("https://example.com/upload");
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.ObjectKey.Should().StartWith("shared/");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_ValidRequest_ObjectKeyContainsUserId()
|
|
{
|
|
// Arrange
|
|
var command = CreateValidCommand();
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("https://example.com/upload");
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.ObjectKey.Should().Contain(TestUserId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_ValidRequest_ObjectKeyContainsFileName()
|
|
{
|
|
// Arrange
|
|
var command = CreateValidCommand();
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("https://example.com/upload");
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.ObjectKey.Should().EndWith(".pdf");
|
|
result.ObjectKey.Should().Contain("document");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Exception Handling Tests
|
|
|
|
[Fact]
|
|
public async Task Handle_StorageProviderThrows_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var command = CreateValidCommand();
|
|
var quota = CreateQuotaWithSpace(MaxQuotaBytes);
|
|
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.Returns(quota);
|
|
_storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
_storageProvider.GetPreSignedUploadUrlAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new Exception("Storage connection failed"));
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Error.Should().NotBeNullOrEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_QuotaRepositoryThrows_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var command = CreateValidCommand();
|
|
|
|
_quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new Exception("Database connection failed"));
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Error.Should().NotBeNullOrEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static SignUploadCommand CreateValidCommand()
|
|
{
|
|
return new SignUploadCommand(
|
|
TestUserId,
|
|
TestFileName,
|
|
TestContentType,
|
|
TestFileSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Create a real UserStorageQuota with available space.
|
|
/// VI: Tạo real UserStorageQuota với dung lượng khả dụng.
|
|
/// </summary>
|
|
private static UserStorageQuota CreateQuotaWithSpace(long maxStorage)
|
|
{
|
|
return new UserStorageQuota(TestUserId, maxStorageBytes: maxStorage);
|
|
}
|
|
|
|
#endregion
|
|
}
|