393 lines
12 KiB
C#
393 lines
12 KiB
C#
using FluentAssertions;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
using NSubstitute.ExceptionExtensions;
|
|
using StorageService.API.Application.Commands.FileShare;
|
|
using StorageService.Domain.AggregatesModel.FileAggregate;
|
|
using StorageService.Domain.AggregatesModel.FileShareAggregate;
|
|
using StorageService.Domain.SeedWork;
|
|
using Xunit;
|
|
|
|
// EN: Alias to avoid collision with System.IO.FileShare
|
|
// VI: Alias để tránh xung đột với System.IO.FileShare
|
|
using DomainFileShare = StorageService.Domain.AggregatesModel.FileShareAggregate.FileShare;
|
|
|
|
namespace StorageService.UnitTests.Handlers;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for CreateFileShareCommandHandler.
|
|
/// VI: Unit tests cho CreateFileShareCommandHandler.
|
|
/// </summary>
|
|
public class FileShareCommandHandlerTests
|
|
{
|
|
private readonly IFileRepository _fileRepository;
|
|
private readonly IFileShareRepository _fileShareRepository;
|
|
private readonly ILogger<CreateFileShareCommandHandler> _createLogger;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly CreateFileShareCommandHandler _createHandler;
|
|
|
|
private static readonly Guid TestFileId = Guid.NewGuid();
|
|
private const string TestUserId = "user-123";
|
|
private const string TestBaseUrl = "https://storage.example.com";
|
|
|
|
public FileShareCommandHandlerTests()
|
|
{
|
|
_fileRepository = Substitute.For<IFileRepository>();
|
|
_fileShareRepository = Substitute.For<IFileShareRepository>();
|
|
_createLogger = Substitute.For<ILogger<CreateFileShareCommandHandler>>();
|
|
_unitOfWork = Substitute.For<IUnitOfWork>();
|
|
|
|
var configData = new Dictionary<string, string?>
|
|
{
|
|
{ "App:BaseUrl", TestBaseUrl }
|
|
};
|
|
_configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(configData)
|
|
.Build();
|
|
|
|
_fileShareRepository.UnitOfWork.Returns(_unitOfWork);
|
|
|
|
_createHandler = new CreateFileShareCommandHandler(
|
|
_fileRepository,
|
|
_fileShareRepository,
|
|
_createLogger,
|
|
_configuration);
|
|
}
|
|
|
|
#region CreateShare Tests
|
|
|
|
[Fact]
|
|
public async Task CreateShare_ValidRequest_CreatesShare()
|
|
{
|
|
// Arrange
|
|
var file = CreateRealStorageFile();
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
|
|
|
|
// Act
|
|
var result = await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
result.ShareId.Should().NotBeNull();
|
|
result.ShareToken.Should().NotBeNullOrEmpty();
|
|
result.ShareUrl.Should().StartWith(TestBaseUrl);
|
|
result.Error.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_ValidRequest_SavesShareToRepository()
|
|
{
|
|
// Arrange
|
|
var file = CreateRealStorageFile();
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
|
|
|
|
// Act
|
|
await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
await _fileShareRepository.Received(1).AddAsync(
|
|
Arg.Is<DomainFileShare>(s =>
|
|
s.FileId == file.Id &&
|
|
s.SharedBy == TestUserId),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_WithPassword_CreatesProtectedShare()
|
|
{
|
|
// Arrange
|
|
var file = CreateRealStorageFile();
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download,
|
|
Password: "SecretP@ss123");
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
|
|
|
|
DomainFileShare? capturedShare = null;
|
|
await _fileShareRepository.AddAsync(
|
|
Arg.Do<DomainFileShare>(s => capturedShare = s),
|
|
Arg.Any<CancellationToken>());
|
|
|
|
// Act
|
|
await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
capturedShare.Should().NotBeNull();
|
|
capturedShare!.PasswordHash.Should().NotBeNullOrEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_WithExpiration_SetsExpiresAt()
|
|
{
|
|
// Arrange
|
|
var file = CreateRealStorageFile();
|
|
var expiresAt = DateTime.UtcNow.AddDays(7);
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download,
|
|
ExpiresAt: expiresAt);
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
|
|
|
|
DomainFileShare? capturedShare = null;
|
|
await _fileShareRepository.AddAsync(
|
|
Arg.Do<DomainFileShare>(s => capturedShare = s),
|
|
Arg.Any<CancellationToken>());
|
|
|
|
// Act
|
|
await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
capturedShare.Should().NotBeNull();
|
|
capturedShare!.ExpiresAt.Should().Be(expiresAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_WithMaxDownloads_SetsLimit()
|
|
{
|
|
// Arrange
|
|
var file = CreateRealStorageFile();
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download,
|
|
MaxDownloads: 10);
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
|
|
|
|
DomainFileShare? capturedShare = null;
|
|
await _fileShareRepository.AddAsync(
|
|
Arg.Do<DomainFileShare>(s => capturedShare = s),
|
|
Arg.Any<CancellationToken>());
|
|
|
|
// Act
|
|
await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
capturedShare.Should().NotBeNull();
|
|
capturedShare!.MaxDownloads.Should().Be(10);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region File Not Found Tests
|
|
|
|
[Fact]
|
|
public async Task CreateShare_FileNotFound_ReturnsError()
|
|
{
|
|
// Arrange
|
|
var command = new CreateFileShareCommand(
|
|
TestFileId,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
|
|
.Returns((StorageFile?)null);
|
|
|
|
// Act
|
|
var result = await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Error.Should().Contain("not found");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_FileNotFound_DoesNotCreateShare()
|
|
{
|
|
// Arrange
|
|
var command = new CreateFileShareCommand(
|
|
TestFileId,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
|
|
.Returns((StorageFile?)null);
|
|
|
|
// Act
|
|
await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
await _fileShareRepository.DidNotReceive().AddAsync(
|
|
Arg.Any<DomainFileShare>(), Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Permission Tests
|
|
|
|
[Fact]
|
|
public async Task CreateShare_NotOwner_ReturnsError()
|
|
{
|
|
// Arrange
|
|
var file = new StorageFile(
|
|
"document.pdf",
|
|
"bucket",
|
|
"key",
|
|
"application/pdf",
|
|
1024,
|
|
"different-user", // Different owner
|
|
StorageProvider.MinIO);
|
|
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
|
|
// Act
|
|
var result = await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Error.Should().Contain("permission");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_NotOwner_DoesNotCreateShare()
|
|
{
|
|
// Arrange
|
|
var file = new StorageFile(
|
|
"document.pdf",
|
|
"bucket",
|
|
"key",
|
|
"application/pdf",
|
|
1024,
|
|
"different-user",
|
|
StorageProvider.MinIO);
|
|
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
|
|
// Act
|
|
await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
await _fileShareRepository.DidNotReceive().AddAsync(
|
|
Arg.Any<DomainFileShare>(), Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ShareUrl Generation Tests
|
|
|
|
[Fact]
|
|
public async Task CreateShare_GeneratesCorrectShareUrl()
|
|
{
|
|
// Arrange
|
|
var file = CreateRealStorageFile();
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
|
|
|
|
// Act
|
|
var result = await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.ShareUrl.Should().StartWith(TestBaseUrl);
|
|
result.ShareUrl.Should().Contain("/api/v1/storage/shares/public/");
|
|
result.ShareUrl.Should().Contain(result.ShareToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Exception Handling Tests
|
|
|
|
[Fact]
|
|
public async Task CreateShare_RepositoryThrows_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var command = new CreateFileShareCommand(
|
|
TestFileId,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new Exception("Database connection failed"));
|
|
|
|
// Act
|
|
var result = await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Error.Should().NotBeNullOrEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_SaveChangesThrows_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var file = CreateRealStorageFile();
|
|
var command = new CreateFileShareCommand(
|
|
file.Id,
|
|
TestUserId,
|
|
SharePermission.Download);
|
|
|
|
_fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
|
|
.Returns(file);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new Exception("Save failed"));
|
|
|
|
// Act
|
|
var result = await _createHandler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static StorageFile CreateRealStorageFile()
|
|
{
|
|
return new StorageFile(
|
|
"document.pdf",
|
|
"storage-bucket",
|
|
"private/user-123/20260115/abc123_document.pdf",
|
|
"application/pdf",
|
|
1024,
|
|
TestUserId,
|
|
StorageProvider.MinIO);
|
|
}
|
|
|
|
#endregion
|
|
}
|