using FsCheck; using FsCheck.Xunit; using Infrastructure; using Microsoft.Extensions.Configuration; using Xunit; using ZR.Common; using ZR.Model.Business.Dto; namespace ZR.Tests { /// /// CosService 属性测试 /// **Feature: work-camera-2.0, Property 1: Pre-signed URL Generation Completeness** /// **Validates: Requirements 1.1, 1.2, 1.4, 1.5** /// public class CosServicePropertyTests : IClassFixture { public CosServicePropertyTests(TestFixture fixture) { // 确保配置已初始化 } /// /// Property 1: Pre-signed URL Generation Completeness /// /// *For any* valid upload request with N images and M workers, the API SHALL return exactly N image entries, each containing: /// - 1 daily photo URL /// - M worker directory URLs (one per worker) /// - 1 content directory URL /// - 1 department directory URL /// - All URLs following the pattern `/workfiles/{yyyyMM}/{yyyyMMdd}/{category}/{subcategory}/{timestamp}_{random}.{ext}` /// /// **Validates: Requirements 1.1, 1.2, 1.4, 1.5** /// [Property(MaxTest = 100)] public Property UrlGenerationCompleteness_ReturnsCorrectStructure() { // Generate valid inputs using a combined generator var requestGen = from imageCount in Gen.Choose(1, 15) from workerCount in Gen.Choose(1, 10) from deptName in Gen.Elements("技术部", "市场部", "财务部", "人事部", "运营部") from content in Gen.Elements("设备检修", "日常巡检", "故障处理", "安装调试", "维护保养") from fileExt in Gen.Elements(".jpg", ".png", ".jpeg") from year in Gen.Choose(2020, 2030) from month in Gen.Choose(1, 12) from day in Gen.Choose(1, 28) select new { imageCount, workerCount, deptName, content, fileExt, year, month, day }; return Prop.ForAll( requestGen.ToArbitrary(), req => { // Generate unique worker names var allWorkers = new[] { "张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十", "郑一", "冯二" }; var workers = allWorkers.Take(req.workerCount).ToList(); var recordTime = new DateTime(req.year, req.month, req.day, 10, 30, 0); var request = new CosUploadUrlsRequest { RecordTime = recordTime, DeptName = req.deptName, Content = req.content, Workers = workers, FileExt = req.fileExt, ImageCount = req.imageCount }; // Act var response = CosService.GetUploadUrls(request); // Assert: Response should not be null if (response == null || response.Images == null) return false.Label("Response or Images is null"); // Assert: Should return exactly N images if (response.Images.Count != req.imageCount) return false.Label($"Expected {req.imageCount} images, got {response.Images.Count}"); // Expected date path format var expectedDatePath = $"/workfiles/{recordTime:yyyyMM}/{recordTime:yyyyMMdd}/"; foreach (var image in response.Images) { // Assert: Each image should have ImageId if (string.IsNullOrEmpty(image.ImageId)) return false.Label("ImageId is null or empty"); // Assert: Each image should have FileName with correct extension if (string.IsNullOrEmpty(image.FileName) || !image.FileName.EndsWith(req.fileExt)) return false.Label($"FileName is invalid or doesn't end with {req.fileExt}"); // Assert: FileName should follow timestamp_random format if (!image.FileName.Contains("_")) return false.Label("FileName doesn't follow timestamp_random format"); // Assert: Each image should have AccessUrl if (string.IsNullOrEmpty(image.AccessUrl)) return false.Label("AccessUrl is null or empty"); // Assert: AccessUrl should contain correct date path if (!image.AccessUrl.Contains(expectedDatePath)) return false.Label($"AccessUrl doesn't contain expected date path: {expectedDatePath}"); // Assert: AccessUrl should contain 当日照片 category if (!image.AccessUrl.Contains("/当日照片/")) return false.Label("AccessUrl doesn't contain /当日照片/"); // Assert: UploadUrls should not be null if (image.UploadUrls == null) return false.Label("UploadUrls is null"); // Assert: Should have 1 daily photo URL if (string.IsNullOrEmpty(image.UploadUrls.Daily)) return false.Label("Daily URL is null or empty"); // Assert: Daily URL should contain correct path if (!image.UploadUrls.Daily.Contains(expectedDatePath) || !image.UploadUrls.Daily.Contains("当日照片")) return false.Label("Daily URL doesn't contain expected path"); // Assert: Should have 1 content directory URL if (string.IsNullOrEmpty(image.UploadUrls.Content)) return false.Label("Content URL is null or empty"); // Assert: Content URL should contain correct path if (!image.UploadUrls.Content.Contains(expectedDatePath) || !image.UploadUrls.Content.Contains("工作内容")) return false.Label("Content URL doesn't contain expected path"); // Assert: Should have 1 department directory URL if (string.IsNullOrEmpty(image.UploadUrls.Dept)) return false.Label("Dept URL is null or empty"); // Assert: Dept URL should contain correct path if (!image.UploadUrls.Dept.Contains(expectedDatePath) || !image.UploadUrls.Dept.Contains("部门")) return false.Label("Dept URL doesn't contain expected path"); // Assert: Should have M worker directory URLs if (image.UploadUrls.Workers == null) return false.Label("Workers URLs dictionary is null"); if (image.UploadUrls.Workers.Count != req.workerCount) return false.Label($"Expected {req.workerCount} worker URLs, got {image.UploadUrls.Workers.Count}"); // Assert: Each worker should have a URL foreach (var worker in workers) { if (!image.UploadUrls.Workers.ContainsKey(worker)) return false.Label($"Missing URL for worker: {worker}"); var workerUrl = image.UploadUrls.Workers[worker]; if (string.IsNullOrEmpty(workerUrl)) return false.Label($"Worker URL is null or empty for: {worker}"); // Assert: Worker URL should contain correct path if (!workerUrl.Contains(expectedDatePath) || !workerUrl.Contains("参与人员")) return false.Label($"Worker URL doesn't contain expected path for: {worker}"); } } return true.Label("All assertions passed"); }); } } }