346 lines
10 KiB
C#
346 lines
10 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StorageService.Domain.AggregatesModel.FileAggregate;
|
|
using StorageService.Domain.AggregatesModel.FileShareAggregate;
|
|
using StorageService.Infrastructure.Persistence;
|
|
|
|
namespace StorageService.FunctionalTests.ApiTests;
|
|
|
|
/// <summary>
|
|
/// EN: Functional tests for File Sharing API endpoints.
|
|
/// VI: Functional tests cho File Sharing API endpoints.
|
|
/// </summary>
|
|
public class FileSharingApiTests : IClassFixture<CustomWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client;
|
|
private readonly CustomWebApplicationFactory _factory;
|
|
|
|
public FileSharingApiTests(CustomWebApplicationFactory factory)
|
|
{
|
|
_factory = factory;
|
|
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
|
{
|
|
AllowAutoRedirect = false
|
|
});
|
|
|
|
// EN: Add mock authentication header / VI: Thêm mock authentication header
|
|
_client.DefaultRequestHeaders.Add("X-User-Id", "test-user-123");
|
|
}
|
|
|
|
#region CreateShare Tests
|
|
|
|
[Fact]
|
|
public async Task CreateShare_ValidRequest_ReturnsShareLink()
|
|
{
|
|
// Arrange
|
|
var fileId = await SeedTestFile("shareable-file.pdf");
|
|
var request = new CreateShareRequest(
|
|
FileId: fileId,
|
|
Permission: "read");
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync("/api/v1/storage/shares", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.Created);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var content = await response.Content.ReadFromJsonAsync<CreateShareResponse>();
|
|
content.Should().NotBeNull();
|
|
content!.ShareToken.Should().NotBeNullOrEmpty();
|
|
content.ShareUrl.Should().NotBeNullOrEmpty();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_WithPassword_ReturnsProtectedShare()
|
|
{
|
|
// Arrange
|
|
var fileId = await SeedTestFile("protected-file.pdf");
|
|
var request = new CreateShareRequest(
|
|
FileId: fileId,
|
|
Permission: "read",
|
|
Password: "SecretP@ss123");
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync("/api/v1/storage/shares", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.Created);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_WithExpiration_SetsExpiryDate()
|
|
{
|
|
// Arrange
|
|
var fileId = await SeedTestFile("expiring-file.pdf");
|
|
var expiresAt = DateTime.UtcNow.AddDays(7);
|
|
var request = new CreateShareRequest(
|
|
FileId: fileId,
|
|
Permission: "read",
|
|
ExpiresAt: expiresAt);
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync("/api/v1/storage/shares", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.Created);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_WithMaxDownloads_SetsLimit()
|
|
{
|
|
// Arrange
|
|
var fileId = await SeedTestFile("limited-file.pdf");
|
|
var request = new CreateShareRequest(
|
|
FileId: fileId,
|
|
Permission: "read",
|
|
MaxDownloads: 5);
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync("/api/v1/storage/shares", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.Created);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateShare_FileNotFound_Returns404()
|
|
{
|
|
// Arrange
|
|
var request = new CreateShareRequest(
|
|
FileId: Guid.NewGuid(), // Non-existent
|
|
Permission: "read");
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync("/api/v1/storage/shares", request);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.NotFound, HttpStatusCode.BadRequest);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AccessShare Tests
|
|
|
|
[Fact]
|
|
public async Task AccessShare_ValidToken_ReturnsFileInfo()
|
|
{
|
|
// Arrange
|
|
var shareToken = await SeedTestShare();
|
|
|
|
// Act
|
|
// EN: Public share access - no auth needed
|
|
// VI: Truy cập share công khai - không cần auth
|
|
var unauthClient = _factory.CreateClient(new WebApplicationFactoryClientOptions
|
|
{
|
|
AllowAutoRedirect = false
|
|
});
|
|
var response = await unauthClient.GetAsync($"/api/v1/storage/shares/public/{shareToken}");
|
|
|
|
// Assert
|
|
// EN: Should return file info or download URL
|
|
// VI: Nên trả về thông tin file hoặc download URL
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AccessShare_InvalidToken_Returns404()
|
|
{
|
|
// Arrange
|
|
var invalidToken = "invalid-share-token-12345";
|
|
|
|
// Act
|
|
var unauthClient = _factory.CreateClient();
|
|
var response = await unauthClient.GetAsync($"/api/v1/storage/shares/public/{invalidToken}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AccessShare_ExpiredToken_Returns410()
|
|
{
|
|
// Arrange
|
|
var expiredToken = await SeedTestShare(expired: true);
|
|
|
|
// Act
|
|
var unauthClient = _factory.CreateClient();
|
|
var response = await unauthClient.GetAsync($"/api/v1/storage/shares/public/{expiredToken}");
|
|
|
|
// Assert
|
|
// EN: 404 or 410 depending on implementation
|
|
// VI: 404 hoặc 410 tùy thuộc vào implementation
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.NotFound, HttpStatusCode.Gone);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region RevokeShare Tests
|
|
|
|
[Fact]
|
|
public async Task RevokeShare_ValidRequest_Returns204()
|
|
{
|
|
// Arrange
|
|
var shareId = await SeedTestShareAndGetId();
|
|
|
|
// Act
|
|
var response = await _client.DeleteAsync($"/api/v1/storage/shares/{shareId}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.NoContent, HttpStatusCode.OK);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RevokeShare_NotFound_Returns404()
|
|
{
|
|
// Arrange
|
|
var nonExistentId = Guid.NewGuid();
|
|
|
|
// Act
|
|
var response = await _client.DeleteAsync($"/api/v1/storage/shares/{nonExistentId}");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.NotFound, HttpStatusCode.BadRequest);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetUserShares Tests
|
|
|
|
[Fact]
|
|
public async Task GetUserShares_WithShares_ReturnsList()
|
|
{
|
|
// Arrange
|
|
await SeedTestShareAndGetId();
|
|
await SeedTestShareAndGetId();
|
|
|
|
// Act
|
|
var response = await _client.GetAsync("/api/v1/storage/shares");
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private async Task<Guid> SeedTestFile(string fileName)
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<StorageServiceContext>();
|
|
|
|
var file = new StorageFile(
|
|
fileName: fileName,
|
|
bucketName: "test-bucket",
|
|
objectKey: $"private/test-user-123/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid():N}_{fileName}",
|
|
contentType: "application/pdf",
|
|
fileSizeBytes: 1024,
|
|
userId: "test-user-123",
|
|
provider: StorageProvider.MinIO,
|
|
accessLevel: FileAccessLevel.Private);
|
|
|
|
context.Files.Add(file);
|
|
await context.SaveChangesAsync();
|
|
|
|
return file.Id;
|
|
}
|
|
|
|
private async Task<string> SeedTestShare(bool expired = false)
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<StorageServiceContext>();
|
|
|
|
// First create a file
|
|
var file = new StorageFile(
|
|
fileName: "share-test.pdf",
|
|
bucketName: "test-bucket",
|
|
objectKey: $"private/test-user-123/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid():N}_share-test.pdf",
|
|
contentType: "application/pdf",
|
|
fileSizeBytes: 1024,
|
|
userId: "test-user-123",
|
|
provider: StorageProvider.MinIO,
|
|
accessLevel: FileAccessLevel.Private);
|
|
|
|
context.Files.Add(file);
|
|
await context.SaveChangesAsync();
|
|
|
|
// Create share
|
|
var expiresAt = expired ? DateTime.UtcNow.AddDays(-1) : DateTime.UtcNow.AddDays(7);
|
|
var share = new FileShare(
|
|
fileId: file.Id,
|
|
sharedBy: "test-user-123",
|
|
permission: SharePermission.Read,
|
|
expiresAt: expiresAt);
|
|
|
|
context.FileShares.Add(share);
|
|
await context.SaveChangesAsync();
|
|
|
|
return share.ShareToken;
|
|
}
|
|
|
|
private async Task<Guid> SeedTestShareAndGetId()
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<StorageServiceContext>();
|
|
|
|
// First create a file
|
|
var file = new StorageFile(
|
|
fileName: "share-test.pdf",
|
|
bucketName: "test-bucket",
|
|
objectKey: $"private/test-user-123/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid():N}_share-test.pdf",
|
|
contentType: "application/pdf",
|
|
fileSizeBytes: 1024,
|
|
userId: "test-user-123",
|
|
provider: StorageProvider.MinIO,
|
|
accessLevel: FileAccessLevel.Private);
|
|
|
|
context.Files.Add(file);
|
|
await context.SaveChangesAsync();
|
|
|
|
// Create share
|
|
var share = new FileShare(
|
|
fileId: file.Id,
|
|
sharedBy: "test-user-123",
|
|
permission: SharePermission.Read);
|
|
|
|
context.FileShares.Add(share);
|
|
await context.SaveChangesAsync();
|
|
|
|
return share.Id;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region Request/Response DTOs
|
|
|
|
/// <summary>
|
|
/// EN: Create share request DTO for tests.
|
|
/// VI: DTO request tạo share cho tests.
|
|
/// </summary>
|
|
public record CreateShareRequest(
|
|
Guid FileId,
|
|
string Permission,
|
|
string? SharedWith = null,
|
|
string? Password = null,
|
|
DateTime? ExpiresAt = null,
|
|
int? MaxDownloads = null);
|
|
|
|
/// <summary>
|
|
/// EN: Create share response DTO for tests.
|
|
/// VI: DTO response tạo share cho tests.
|
|
/// </summary>
|
|
public record CreateShareResponse(
|
|
Guid ShareId,
|
|
string ShareToken,
|
|
string ShareUrl);
|
|
|
|
#endregion
|