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

401 lines
13 KiB
C#

using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Admin.Business.Models;
using HoneyBox.Admin.Business.Models.Config;
using HoneyBox.Admin.Business.Models.Upload;
using HoneyBox.Admin.Business.Services;
using HoneyBox.Admin.Business.Services.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace HoneyBox.Tests.Services;
/// <summary>
/// UploadService 单元测试
/// </summary>
public class UploadServiceTests
{
private readonly Mock<IAdminConfigService> _mockConfigService;
private readonly Mock<IStorageProvider> _mockLocalProvider;
private readonly Mock<IStorageProvider> _mockCosProvider;
private readonly Mock<ILogger<UploadService>> _mockLogger;
private readonly UploadService _service;
public UploadServiceTests()
{
_mockConfigService = new Mock<IAdminConfigService>();
_mockLocalProvider = new Mock<IStorageProvider>();
_mockCosProvider = new Mock<IStorageProvider>();
_mockLogger = new Mock<ILogger<UploadService>>();
// 设置存储提供者类型
_mockLocalProvider.Setup(p => p.StorageType).Returns("1");
_mockCosProvider.Setup(p => p.StorageType).Returns("3");
var providers = new List<IStorageProvider> { _mockLocalProvider.Object, _mockCosProvider.Object };
_service = new UploadService(_mockConfigService.Object, providers, _mockLogger.Object);
}
#region File Validation Tests
[Fact]
public void ValidateFile_NullFile_ReturnsError()
{
// Act
var result = UploadService.ValidateFile(null);
// Assert
Assert.Equal("请选择要上传的文件", result);
}
[Fact]
public void ValidateFile_EmptyFile_ReturnsError()
{
// Arrange
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.Length).Returns(0);
// Act
var result = UploadService.ValidateFile(mockFile.Object);
// Assert
Assert.Equal("请选择要上传的文件", result);
}
[Fact]
public void ValidateFile_FileTooLarge_ReturnsError()
{
// Arrange
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.Length).Returns(11 * 1024 * 1024); // 11MB
mockFile.Setup(f => f.FileName).Returns("test.jpg");
mockFile.Setup(f => f.ContentType).Returns("image/jpeg");
// Act
var result = UploadService.ValidateFile(mockFile.Object);
// Assert
Assert.Equal("文件大小不能超过10MB", result);
}
[Fact]
public void ValidateFile_InvalidExtension_ReturnsError()
{
// Arrange
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.Length).Returns(1024);
mockFile.Setup(f => f.FileName).Returns("test.txt");
mockFile.Setup(f => f.ContentType).Returns("text/plain");
// Act
var result = UploadService.ValidateFile(mockFile.Object);
// Assert
Assert.Equal("只支持 jpg、jpeg、png、gif、webp 格式的图片", result);
}
[Theory]
[InlineData("test.jpg", "image/jpeg")]
[InlineData("test.jpeg", "image/jpeg")]
[InlineData("test.png", "image/png")]
[InlineData("test.gif", "image/gif")]
[InlineData("test.webp", "image/webp")]
public void ValidateFile_ValidImageFormats_ReturnsNull(string fileName, string contentType)
{
// Arrange
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.Length).Returns(1024);
mockFile.Setup(f => f.FileName).Returns(fileName);
mockFile.Setup(f => f.ContentType).Returns(contentType);
// Act
var result = UploadService.ValidateFile(mockFile.Object);
// Assert
Assert.Null(result);
}
[Fact]
public void ValidateFile_MaxSizeExactly_ReturnsNull()
{
// Arrange - 正好10MB
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.Length).Returns(10 * 1024 * 1024);
mockFile.Setup(f => f.FileName).Returns("test.jpg");
mockFile.Setup(f => f.ContentType).Returns("image/jpeg");
// Act
var result = UploadService.ValidateFile(mockFile.Object);
// Assert
Assert.Null(result);
}
#endregion
#region Extension Validation Tests
[Theory]
[InlineData(".jpg", true)]
[InlineData(".jpeg", true)]
[InlineData(".png", true)]
[InlineData(".gif", true)]
[InlineData(".webp", true)]
[InlineData(".JPG", true)]
[InlineData(".JPEG", true)]
[InlineData(".PNG", true)]
[InlineData(".txt", false)]
[InlineData(".pdf", false)]
[InlineData(".exe", false)]
[InlineData("", false)]
[InlineData(null, false)]
public void IsValidExtension_ReturnsExpectedResult(string? extension, bool expected)
{
// Act
var result = UploadService.IsValidExtension(extension);
// Assert
Assert.Equal(expected, result);
}
#endregion
#region File Size Validation Tests
[Theory]
[InlineData(1, true)]
[InlineData(1024, true)]
[InlineData(1024 * 1024, true)]
[InlineData(10 * 1024 * 1024, true)]
[InlineData(10 * 1024 * 1024 + 1, false)]
[InlineData(0, false)]
[InlineData(-1, false)]
public void IsValidFileSize_ReturnsExpectedResult(long fileSize, bool expected)
{
// Act
var result = UploadService.IsValidFileSize(fileSize);
// Assert
Assert.Equal(expected, result);
}
#endregion
#region Unique FileName Tests
[Fact]
public void GenerateUniqueFileName_PreservesExtension()
{
// Arrange
var originalFileName = "test.jpg";
// Act
var uniqueName = UploadService.GenerateUniqueFileName(originalFileName);
// Assert
Assert.EndsWith(".jpg", uniqueName);
}
[Fact]
public void GenerateUniqueFileName_NormalizesExtensionToLowercase()
{
// Arrange
var originalFileName = "test.JPG";
// Act
var uniqueName = UploadService.GenerateUniqueFileName(originalFileName);
// Assert
Assert.EndsWith(".jpg", uniqueName);
}
[Fact]
public void GenerateUniqueFileName_GeneratesDifferentNames()
{
// Arrange
var originalFileName = "test.jpg";
// Act
var name1 = UploadService.GenerateUniqueFileName(originalFileName);
var name2 = UploadService.GenerateUniqueFileName(originalFileName);
// Assert
Assert.NotEqual(name1, name2);
}
#endregion
#region Storage Provider Selection Tests
[Fact]
public async Task UploadImageAsync_UsesLocalStorageByDefault()
{
// Arrange
_mockConfigService.Setup(c => c.GetConfigAsync<UploadSetting>(ConfigKeys.Uploads))
.ReturnsAsync((UploadSetting?)null);
_mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(UploadResult.Ok("/uploads/2026/01/19/test.jpg"));
var mockFile = CreateMockFile("test.jpg", "image/jpeg", 1024);
// Act
var result = await _service.UploadImageAsync(mockFile.Object);
// Assert
_mockLocalProvider.Verify(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once);
_mockCosProvider.Verify(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Fact]
public async Task UploadImageAsync_UsesCosStorageWhenConfigured()
{
// Arrange
var uploadSetting = new UploadSetting { Type = "3" };
_mockConfigService.Setup(c => c.GetConfigAsync<UploadSetting>(ConfigKeys.Uploads))
.ReturnsAsync(uploadSetting);
_mockCosProvider.Setup(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(UploadResult.Ok("https://cdn.example.com/uploads/2026/01/19/test.jpg"));
var mockFile = CreateMockFile("test.jpg", "image/jpeg", 1024);
// Act
var result = await _service.UploadImageAsync(mockFile.Object);
// Assert
_mockCosProvider.Verify(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once);
_mockLocalProvider.Verify(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Fact]
public async Task UploadImageAsync_FallsBackToLocalStorageForInvalidType()
{
// Arrange
var uploadSetting = new UploadSetting { Type = "999" }; // 无效类型
_mockConfigService.Setup(c => c.GetConfigAsync<UploadSetting>(ConfigKeys.Uploads))
.ReturnsAsync(uploadSetting);
_mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(UploadResult.Ok("/uploads/2026/01/19/test.jpg"));
var mockFile = CreateMockFile("test.jpg", "image/jpeg", 1024);
// Act
var result = await _service.UploadImageAsync(mockFile.Object);
// Assert
_mockLocalProvider.Verify(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
#endregion
#region Upload Response Tests
[Fact]
public async Task UploadImageAsync_ReturnsCorrectResponse()
{
// Arrange
_mockConfigService.Setup(c => c.GetConfigAsync<UploadSetting>(ConfigKeys.Uploads))
.ReturnsAsync((UploadSetting?)null);
_mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(UploadResult.Ok("/uploads/2026/01/19/test.jpg"));
var mockFile = CreateMockFile("original.jpg", "image/jpeg", 2048);
// Act
var result = await _service.UploadImageAsync(mockFile.Object);
// Assert
Assert.Equal("/uploads/2026/01/19/test.jpg", result.Url);
Assert.Equal("original.jpg", result.FileName);
Assert.Equal(2048, result.FileSize);
}
[Fact]
public async Task UploadImageAsync_ThrowsOnUploadFailure()
{
// Arrange
_mockConfigService.Setup(c => c.GetConfigAsync<UploadSetting>(ConfigKeys.Uploads))
.ReturnsAsync((UploadSetting?)null);
_mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(UploadResult.Fail("磁盘空间不足"));
var mockFile = CreateMockFile("test.jpg", "image/jpeg", 1024);
// Act & Assert
var ex = await Assert.ThrowsAsync<BusinessException>(() => _service.UploadImageAsync(mockFile.Object));
Assert.Equal(BusinessErrorCodes.OperationFailed, ex.Code);
Assert.Equal("磁盘空间不足", ex.Message);
}
#endregion
#region Batch Upload Tests
[Fact]
public async Task UploadImagesAsync_UploadsAllFiles()
{
// Arrange
_mockConfigService.Setup(c => c.GetConfigAsync<UploadSetting>(ConfigKeys.Uploads))
.ReturnsAsync((UploadSetting?)null);
var uploadCount = 0;
_mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(() => UploadResult.Ok($"/uploads/2026/01/19/file{++uploadCount}.jpg"));
var files = new List<IFormFile>
{
CreateMockFile("file1.jpg", "image/jpeg", 1024).Object,
CreateMockFile("file2.jpg", "image/jpeg", 2048).Object,
CreateMockFile("file3.jpg", "image/jpeg", 3072).Object
};
// Act
var results = await _service.UploadImagesAsync(files);
// Assert
Assert.Equal(3, results.Count);
_mockLocalProvider.Verify(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string>()), Times.Exactly(3));
}
[Fact]
public async Task UploadImagesAsync_ThrowsOnEmptyList()
{
// Arrange
var files = new List<IFormFile>();
// Act & Assert
var ex = await Assert.ThrowsAsync<BusinessException>(() => _service.UploadImagesAsync(files));
Assert.Equal(BusinessErrorCodes.ValidationFailed, ex.Code);
}
[Fact]
public async Task UploadImagesAsync_ThrowsOnNullList()
{
// Act & Assert
var ex = await Assert.ThrowsAsync<BusinessException>(() => _service.UploadImagesAsync(null!));
Assert.Equal(BusinessErrorCodes.ValidationFailed, ex.Code);
}
#endregion
#region Helper Methods
private static Mock<IFormFile> CreateMockFile(string fileName, string contentType, long length)
{
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.FileName).Returns(fileName);
mockFile.Setup(f => f.ContentType).Returns(contentType);
mockFile.Setup(f => f.Length).Returns(length);
mockFile.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(new byte[length]));
return mockFile;
}
#endregion
}