401 lines
13 KiB
C#
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
|
|
}
|