HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/LocalStorageProviderTests.cs
2026-01-19 15:05:52 +08:00

452 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Admin.Business.Services.Storage;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace HoneyBox.Tests.Services;
/// <summary>
/// LocalStorageProvider 单元测试
/// </summary>
public class LocalStorageProviderTests : IDisposable
{
private readonly Mock<IWebHostEnvironment> _mockEnvironment;
private readonly Mock<ILogger<LocalStorageProvider>> _mockLogger;
private readonly string _testWebRootPath;
private readonly LocalStorageProvider _provider;
public LocalStorageProviderTests()
{
_mockEnvironment = new Mock<IWebHostEnvironment>();
_mockLogger = new Mock<ILogger<LocalStorageProvider>>();
// 创建临时测试目录
_testWebRootPath = Path.Combine(Path.GetTempPath(), $"LocalStorageTest_{Guid.NewGuid():N}");
Directory.CreateDirectory(_testWebRootPath);
_mockEnvironment.Setup(e => e.WebRootPath).Returns(_testWebRootPath);
_provider = new LocalStorageProvider(_mockEnvironment.Object, _mockLogger.Object);
}
public void Dispose()
{
// 清理测试目录
if (Directory.Exists(_testWebRootPath))
{
try
{
Directory.Delete(_testWebRootPath, true);
}
catch
{
// 忽略清理错误
}
}
}
#region StorageType Tests
[Fact]
public void StorageType_ShouldReturn1()
{
// Assert
Assert.Equal("1", _provider.StorageType);
}
#endregion
#region Directory Creation Tests
[Fact]
public async Task UploadAsync_ShouldCreateDateBasedDirectory()
{
// Arrange
var content = "test content"u8.ToArray();
using var stream = new MemoryStream(content);
var fileName = "test.jpg";
// Act
var result = await _provider.UploadAsync(stream, fileName, "image/jpeg");
// Assert
Assert.True(result.Success);
// 验证目录结构
var now = DateTime.Now;
var expectedDir = Path.Combine(_testWebRootPath, "uploads",
now.Year.ToString(),
now.Month.ToString("D2"),
now.Day.ToString("D2"));
Assert.True(Directory.Exists(expectedDir));
}
[Fact]
public async Task UploadAsync_ShouldCreateNestedDirectories()
{
// Arrange - 确保目录不存在
var uploadsDir = Path.Combine(_testWebRootPath, "uploads");
if (Directory.Exists(uploadsDir))
{
Directory.Delete(uploadsDir, true);
}
var content = "test content"u8.ToArray();
using var stream = new MemoryStream(content);
var fileName = "test.png";
// Act
var result = await _provider.UploadAsync(stream, fileName, "image/png");
// Assert
Assert.True(result.Success);
Assert.True(Directory.Exists(uploadsDir));
}
#endregion
#region File Save Tests
[Fact]
public async Task UploadAsync_ShouldSaveFileContent()
{
// Arrange
var expectedContent = "test file content for verification"u8.ToArray();
using var stream = new MemoryStream(expectedContent);
var fileName = "content_test.jpg";
// Act
var result = await _provider.UploadAsync(stream, fileName, "image/jpeg");
// Assert
Assert.True(result.Success);
Assert.NotNull(result.Url);
// 验证文件内容
var physicalPath = Path.Combine(_testWebRootPath, result.Url!.TrimStart('/').Replace('/', Path.DirectorySeparatorChar));
Assert.True(File.Exists(physicalPath));
var savedContent = await File.ReadAllBytesAsync(physicalPath);
Assert.Equal(expectedContent, savedContent);
}
[Fact]
public async Task UploadAsync_ShouldPreserveFileExtension()
{
// Arrange
var content = "test"u8.ToArray();
using var stream = new MemoryStream(content);
var fileName = "image.webp";
// Act
var result = await _provider.UploadAsync(stream, fileName, "image/webp");
// Assert
Assert.True(result.Success);
Assert.NotNull(result.Url);
Assert.EndsWith(".webp", result.Url);
}
[Theory]
[InlineData("test.jpg", ".jpg")]
[InlineData("test.JPEG", ".jpeg")]
[InlineData("test.PNG", ".png")]
[InlineData("test.gif", ".gif")]
[InlineData("test.WebP", ".webp")]
public async Task UploadAsync_ShouldNormalizeExtensionToLowercase(string fileName, string expectedExtension)
{
// Arrange
var content = "test"u8.ToArray();
using var stream = new MemoryStream(content);
// Act
var result = await _provider.UploadAsync(stream, fileName, "image/jpeg");
// Assert
Assert.True(result.Success);
Assert.NotNull(result.Url);
Assert.EndsWith(expectedExtension, result.Url);
}
#endregion
#region URL Format Tests
[Fact]
public async Task UploadAsync_UrlShouldStartWithUploads()
{
// Arrange
var content = "test"u8.ToArray();
using var stream = new MemoryStream(content);
var fileName = "test.jpg";
// Act
var result = await _provider.UploadAsync(stream, fileName, "image/jpeg");
// Assert
Assert.True(result.Success);
Assert.NotNull(result.Url);
Assert.StartsWith("/uploads/", result.Url);
}
[Fact]
public async Task UploadAsync_UrlShouldContainDatePath()
{
// Arrange
var content = "test"u8.ToArray();
using var stream = new MemoryStream(content);
var fileName = "test.jpg";
var now = DateTime.Now;
// Act
var result = await _provider.UploadAsync(stream, fileName, "image/jpeg");
// Assert
Assert.True(result.Success);
Assert.NotNull(result.Url);
var expectedDatePath = $"/{now.Year}/{now.Month:D2}/{now.Day:D2}/";
Assert.Contains(expectedDatePath, result.Url);
}
[Fact]
public async Task UploadAsync_UrlShouldUseForwardSlashes()
{
// Arrange
var content = "test"u8.ToArray();
using var stream = new MemoryStream(content);
var fileName = "test.jpg";
// Act
var result = await _provider.UploadAsync(stream, fileName, "image/jpeg");
// Assert
Assert.True(result.Success);
Assert.NotNull(result.Url);
Assert.DoesNotContain("\\", result.Url);
}
#endregion
#region Delete Tests
[Fact]
public async Task DeleteAsync_ShouldDeleteExistingFile()
{
// Arrange - 先上传一个文件
var content = "test"u8.ToArray();
using var stream = new MemoryStream(content);
var uploadResult = await _provider.UploadAsync(stream, "to_delete.jpg", "image/jpeg");
Assert.True(uploadResult.Success);
// Act
var deleteResult = await _provider.DeleteAsync(uploadResult.Url!);
// Assert
Assert.True(deleteResult);
var physicalPath = Path.Combine(_testWebRootPath, uploadResult.Url!.TrimStart('/').Replace('/', Path.DirectorySeparatorChar));
Assert.False(File.Exists(physicalPath));
}
[Fact]
public async Task DeleteAsync_ShouldReturnFalseForNonExistentFile()
{
// Arrange
var nonExistentUrl = "/uploads/2026/01/19/nonexistent.jpg";
// Act
var result = await _provider.DeleteAsync(nonExistentUrl);
// Assert
Assert.False(result);
}
[Fact]
public async Task DeleteAsync_ShouldReturnFalseForEmptyUrl()
{
// Act
var result = await _provider.DeleteAsync("");
// Assert
Assert.False(result);
}
[Fact]
public async Task DeleteAsync_ShouldReturnFalseForNullUrl()
{
// Act
var result = await _provider.DeleteAsync(null!);
// Assert
Assert.False(result);
}
#endregion
}
/// <summary>
/// LocalStorageProvider 属性测试
/// **Property 4: 存储策略一致性 - 本地存储URL格式**
/// **Validates: Requirements 2.3**
/// </summary>
public class LocalStorageProviderPropertyTests : IDisposable
{
private readonly string _testWebRootPath;
public LocalStorageProviderPropertyTests()
{
_testWebRootPath = Path.Combine(Path.GetTempPath(), $"LocalStoragePropertyTest_{Guid.NewGuid():N}");
Directory.CreateDirectory(_testWebRootPath);
}
public void Dispose()
{
if (Directory.Exists(_testWebRootPath))
{
try
{
Directory.Delete(_testWebRootPath, true);
}
catch
{
// 忽略清理错误
}
}
}
/// <summary>
/// **Feature: image-upload-feature, Property 4: 存储策略一致性 - 本地存储URL格式**
/// *For any* 上传操作返回的URL格式 SHALL 与本地存储类型一致URL以 `/uploads/` 开头
/// **Validates: Requirements 2.3**
/// </summary>
[Property(MaxTest = 100)]
public bool LocalStorage_UrlFormat_ShouldStartWithUploads(PositiveInt seed)
{
var mockEnvironment = new Mock<IWebHostEnvironment>();
var mockLogger = new Mock<ILogger<LocalStorageProvider>>();
var testDir = Path.Combine(_testWebRootPath, $"test_{seed.Get}");
Directory.CreateDirectory(testDir);
mockEnvironment.Setup(e => e.WebRootPath).Returns(testDir);
var provider = new LocalStorageProvider(mockEnvironment.Object, mockLogger.Object);
var extensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
var extension = extensions[seed.Get % extensions.Length];
var fileName = $"test{extension}";
var content = System.Text.Encoding.UTF8.GetBytes($"test content {seed.Get}");
using var stream = new MemoryStream(content);
var result = provider.UploadAsync(stream, fileName, "image/jpeg").GetAwaiter().GetResult();
if (!result.Success || result.Url == null)
return false;
// 验证URL以 /uploads/ 开头
return result.Url.StartsWith("/uploads/");
}
/// <summary>
/// **Feature: image-upload-feature, Property 4: 存储策略一致性 - 本地存储URL格式**
/// *For any* 上传操作返回的URL SHALL 包含日期路径格式 yyyy/MM/dd
/// **Validates: Requirements 2.2, 2.3**
/// </summary>
[Property(MaxTest = 100)]
public bool LocalStorage_UrlFormat_ShouldContainDatePath(PositiveInt seed)
{
var mockEnvironment = new Mock<IWebHostEnvironment>();
var mockLogger = new Mock<ILogger<LocalStorageProvider>>();
var testDir = Path.Combine(_testWebRootPath, $"date_test_{seed.Get}");
Directory.CreateDirectory(testDir);
mockEnvironment.Setup(e => e.WebRootPath).Returns(testDir);
var provider = new LocalStorageProvider(mockEnvironment.Object, mockLogger.Object);
var fileName = $"test_{seed.Get}.jpg";
var content = System.Text.Encoding.UTF8.GetBytes($"content {seed.Get}");
using var stream = new MemoryStream(content);
var result = provider.UploadAsync(stream, fileName, "image/jpeg").GetAwaiter().GetResult();
if (!result.Success || result.Url == null)
return false;
var now = DateTime.Now;
var expectedDatePath = $"/{now.Year}/{now.Month:D2}/{now.Day:D2}/";
return result.Url.Contains(expectedDatePath);
}
/// <summary>
/// **Feature: image-upload-feature, Property 3: 文件名唯一性**
/// *For any* 两次上传操作,即使上传相同的文件,生成的文件名 SHALL 不同
/// **Validates: Requirements 1.5**
/// </summary>
[Property(MaxTest = 50)]
public bool LocalStorage_FileNames_ShouldBeUnique(PositiveInt seed)
{
var mockEnvironment = new Mock<IWebHostEnvironment>();
var mockLogger = new Mock<ILogger<LocalStorageProvider>>();
var testDir = Path.Combine(_testWebRootPath, $"unique_test_{seed.Get}");
Directory.CreateDirectory(testDir);
mockEnvironment.Setup(e => e.WebRootPath).Returns(testDir);
var provider = new LocalStorageProvider(mockEnvironment.Object, mockLogger.Object);
var fileName = "same_file.jpg";
var content = "same content"u8.ToArray();
// 第一次上传
using var stream1 = new MemoryStream(content);
var result1 = provider.UploadAsync(stream1, fileName, "image/jpeg").GetAwaiter().GetResult();
// 第二次上传(相同文件名)
using var stream2 = new MemoryStream(content);
var result2 = provider.UploadAsync(stream2, fileName, "image/jpeg").GetAwaiter().GetResult();
if (!result1.Success || !result2.Success)
return false;
// 验证两次上传的URL不同
return result1.Url != result2.Url;
}
/// <summary>
/// **Feature: image-upload-feature, Property 4: 存储策略一致性**
/// *For any* 成功上传的文件URL对应的物理文件 SHALL 存在
/// **Validates: Requirements 2.3**
/// </summary>
[Property(MaxTest = 50)]
public bool LocalStorage_UploadedFile_ShouldExistOnDisk(PositiveInt seed)
{
var mockEnvironment = new Mock<IWebHostEnvironment>();
var mockLogger = new Mock<ILogger<LocalStorageProvider>>();
var testDir = Path.Combine(_testWebRootPath, $"exist_test_{seed.Get}");
Directory.CreateDirectory(testDir);
mockEnvironment.Setup(e => e.WebRootPath).Returns(testDir);
var provider = new LocalStorageProvider(mockEnvironment.Object, mockLogger.Object);
var fileName = $"file_{seed.Get}.jpg";
var content = System.Text.Encoding.UTF8.GetBytes($"content for {seed.Get}");
using var stream = new MemoryStream(content);
var result = provider.UploadAsync(stream, fileName, "image/jpeg").GetAwaiter().GetResult();
if (!result.Success || result.Url == null)
return false;
// 验证物理文件存在
var physicalPath = Path.Combine(testDir, result.Url.TrimStart('/').Replace('/', Path.DirectorySeparatorChar));
return File.Exists(physicalPath);
}
}