Files
pos-system/services/storage-service-net/tests/StorageService.UnitTests/Handlers/FileShareCommandHandlerTests.cs

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
}