386 lines
10 KiB
C#
386 lines
10 KiB
C#
using FluentAssertions;
|
|
using StorageService.Domain.AggregatesModel.FileAggregate;
|
|
using Xunit;
|
|
|
|
namespace StorageService.UnitTests.Domain;
|
|
/// <summary>
|
|
/// EN: Tests for StorageFile aggregate root.
|
|
/// VI: Kiểm thử cho aggregate root StorageFile.
|
|
/// </summary>
|
|
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<FileUploadedDomainEvent>().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<ArgumentNullException>()
|
|
.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<ArgumentNullException>()
|
|
.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<ArgumentNullException>()
|
|
.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<ArgumentNullException>()
|
|
.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<InvalidOperationException>()
|
|
.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<FileDeletedDomainEvent>().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<ArgumentException>()
|
|
.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<ArgumentException>();
|
|
}
|
|
|
|
#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<InvalidOperationException>()
|
|
.WithMessage("*deleted*");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static StorageFile CreateValidStorageFile()
|
|
{
|
|
return new StorageFile(
|
|
ValidFileName,
|
|
ValidBucketName,
|
|
ValidObjectKey,
|
|
ValidContentType,
|
|
ValidFileSize,
|
|
ValidUserId,
|
|
StorageProvider.MinIO,
|
|
FileAccessLevel.Private);
|
|
}
|
|
|
|
#endregion
|
|
}
|