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; /// /// EN: Unit tests for CreateFileShareCommandHandler and RevokeFileShareCommandHandler. /// VI: Unit tests cho CreateFileShareCommandHandler và RevokeFileShareCommandHandler. /// public class FileShareCommandHandlerTests { private readonly IFileRepository _fileRepository; private readonly IFileShareRepository _fileShareRepository; private readonly ILogger _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() { // EN: Setup mocks / VI: Setup mocks _fileRepository = Substitute.For(); _fileShareRepository = Substitute.For(); _createLogger = Substitute.For>(); _unitOfWork = Substitute.For(); // EN: Setup configuration / VI: Setup configuration var configData = new Dictionary { { "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 = CreateMockStorageFile(); var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .Returns(file); _unitOfWork.SaveEntitiesAsync(Arg.Any()).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 = CreateMockStorageFile(); var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .Returns(file); _unitOfWork.SaveEntitiesAsync(Arg.Any()).Returns(true); // Act await _createHandler.Handle(command, CancellationToken.None); // Assert await _fileShareRepository.Received(1).AddAsync( Arg.Is(s => s.FileId == TestFileId && s.SharedBy == TestUserId), Arg.Any()); } [Fact] public async Task CreateShare_WithPassword_CreatesProtectedShare() { // Arrange var file = CreateMockStorageFile(); var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download, Password: "SecretP@ss123"); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .Returns(file); _unitOfWork.SaveEntitiesAsync(Arg.Any()).Returns(true); DomainFileShare? capturedShare = null; await _fileShareRepository.AddAsync( Arg.Do(s => capturedShare = s), Arg.Any()); // 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 = CreateMockStorageFile(); var expiresAt = DateTime.UtcNow.AddDays(7); var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download, ExpiresAt: expiresAt); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .Returns(file); _unitOfWork.SaveEntitiesAsync(Arg.Any()).Returns(true); DomainFileShare? capturedShare = null; await _fileShareRepository.AddAsync( Arg.Do(s => capturedShare = s), Arg.Any()); // 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 = CreateMockStorageFile(); var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download, MaxDownloads: 10); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .Returns(file); _unitOfWork.SaveEntitiesAsync(Arg.Any()).Returns(true); DomainFileShare? capturedShare = null; await _fileShareRepository.AddAsync( Arg.Do(s => capturedShare = s), Arg.Any()); // 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()) .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()) .Returns((StorageFile?)null); // Act await _createHandler.Handle(command, CancellationToken.None); // Assert await _fileShareRepository.DidNotReceive().AddAsync( Arg.Any(), Arg.Any()); } #endregion #region Permission Tests [Fact] public async Task CreateShare_NotOwner_ReturnsError() { // Arrange var file = CreateMockStorageFile(); file.UserId.Returns("different-user"); // Different owner var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .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 = CreateMockStorageFile(); file.UserId.Returns("different-user"); var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .Returns(file); // Act await _createHandler.Handle(command, CancellationToken.None); // Assert await _fileShareRepository.DidNotReceive().AddAsync( Arg.Any(), Arg.Any()); } #endregion #region ShareUrl Generation Tests [Fact] public async Task CreateShare_GeneratesCorrectShareUrl() { // Arrange var file = CreateMockStorageFile(); var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .Returns(file); _unitOfWork.SaveEntitiesAsync(Arg.Any()).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()) .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 = CreateMockStorageFile(); var command = new CreateFileShareCommand( TestFileId, TestUserId, SharePermission.Download); _fileRepository.GetByIdAsync(TestFileId, Arg.Any()) .Returns(file); _unitOfWork.SaveEntitiesAsync(Arg.Any()) .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 CreateMockStorageFile() { var file = Substitute.For(); file.Id.Returns(TestFileId); file.UserId.Returns(TestUserId); file.FileName.Returns("document.pdf"); file.IsDeleted.Returns(false); return file; } #endregion }