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

339 lines
11 KiB
C#

using FluentAssertions;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using StorageService.API.Application.Commands;
using StorageService.Domain.AggregatesModel.FileAggregate;
using StorageService.Domain.AggregatesModel.QuotaAggregate;
using StorageService.Domain.SeedWork;
using StorageService.Infrastructure.Caching;
using StorageService.Infrastructure.Storage;
using Xunit;
namespace StorageService.UnitTests.Handlers;
/// <summary>
/// EN: Unit tests for DeleteFileCommandHandler.
/// VI: Unit tests cho DeleteFileCommandHandler.
/// </summary>
public class DeleteFileCommandHandlerTests
{
private readonly IFileRepository _fileRepository;
private readonly IQuotaRepository _quotaRepository;
private readonly IStorageProviderFactory _storageProviderFactory;
private readonly IStorageProvider _storageProvider;
private readonly IRedisCacheService _cache;
private readonly ILogger<DeleteFileCommandHandler> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly DeleteFileCommandHandler _handler;
private static readonly Guid TestFileId = Guid.NewGuid();
private const string TestUserId = "user-123";
private const string TestBucketName = "storage-bucket";
private const string TestObjectKey = "private/user-123/20260115/abc123_document.pdf";
private const long TestFileSize = 1024 * 1024; // 1MB
public DeleteFileCommandHandlerTests()
{
// EN: Setup mocks / VI: Setup mocks
_fileRepository = Substitute.For<IFileRepository>();
_quotaRepository = Substitute.For<IQuotaRepository>();
_storageProviderFactory = Substitute.For<IStorageProviderFactory>();
_storageProvider = Substitute.For<IStorageProvider>();
_cache = Substitute.For<IRedisCacheService>();
_logger = Substitute.For<ILogger<DeleteFileCommandHandler>>();
_unitOfWork = Substitute.For<IUnitOfWork>();
_storageProviderFactory.GetProvider(Arg.Any<StorageProvider>()).Returns(_storageProvider);
_fileRepository.UnitOfWork.Returns(_unitOfWork);
_handler = new DeleteFileCommandHandler(
_fileRepository,
_quotaRepository,
_storageProviderFactory,
_cache,
_logger);
}
#region Happy Path Tests
[Fact]
public async Task Handle_FileExists_SoftDeletesFile()
{
// Arrange
var file = CreateMockStorageFile();
var quota = CreateMockQuota();
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns(file);
_quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
.Returns(quota);
_storageProvider.DeleteAsync(TestBucketName, TestObjectKey, Arg.Any<CancellationToken>())
.Returns(true);
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Success.Should().BeTrue();
result.Error.Should().BeNull();
file.Received(1).Delete();
_fileRepository.Received(1).Update(file);
}
[Fact]
public async Task Handle_ValidDelete_UpdatesQuota()
{
// Arrange
var file = CreateMockStorageFile();
var quota = CreateMockQuota();
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns(file);
_quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
.Returns(quota);
_storageProvider.DeleteAsync(TestBucketName, TestObjectKey, Arg.Any<CancellationToken>())
.Returns(true);
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
// Act
await _handler.Handle(command, CancellationToken.None);
// Assert
quota.Received(1).RemoveUsage(TestFileSize);
_quotaRepository.Received(1).Update(quota);
}
[Fact]
public async Task Handle_ValidDelete_InvalidatesCache()
{
// Arrange
var file = CreateMockStorageFile();
var quota = CreateMockQuota();
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns(file);
_quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
.Returns(quota);
_storageProvider.DeleteAsync(TestBucketName, TestObjectKey, Arg.Any<CancellationToken>())
.Returns(true);
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
// Act
await _handler.Handle(command, CancellationToken.None);
// Assert
// EN: Should invalidate file metadata cache
// VI: Nên invalidate file metadata cache
await _cache.Received().DeleteAsync(
Arg.Is<string>(key => key.Contains(TestFileId.ToString())),
Arg.Any<CancellationToken>());
}
#endregion
#region File Not Found Tests
[Fact]
public async Task Handle_FileNotFound_ReturnsNotFoundError()
{
// Arrange
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns((StorageFile?)null);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("not found");
}
[Fact]
public async Task Handle_FileNotFound_DoesNotDeleteFromStorage()
{
// Arrange
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns((StorageFile?)null);
// Act
await _handler.Handle(command, CancellationToken.None);
// Assert
await _storageProvider.DidNotReceive().DeleteAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
}
#endregion
#region Permission Tests
[Fact]
public async Task Handle_NotOwner_ReturnsPermissionError()
{
// Arrange
var file = CreateMockStorageFile();
file.UserId.Returns("different-user"); // Different owner
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns(file);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("permission");
}
[Fact]
public async Task Handle_NotOwner_DoesNotDeleteFile()
{
// Arrange
var file = CreateMockStorageFile();
file.UserId.Returns("different-user");
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns(file);
// Act
await _handler.Handle(command, CancellationToken.None);
// Assert
file.DidNotReceive().Delete();
await _storageProvider.DidNotReceive().DeleteAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
}
#endregion
#region Storage Delete Failure Tests
[Fact]
public async Task Handle_StorageDeleteFails_StillSoftDeletesFile()
{
// Arrange
var file = CreateMockStorageFile();
var quota = CreateMockQuota();
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns(file);
_quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
.Returns(quota);
_storageProvider.DeleteAsync(TestBucketName, TestObjectKey, Arg.Any<CancellationToken>())
.Returns(false); // Storage delete fails
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
// EN: Should still succeed with soft delete even if storage delete fails
// VI: Vẫn nên thành công với soft delete ngay cả khi storage delete thất bại
result.Success.Should().BeTrue();
file.Received(1).Delete();
}
#endregion
#region Quota Not Found Tests
[Fact]
public async Task Handle_QuotaNotFound_StillDeletesFile()
{
// Arrange
var file = CreateMockStorageFile();
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns(file);
_quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
.Returns((UserStorageQuota?)null);
_storageProvider.DeleteAsync(TestBucketName, TestObjectKey, Arg.Any<CancellationToken>())
.Returns(true);
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Success.Should().BeTrue();
file.Received(1).Delete();
}
#endregion
#region Exception Handling Tests
[Fact]
public async Task Handle_RepositoryThrows_ReturnsFailure()
{
// Arrange
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, 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();
}
[Fact]
public async Task Handle_StorageProviderThrows_ReturnsFailure()
{
// Arrange
var file = CreateMockStorageFile();
var command = new DeleteFileCommand(TestFileId, TestUserId);
_fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
.Returns(file);
_storageProvider.DeleteAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("Storage connection failed"));
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Success.Should().BeFalse();
}
#endregion
#region Helper Methods
private static StorageFile CreateMockStorageFile()
{
var file = Substitute.For<StorageFile>();
file.Id.Returns(TestFileId);
file.UserId.Returns(TestUserId);
file.BucketName.Returns(TestBucketName);
file.ObjectKey.Returns(TestObjectKey);
file.FileSizeBytes.Returns(TestFileSize);
file.Provider.Returns(StorageProvider.MinIO);
return file;
}
private static UserStorageQuota CreateMockQuota()
{
var quota = Substitute.For<UserStorageQuota>();
return quota;
}
#endregion
}