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");
});
}
}
}