306 lines
10 KiB
C#
306 lines
10 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using HoneyBox.Admin.Business.Models.Config;
|
||
using HoneyBox.Admin.Business.Services.Storage;
|
||
using Microsoft.Extensions.Logging;
|
||
using Moq;
|
||
using Xunit;
|
||
|
||
namespace HoneyBox.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// TencentCosProvider 单元测试
|
||
/// 注意: 由于COS SDK依赖,UploadAsync和DeleteAsync方法的测试需要在集成测试中进行
|
||
/// 这里只测试静态辅助方法和URL生成逻辑
|
||
/// </summary>
|
||
public class TencentCosProviderTests
|
||
{
|
||
#region URL Generation Tests
|
||
|
||
[Theory]
|
||
[InlineData("https://cdn.example.com", "uploads/2026/01/19/test.jpg", "https://cdn.example.com/uploads/2026/01/19/test.jpg")]
|
||
[InlineData("https://cdn.example.com/", "uploads/2026/01/19/test.jpg", "https://cdn.example.com/uploads/2026/01/19/test.jpg")]
|
||
[InlineData("cdn.example.com", "uploads/2026/01/19/test.jpg", "https://cdn.example.com/uploads/2026/01/19/test.jpg")]
|
||
[InlineData("http://cdn.example.com", "uploads/2026/01/19/test.jpg", "http://cdn.example.com/uploads/2026/01/19/test.jpg")]
|
||
[InlineData("https://cdn.example.com", "/uploads/2026/01/19/test.jpg", "https://cdn.example.com/uploads/2026/01/19/test.jpg")]
|
||
public void GenerateAccessUrl_ShouldFormatCorrectly(string domain, string objectKey, string expectedUrl)
|
||
{
|
||
// Act
|
||
var result = TencentCosProvider.GenerateAccessUrl(domain, objectKey);
|
||
|
||
// Assert
|
||
Assert.Equal(expectedUrl, result);
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateUniqueFileName_ShouldPreserveExtension()
|
||
{
|
||
// Arrange
|
||
var originalFileName = "test.jpg";
|
||
|
||
// Act
|
||
var result = TencentCosProvider.GenerateUniqueFileName(originalFileName);
|
||
|
||
// Assert
|
||
Assert.EndsWith(".jpg", result);
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("test.JPG", ".jpg")]
|
||
[InlineData("test.JPEG", ".jpeg")]
|
||
[InlineData("test.PNG", ".png")]
|
||
[InlineData("test.GIF", ".gif")]
|
||
[InlineData("test.WebP", ".webp")]
|
||
public void GenerateUniqueFileName_ShouldNormalizeExtensionToLowercase(string fileName, string expectedExtension)
|
||
{
|
||
// Act
|
||
var result = TencentCosProvider.GenerateUniqueFileName(fileName);
|
||
|
||
// Assert
|
||
Assert.EndsWith(expectedExtension, result);
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateUniqueFileName_ShouldGenerateDifferentNamesForSameFile()
|
||
{
|
||
// Arrange
|
||
var fileName = "same_file.jpg";
|
||
|
||
// Act
|
||
var result1 = TencentCosProvider.GenerateUniqueFileName(fileName);
|
||
Thread.Sleep(1); // 确保时间戳不同
|
||
var result2 = TencentCosProvider.GenerateUniqueFileName(fileName);
|
||
|
||
// Assert
|
||
Assert.NotEqual(result1, result2);
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateUniqueFileName_ShouldContainTimestamp()
|
||
{
|
||
// Arrange
|
||
var fileName = "test.jpg";
|
||
var now = DateTime.Now;
|
||
|
||
// Act
|
||
var result = TencentCosProvider.GenerateUniqueFileName(fileName);
|
||
|
||
// Assert
|
||
// 文件名格式: {timestamp}_{guid}.jpg
|
||
// timestamp格式: yyyyMMddHHmmssfff
|
||
Assert.Contains(now.ToString("yyyyMMdd"), result);
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateUniqueFileName_ShouldContainGuidPart()
|
||
{
|
||
// Arrange
|
||
var fileName = "test.jpg";
|
||
|
||
// Act
|
||
var result = TencentCosProvider.GenerateUniqueFileName(fileName);
|
||
|
||
// Assert
|
||
// 文件名格式: {timestamp}_{guid}.jpg
|
||
// 应该包含下划线分隔符
|
||
Assert.Contains("_", result);
|
||
// 下划线后应该有8个字符(GUID前8位)+ 扩展名
|
||
var parts = result.Split('_');
|
||
Assert.Equal(2, parts.Length);
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateAccessUrl_ShouldHandleDomainWithoutProtocol()
|
||
{
|
||
// Arrange
|
||
var domain = "cdn.example.com";
|
||
var objectKey = "uploads/test.jpg";
|
||
|
||
// Act
|
||
var result = TencentCosProvider.GenerateAccessUrl(domain, objectKey);
|
||
|
||
// Assert
|
||
Assert.StartsWith("https://", result);
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateAccessUrl_ShouldPreserveHttpProtocol()
|
||
{
|
||
// Arrange
|
||
var domain = "http://cdn.example.com";
|
||
var objectKey = "uploads/test.jpg";
|
||
|
||
// Act
|
||
var result = TencentCosProvider.GenerateAccessUrl(domain, objectKey);
|
||
|
||
// Assert
|
||
Assert.StartsWith("http://", result);
|
||
Assert.DoesNotContain("https://", result);
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateAccessUrl_ShouldRemoveTrailingSlashFromDomain()
|
||
{
|
||
// Arrange
|
||
var domain = "https://cdn.example.com/";
|
||
var objectKey = "uploads/test.jpg";
|
||
|
||
// Act
|
||
var result = TencentCosProvider.GenerateAccessUrl(domain, objectKey);
|
||
|
||
// Assert
|
||
Assert.DoesNotContain("//uploads", result);
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateAccessUrl_ShouldHandleObjectKeyWithLeadingSlash()
|
||
{
|
||
// Arrange
|
||
var domain = "https://cdn.example.com";
|
||
var objectKey = "/uploads/test.jpg";
|
||
|
||
// Act
|
||
var result = TencentCosProvider.GenerateAccessUrl(domain, objectKey);
|
||
|
||
// Assert
|
||
Assert.Equal("https://cdn.example.com/uploads/test.jpg", result);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// TencentCosProvider 属性测试
|
||
/// **Property 4: 存储策略一致性 - COS URL格式**
|
||
/// **Validates: Requirements 3.3, 3.4**
|
||
/// </summary>
|
||
public class TencentCosProviderPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: image-upload-feature, Property 4: 存储策略一致性 - COS URL格式**
|
||
/// *For any* 有效的Domain和objectKey,生成的URL SHALL 以配置的Domain开头
|
||
/// **Validates: Requirements 3.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool CosStorage_UrlFormat_ShouldStartWithDomain(PositiveInt seed)
|
||
{
|
||
// 生成测试数据
|
||
var domains = new[]
|
||
{
|
||
"https://cdn.example.com",
|
||
"https://bucket.cos.ap-guangzhou.myqcloud.com",
|
||
"cdn.test.com",
|
||
"http://cdn.local.com"
|
||
};
|
||
var domain = domains[seed.Get % domains.Length];
|
||
|
||
var objectKey = $"uploads/2026/01/{(seed.Get % 28) + 1:D2}/test_{seed.Get}.jpg";
|
||
|
||
// Act
|
||
var url = TencentCosProvider.GenerateAccessUrl(domain, objectKey);
|
||
|
||
// 验证URL以正确的协议和域名开头
|
||
var normalizedDomain = domain.TrimEnd('/');
|
||
if (!normalizedDomain.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
|
||
!normalizedDomain.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
normalizedDomain = $"https://{normalizedDomain}";
|
||
}
|
||
|
||
return url.StartsWith(normalizedDomain);
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: image-upload-feature, Property 4: 存储策略一致性 - COS URL格式**
|
||
/// *For any* 有效的Domain和objectKey,生成的URL SHALL 包含完整的对象路径
|
||
/// **Validates: Requirements 3.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool CosStorage_UrlFormat_ShouldContainObjectKey(PositiveInt seed)
|
||
{
|
||
var domain = "https://cdn.example.com";
|
||
var objectKey = $"uploads/2026/01/{(seed.Get % 28) + 1:D2}/file_{seed.Get}.jpg";
|
||
|
||
// Act
|
||
var url = TencentCosProvider.GenerateAccessUrl(domain, objectKey);
|
||
|
||
// 验证URL包含对象路径
|
||
var normalizedKey = objectKey.StartsWith('/') ? objectKey : $"/{objectKey}";
|
||
return url.EndsWith(normalizedKey) || url.Contains(objectKey);
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: image-upload-feature, Property 3: 文件名唯一性**
|
||
/// *For any* 两次调用GenerateUniqueFileName,即使使用相同的原始文件名,生成的文件名 SHALL 不同
|
||
/// **Validates: Requirements 1.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool CosStorage_FileNames_ShouldBeUnique(PositiveInt seed)
|
||
{
|
||
var extensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
|
||
var extension = extensions[seed.Get % extensions.Length];
|
||
var fileName = $"test{extension}";
|
||
|
||
// 生成两个文件名
|
||
var name1 = TencentCosProvider.GenerateUniqueFileName(fileName);
|
||
Thread.Sleep(1); // 确保时间戳不同
|
||
var name2 = TencentCosProvider.GenerateUniqueFileName(fileName);
|
||
|
||
// 验证两个文件名不同
|
||
return name1 != name2;
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: image-upload-feature, Property 4: 存储策略一致性 - COS URL格式**
|
||
/// *For any* 生成的文件名,SHALL 保留原始文件的扩展名(小写)
|
||
/// **Validates: Requirements 3.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool CosStorage_FileName_ShouldPreserveExtension(PositiveInt seed)
|
||
{
|
||
var extensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".JPG", ".PNG", ".WEBP" };
|
||
var extension = extensions[seed.Get % extensions.Length];
|
||
var fileName = $"original_{seed.Get}{extension}";
|
||
|
||
// Act
|
||
var uniqueName = TencentCosProvider.GenerateUniqueFileName(fileName);
|
||
|
||
// 验证扩展名被保留(小写)
|
||
var expectedExtension = extension.ToLowerInvariant();
|
||
return uniqueName.EndsWith(expectedExtension);
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: image-upload-feature, Property 4: 存储策略一致性 - COS URL格式**
|
||
/// *For any* 生成的URL,SHALL 不包含双斜杠(除了协议部分)
|
||
/// **Validates: Requirements 3.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool CosStorage_UrlFormat_ShouldNotContainDoubleSlashes(PositiveInt seed)
|
||
{
|
||
var domains = new[]
|
||
{
|
||
"https://cdn.example.com",
|
||
"https://cdn.example.com/",
|
||
"cdn.test.com",
|
||
"cdn.test.com/"
|
||
};
|
||
var domain = domains[seed.Get % domains.Length];
|
||
|
||
var objectKeys = new[]
|
||
{
|
||
"uploads/2026/01/19/test.jpg",
|
||
"/uploads/2026/01/19/test.jpg"
|
||
};
|
||
var objectKey = objectKeys[seed.Get % objectKeys.Length];
|
||
|
||
// Act
|
||
var url = TencentCosProvider.GenerateAccessUrl(domain, objectKey);
|
||
|
||
// 移除协议部分后检查是否有双斜杠
|
||
var urlWithoutProtocol = url.Replace("https://", "").Replace("http://", "");
|
||
return !urlWithoutProtocol.Contains("//");
|
||
}
|
||
}
|