using FsCheck; using FsCheck.Xunit; using Xunit; using ZR.Model.Business.Dto; namespace ZR.Tests { /// /// 迁移URL更新完整性属性测试 /// **Feature: work-camera-2.0, Property 7: Migration URL Update Integrity** /// **Validates: Requirements 10.3, 10.4** /// public class MigrationUrlUpdatePropertyTests { /// /// COS域名URL /// private const string CosDomainUrl = "https://miaoyu-1308826010.cos.ap-shanghai.myqcloud.com"; /// /// COS路径前缀 /// private const string CosPrefixes = "workfiles"; /// /// 旧服务器URL前缀 /// private const string OldServerUrl = "http://old-server.com"; /// /// Property 7: Migration URL Update Integrity - Valid COS URL Validation /// /// *For any* migration update request, /// the system SHALL validate all new URLs are valid COS URLs. /// /// **Validates: Requirements 10.3, 10.4** /// [Property(MaxTest = 100)] public Property ValidCosUrl_ShouldBeAccepted() { // Generate valid COS URL components var urlGen = from year in Gen.Choose(2020, 2025) from month in Gen.Choose(1, 12) from day in Gen.Choose(1, 28) from timestamp in Gen.Choose(100000000, 999999999) from random in Gen.Choose(1000, 9999) select new { year, month, day, timestamp, random }; return Prop.ForAll( urlGen.ToArbitrary(), u => { var dateFolder = $"{u.year}{u.month:D2}"; var dayFolder = $"{u.year}{u.month:D2}{u.day:D2}"; var fileName = $"{u.timestamp}_{u.random}.jpg"; var validCosUrl = $"{CosDomainUrl}/{CosPrefixes}/{dateFolder}/{dayFolder}/当日照片/{fileName}"; var isValid = IsValidCosUrl(validCosUrl); if (!isValid) return false.Label($"Valid COS URL '{validCosUrl}' should be accepted"); return true.Label("Valid COS URL accepted"); }); } /// /// Property 7: Migration URL Update Integrity - Invalid URL Rejection /// /// *For any* URL that doesn't match the COS domain pattern, /// the system SHALL reject it. /// /// **Validates: Requirements 10.3, 10.4** /// [Property(MaxTest = 100)] public Property InvalidUrl_ShouldBeRejected() { // Generate various invalid URLs var invalidUrlGen = Gen.OneOf( // Old server URLs Gen.Constant($"{OldServerUrl}/images/test.jpg"), // Wrong domain Gen.Constant("https://other-bucket.cos.ap-shanghai.myqcloud.com/workfiles/202501/20250101/test.jpg"), // Missing prefix Gen.Constant($"{CosDomainUrl}/other/202501/20250101/test.jpg"), // HTTP instead of HTTPS Gen.Constant("http://miaoyu-1308826010.cos.ap-shanghai.myqcloud.com/workfiles/202501/20250101/test.jpg"), // Empty URL Gen.Constant(""), // Null-like Gen.Constant(" ") ); return Prop.ForAll( invalidUrlGen.ToArbitrary(), invalidUrl => { var isValid = IsValidCosUrl(invalidUrl); if (isValid) return false.Label($"Invalid URL '{invalidUrl}' should be rejected"); return true.Label("Invalid URL rejected"); }); } /// /// Property 7: Migration URL Update Integrity - URL Pair Mapping Preservation /// /// *For any* migration update request with N URL pairs, /// the system SHALL preserve the mapping between old and new URLs. /// /// **Validates: Requirements 10.3, 10.4** /// [Property(MaxTest = 100)] public Property UrlPairMapping_ShouldBePreserved() { // Generate URL pair count var pairCountGen = Gen.Choose(1, 5); return Prop.ForAll( pairCountGen.ToArbitrary(), pairCount => { var dto = new MigrationUpdateDto { RecordId = 1, ImageUrls = new List() }; // Generate URL pairs for (int i = 0; i < pairCount; i++) { var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds() + i; var random = new System.Random().Next(1000, 9999); dto.ImageUrls.Add(new MigrationUrlPair { OldUrl = $"{OldServerUrl}/images/image{i}.jpg", NewUrl = $"{CosDomainUrl}/{CosPrefixes}/202501/20250101/当日照片/{timestamp}_{random}.jpg" }); } // Verify: All pairs should have distinct old URLs var distinctOldUrls = dto.ImageUrls.Select(p => p.OldUrl).Distinct().Count(); if (distinctOldUrls != pairCount) return false.Label($"Expected {pairCount} distinct old URLs, got {distinctOldUrls}"); // Verify: All pairs should have distinct new URLs var distinctNewUrls = dto.ImageUrls.Select(p => p.NewUrl).Distinct().Count(); if (distinctNewUrls != pairCount) return false.Label($"Expected {pairCount} distinct new URLs, got {distinctNewUrls}"); // Verify: Each pair maintains its mapping for (int i = 0; i < pairCount; i++) { var pair = dto.ImageUrls[i]; if (!pair.OldUrl.Contains($"image{i}")) return false.Label($"Pair {i} old URL mapping broken"); } return true.Label($"URL pair mapping preserved for {pairCount} pairs"); }); } /// /// Property 7: Migration URL Update Integrity - All URLs Must Be Valid /// /// *For any* migration update request, /// if ANY new URL is invalid, the entire update SHALL fail. /// /// **Validates: Requirements 10.3, 10.4** /// [Property(MaxTest = 100)] public Property AllUrlsMustBeValid_OrUpdateFails() { // Generate URL pair count and position of invalid URL var testGen = from pairCount in Gen.Choose(2, 5) from invalidPosition in Gen.Choose(0, pairCount - 1) select new { pairCount, invalidPosition }; return Prop.ForAll( testGen.ToArbitrary(), t => { var dto = new MigrationUpdateDto { RecordId = 1, ImageUrls = new List() }; // Generate URL pairs with one invalid for (int i = 0; i < t.pairCount; i++) { var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds() + i; var random = new System.Random().Next(1000, 9999); var newUrl = i == t.invalidPosition ? $"{OldServerUrl}/images/invalid{i}.jpg" // Invalid URL : $"{CosDomainUrl}/{CosPrefixes}/202501/20250101/当日照片/{timestamp}_{random}.jpg"; // Valid URL dto.ImageUrls.Add(new MigrationUrlPair { OldUrl = $"{OldServerUrl}/images/image{i}.jpg", NewUrl = newUrl }); } // Validate all URLs var allValid = dto.ImageUrls.All(p => IsValidCosUrl(p.NewUrl)); if (allValid) return false.Label($"Should have detected invalid URL at position {t.invalidPosition}"); // Verify: The invalid URL is at the expected position var invalidUrl = dto.ImageUrls[t.invalidPosition].NewUrl; if (IsValidCosUrl(invalidUrl)) return false.Label($"URL at position {t.invalidPosition} should be invalid"); return true.Label($"Correctly detected invalid URL at position {t.invalidPosition}"); }); } /// /// Property 7: Migration URL Update Integrity - Empty Request Handling /// /// *For any* migration update request with empty or null ImageUrls, /// the system SHALL reject the request. /// /// **Validates: Requirements 10.3, 10.4** /// [Fact] public void EmptyImageUrls_ShouldBeRejected() { var dto1 = new MigrationUpdateDto { RecordId = 1, ImageUrls = null }; var dto2 = new MigrationUpdateDto { RecordId = 1, ImageUrls = new List() }; // Verify: Both should be considered invalid var isValid1 = ValidateUpdateRequest(dto1); var isValid2 = ValidateUpdateRequest(dto2); Assert.False(isValid1, "Null ImageUrls should be rejected"); Assert.False(isValid2, "Empty ImageUrls should be rejected"); } /// /// Property 7: Migration URL Update Integrity - Invalid RecordId Handling /// /// *For any* migration update request with invalid RecordId, /// the system SHALL reject the request. /// /// **Validates: Requirements 10.3, 10.4** /// [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(-100)] public void InvalidRecordId_ShouldBeRejected(int recordId) { var dto = new MigrationUpdateDto { RecordId = recordId, ImageUrls = new List { new MigrationUrlPair { OldUrl = $"{OldServerUrl}/images/test.jpg", NewUrl = $"{CosDomainUrl}/{CosPrefixes}/202501/20250101/当日照片/test.jpg" } } }; var isValid = ValidateUpdateRequest(dto); Assert.False(isValid, $"RecordId {recordId} should be rejected"); } /// /// 验证是否为有效的COS URL(与CosService.IsValidCosUrl逻辑一致) /// private bool IsValidCosUrl(string url) { if (string.IsNullOrWhiteSpace(url)) return false; return url.StartsWith($"{CosDomainUrl}/{CosPrefixes}/"); } /// /// 验证更新请求是否有效 /// private bool ValidateUpdateRequest(MigrationUpdateDto dto) { if (dto == null) return false; if (dto.RecordId <= 0) return false; if (dto.ImageUrls == null || dto.ImageUrls.Count == 0) return false; return true; } } }