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;
///
/// LocalStorageProvider 单元测试
///
public class LocalStorageProviderTests : IDisposable
{
private readonly Mock _mockEnvironment;
private readonly Mock> _mockLogger;
private readonly string _testWebRootPath;
private readonly LocalStorageProvider _provider;
public LocalStorageProviderTests()
{
_mockEnvironment = new Mock();
_mockLogger = new Mock>();
// 创建临时测试目录
_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
}
///
/// LocalStorageProvider 属性测试
/// **Property 4: 存储策略一致性 - 本地存储URL格式**
/// **Validates: Requirements 2.3**
///
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
{
// 忽略清理错误
}
}
}
///
/// **Feature: image-upload-feature, Property 4: 存储策略一致性 - 本地存储URL格式**
/// *For any* 上传操作,返回的URL格式 SHALL 与本地存储类型一致:URL以 `/uploads/` 开头
/// **Validates: Requirements 2.3**
///
[Property(MaxTest = 100)]
public bool LocalStorage_UrlFormat_ShouldStartWithUploads(PositiveInt seed)
{
var mockEnvironment = new Mock();
var mockLogger = new Mock>();
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/");
}
///
/// **Feature: image-upload-feature, Property 4: 存储策略一致性 - 本地存储URL格式**
/// *For any* 上传操作,返回的URL SHALL 包含日期路径格式 yyyy/MM/dd
/// **Validates: Requirements 2.2, 2.3**
///
[Property(MaxTest = 100)]
public bool LocalStorage_UrlFormat_ShouldContainDatePath(PositiveInt seed)
{
var mockEnvironment = new Mock();
var mockLogger = new Mock>();
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);
}
///
/// **Feature: image-upload-feature, Property 3: 文件名唯一性**
/// *For any* 两次上传操作,即使上传相同的文件,生成的文件名 SHALL 不同
/// **Validates: Requirements 1.5**
///
[Property(MaxTest = 50)]
public bool LocalStorage_FileNames_ShouldBeUnique(PositiveInt seed)
{
var mockEnvironment = new Mock();
var mockLogger = new Mock>();
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;
}
///
/// **Feature: image-upload-feature, Property 4: 存储策略一致性**
/// *For any* 成功上传的文件,URL对应的物理文件 SHALL 存在
/// **Validates: Requirements 2.3**
///
[Property(MaxTest = 50)]
public bool LocalStorage_UploadedFile_ShouldExistOnDisk(PositiveInt seed)
{
var mockEnvironment = new Mock();
var mockLogger = new Mock>();
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);
}
}