using FluentAssertions; using StorageService.Domain.AggregatesModel.FileAggregate; using Xunit; namespace StorageService.UnitTests.Domain; /// /// EN: Tests for StorageFile aggregate root. /// VI: Kiểm thử cho aggregate root StorageFile. /// public class StorageFileTests { private const string ValidFileName = "test-file.pdf"; private const string ValidBucketName = "storage-bucket"; private const string ValidObjectKey = "private/user-123/20260115/abc123_test-file.pdf"; private const string ValidContentType = "application/pdf"; private const long ValidFileSize = 1024 * 1024; // 1MB private const string ValidUserId = "user-123"; #region Constructor Tests [Fact] public void Constructor_ValidParams_CreatesFileWithCorrectProperties() { // Arrange & Act var file = new StorageFile( ValidFileName, ValidBucketName, ValidObjectKey, ValidContentType, ValidFileSize, ValidUserId, StorageProvider.MinIO, FileAccessLevel.Private, tenantId: "tenant-1", checksum: "abc123"); // Assert file.FileName.Should().Be(ValidFileName); file.BucketName.Should().Be(ValidBucketName); file.ObjectKey.Should().Be(ValidObjectKey); file.ContentType.Should().Be(ValidContentType); file.FileSizeBytes.Should().Be(ValidFileSize); file.UserId.Should().Be(ValidUserId); file.Provider.Should().Be(StorageProvider.MinIO); file.AccessLevel.Should().Be(FileAccessLevel.Private); file.TenantId.Should().Be("tenant-1"); file.Checksum.Should().Be("abc123"); file.IsDeleted.Should().BeFalse(); file.UploadedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); } [Fact] public void Constructor_ValidParams_RaisesFileUploadedDomainEvent() { // Arrange & Act var file = CreateValidStorageFile(); // Assert file.DomainEvents.Should().ContainSingle(e => e is FileUploadedDomainEvent); var domainEvent = file.DomainEvents.OfType().First(); domainEvent.FileId.Should().Be(file.Id); domainEvent.FileName.Should().Be(ValidFileName); domainEvent.UserId.Should().Be(ValidUserId); domainEvent.FileSizeBytes.Should().Be(ValidFileSize); } [Fact] public void Constructor_NullFileName_ThrowsArgumentNullException() { // Arrange & Act var act = () => new StorageFile( null!, ValidBucketName, ValidObjectKey, ValidContentType, ValidFileSize, ValidUserId, StorageProvider.MinIO); // Assert act.Should().Throw() .WithParameterName("fileName"); } [Fact] public void Constructor_NullBucketName_ThrowsArgumentNullException() { // Arrange & Act var act = () => new StorageFile( ValidFileName, null!, ValidObjectKey, ValidContentType, ValidFileSize, ValidUserId, StorageProvider.MinIO); // Assert act.Should().Throw() .WithParameterName("bucketName"); } [Fact] public void Constructor_NullObjectKey_ThrowsArgumentNullException() { // Arrange & Act var act = () => new StorageFile( ValidFileName, ValidBucketName, null!, ValidContentType, ValidFileSize, ValidUserId, StorageProvider.MinIO); // Assert act.Should().Throw() .WithParameterName("objectKey"); } [Fact] public void Constructor_NullUserId_ThrowsArgumentNullException() { // Arrange & Act var act = () => new StorageFile( ValidFileName, ValidBucketName, ValidObjectKey, ValidContentType, ValidFileSize, null!, StorageProvider.MinIO); // Assert act.Should().Throw() .WithParameterName("userId"); } [Fact] public void Constructor_NullContentType_SetsDefaultContentType() { // Arrange & Act var file = new StorageFile( ValidFileName, ValidBucketName, ValidObjectKey, null!, ValidFileSize, ValidUserId, StorageProvider.MinIO); // Assert file.ContentType.Should().Be("application/octet-stream"); } #endregion #region MarkAccessed Tests [Fact] public void MarkAccessed_UpdatesLastAccessedAt() { // Arrange var file = CreateValidStorageFile(); var beforeAccess = DateTime.UtcNow; // Act file.MarkAccessed(); // Assert file.LastAccessedAt.Should().NotBeNull(); file.LastAccessedAt.Should().BeOnOrAfter(beforeAccess); file.LastAccessedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); } [Fact] public void MarkAccessed_CalledMultipleTimes_UpdatesToLatestTime() { // Arrange var file = CreateValidStorageFile(); file.MarkAccessed(); var firstAccess = file.LastAccessedAt; // Act Thread.Sleep(10); // Small delay file.MarkAccessed(); // Assert file.LastAccessedAt.Should().BeAfter(firstAccess!.Value); } #endregion #region UpdateAccessLevel Tests [Fact] public void UpdateAccessLevel_NonDeletedFile_UpdatesAccessLevel() { // Arrange var file = CreateValidStorageFile(); file.AccessLevel.Should().Be(FileAccessLevel.Private); // Act file.UpdateAccessLevel(FileAccessLevel.Public); // Assert file.AccessLevel.Should().Be(FileAccessLevel.Public); } [Fact] public void UpdateAccessLevel_ToShared_UpdatesAccessLevel() { // Arrange var file = CreateValidStorageFile(); // Act file.UpdateAccessLevel(FileAccessLevel.Shared); // Assert file.AccessLevel.Should().Be(FileAccessLevel.Shared); } [Fact] public void UpdateAccessLevel_DeletedFile_ThrowsInvalidOperationException() { // Arrange var file = CreateValidStorageFile(); file.Delete(); // Act var act = () => file.UpdateAccessLevel(FileAccessLevel.Public); // Assert act.Should().Throw() .WithMessage("*deleted*"); } #endregion #region Delete Tests [Fact] public void Delete_SetsIsDeletedAndRaisesEvent() { // Arrange var file = CreateValidStorageFile(); file.ClearDomainEvents(); // Clear constructor event // Act file.Delete(); // Assert file.IsDeleted.Should().BeTrue(); file.DeletedAt.Should().NotBeNull(); file.DeletedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); file.DomainEvents.Should().ContainSingle(e => e is FileDeletedDomainEvent); var deletedEvent = file.DomainEvents.OfType().First(); deletedEvent.FileId.Should().Be(file.Id); deletedEvent.UserId.Should().Be(ValidUserId); deletedEvent.FileSizeBytes.Should().Be(ValidFileSize); } [Fact] public void Delete_AlreadyDeleted_DoesNothing() { // Arrange var file = CreateValidStorageFile(); file.Delete(); var firstDeletedAt = file.DeletedAt; file.ClearDomainEvents(); // Act Thread.Sleep(10); file.Delete(); // Assert file.DeletedAt.Should().Be(firstDeletedAt); file.DomainEvents.Should().BeEmpty(); // No new event raised } #endregion #region SetExpiration Tests [Fact] public void SetExpiration_FutureDate_SetsExpiresAt() { // Arrange var file = CreateValidStorageFile(); var futureDate = DateTime.UtcNow.AddDays(7); // Act file.SetExpiration(futureDate); // Assert file.ExpiresAt.Should().Be(futureDate); } [Fact] public void SetExpiration_PastDate_ThrowsArgumentException() { // Arrange var file = CreateValidStorageFile(); var pastDate = DateTime.UtcNow.AddHours(-1); // Act var act = () => file.SetExpiration(pastDate); // Assert act.Should().Throw() .WithParameterName("expiresAt") .WithMessage("*future*"); } [Fact] public void SetExpiration_CurrentTime_ThrowsArgumentException() { // Arrange var file = CreateValidStorageFile(); // Act var act = () => file.SetExpiration(DateTime.UtcNow); // Assert act.Should().Throw(); } #endregion #region UpdateFromVersion Tests [Fact] public void UpdateFromVersion_NonDeletedFile_UpdatesProperties() { // Arrange var file = CreateValidStorageFile(); var newObjectKey = "private/user-123/20260115/new_version.pdf"; var newSize = 2 * 1024 * 1024L; // 2MB var newContentType = "application/vnd.pdf"; // Act file.UpdateFromVersion(newObjectKey, newSize, newContentType); // Assert file.ObjectKey.Should().Be(newObjectKey); file.FileSizeBytes.Should().Be(newSize); file.ContentType.Should().Be(newContentType); } [Fact] public void UpdateFromVersion_DeletedFile_ThrowsInvalidOperationException() { // Arrange var file = CreateValidStorageFile(); file.Delete(); // Act var act = () => file.UpdateFromVersion("new-key", 1024, "text/plain"); // Assert act.Should().Throw() .WithMessage("*deleted*"); } #endregion #region Helper Methods private static StorageFile CreateValidStorageFile() { return new StorageFile( ValidFileName, ValidBucketName, ValidObjectKey, ValidContentType, ValidFileSize, ValidUserId, StorageProvider.MinIO, FileAccessLevel.Private); } #endregion }