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); } }