using FsCheck; using FsCheck.Xunit; using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Config; using MiAssessment.Admin.Business.Models.Upload; using MiAssessment.Admin.Business.Services; using MiAssessment.Admin.Business.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace MiAssessment.Tests.Services; /// /// UploadService 单元测试 /// public class UploadServiceTests { private readonly Mock _mockConfigService; private readonly Mock _mockLocalProvider; private readonly Mock _mockCosProvider; private readonly Mock> _mockLogger; private readonly UploadService _service; public UploadServiceTests() { _mockConfigService = new Mock(); _mockLocalProvider = new Mock(); _mockCosProvider = new Mock(); _mockLogger = new Mock>(); // 设置存储提供者类型 _mockLocalProvider.Setup(p => p.StorageType).Returns("1"); _mockCosProvider.Setup(p => p.StorageType).Returns("3"); var providers = new List { _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(); 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(); 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(); 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(); 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(); 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(ConfigKeys.Uploads)) .ReturnsAsync((UploadSetting?)null); _mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) .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(), It.IsAny(), It.IsAny()), Times.Once); _mockCosProvider.Verify(p => p.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task UploadImageAsync_UsesCosStorageWhenConfigured() { // Arrange var uploadSetting = new UploadSetting { Type = "3" }; _mockConfigService.Setup(c => c.GetConfigAsync(ConfigKeys.Uploads)) .ReturnsAsync(uploadSetting); _mockCosProvider.Setup(p => p.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) .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(), It.IsAny(), It.IsAny()), Times.Once); _mockLocalProvider.Verify(p => p.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task UploadImageAsync_FallsBackToLocalStorageForInvalidType() { // Arrange var uploadSetting = new UploadSetting { Type = "999" }; // 无效类型 _mockConfigService.Setup(c => c.GetConfigAsync(ConfigKeys.Uploads)) .ReturnsAsync(uploadSetting); _mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) .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(), It.IsAny(), It.IsAny()), Times.Once); } #endregion #region Upload Response Tests [Fact] public async Task UploadImageAsync_ReturnsCorrectResponse() { // Arrange _mockConfigService.Setup(c => c.GetConfigAsync(ConfigKeys.Uploads)) .ReturnsAsync((UploadSetting?)null); _mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) .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(ConfigKeys.Uploads)) .ReturnsAsync((UploadSetting?)null); _mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(UploadResult.Fail("磁盘空间不足")); var mockFile = CreateMockFile("test.jpg", "image/jpeg", 1024); // Act & Assert var ex = await Assert.ThrowsAsync(() => _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(ConfigKeys.Uploads)) .ReturnsAsync((UploadSetting?)null); var uploadCount = 0; _mockLocalProvider.Setup(p => p.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => UploadResult.Ok($"/uploads/2026/01/19/file{++uploadCount}.jpg")); var files = new List { 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(), It.IsAny(), It.IsAny()), Times.Exactly(3)); } [Fact] public async Task UploadImagesAsync_ThrowsOnEmptyList() { // Arrange var files = new List(); // Act & Assert var ex = await Assert.ThrowsAsync(() => _service.UploadImagesAsync(files)); Assert.Equal(BusinessErrorCodes.ValidationFailed, ex.Code); } [Fact] public async Task UploadImagesAsync_ThrowsOnNullList() { // Act & Assert var ex = await Assert.ThrowsAsync(() => _service.UploadImagesAsync(null!)); Assert.Equal(BusinessErrorCodes.ValidationFailed, ex.Code); } #endregion #region Helper Methods private static Mock CreateMockFile(string fileName, string contentType, long length) { var mockFile = new Mock(); 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 }