452 lines
14 KiB
C#
452 lines
14 KiB
C#
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);
|
||
}
|
||
}
|