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; /// /// TencentCosProvider 单元测试 /// 注意: 由于COS SDK依赖,UploadAsync和DeleteAsync方法的测试需要在集成测试中进行 /// 这里只测试静态辅助方法和URL生成逻辑 /// 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 } /// /// TencentCosProvider 属性测试 /// **Property 4: 存储策略一致性 - COS URL格式** /// **Validates: Requirements 3.3, 3.4** /// public class TencentCosProviderPropertyTests { /// /// **Feature: image-upload-feature, Property 4: 存储策略一致性 - COS URL格式** /// *For any* 有效的Domain和objectKey,生成的URL SHALL 以配置的Domain开头 /// **Validates: Requirements 3.3** /// [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); } /// /// **Feature: image-upload-feature, Property 4: 存储策略一致性 - COS URL格式** /// *For any* 有效的Domain和objectKey,生成的URL SHALL 包含完整的对象路径 /// **Validates: Requirements 3.3** /// [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); } /// /// **Feature: image-upload-feature, Property 3: 文件名唯一性** /// *For any* 两次调用GenerateUniqueFileName,即使使用相同的原始文件名,生成的文件名 SHALL 不同 /// **Validates: Requirements 1.5** /// [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; } /// /// **Feature: image-upload-feature, Property 4: 存储策略一致性 - COS URL格式** /// *For any* 生成的文件名,SHALL 保留原始文件的扩展名(小写) /// **Validates: Requirements 3.3** /// [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); } /// /// **Feature: image-upload-feature, Property 4: 存储策略一致性 - COS URL格式** /// *For any* 生成的URL,SHALL 不包含双斜杠(除了协议部分) /// **Validates: Requirements 3.3** /// [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("//"); } }