using FsCheck; using FsCheck.Xunit; using WorkCameraExport.Models; using WorkCameraExport.Services; namespace WorkCameraExport.Tests { /// /// ConfigService 属性测试 /// Feature: work-camera-2.0.1, Property 12: 配置持久化 /// Validates: Requirements 12.2 /// public class ConfigServicePropertyTests : IDisposable { private readonly string _testDataPath; private readonly ConfigService _configService; public ConfigServicePropertyTests() { // 为每个测试创建唯一的临时目录 _testDataPath = Path.Combine(Path.GetTempPath(), $"WorkCameraExport_Test_{Guid.NewGuid():N}"); Directory.CreateDirectory(_testDataPath); _configService = new ConfigService(_testDataPath); } public void Dispose() { // 清理测试目录 try { if (Directory.Exists(_testDataPath)) { Directory.Delete(_testDataPath, true); } } catch { // 忽略清理错误 } } /// /// Property 12: 配置持久化 /// For any 配置修改操作,保存后重新加载配置应得到相同的值。 /// [Property(MaxTest = 100)] public Property ConfigPersistence_SaveThenLoad_ShouldReturnSameValues() { return Prop.ForAll( Arb.From(), Arb.From(), Arb.From(), (concurrency, quality, autoClean) => { // Arrange: 创建配置,限制在有效范围内 var originalConfig = new AppConfig { DefaultSavePath = _testDataPath, ImageDownloadConcurrency = Math.Clamp(concurrency.Get % 10 + 1, 1, 10), ImageCompressQuality = Math.Clamp(quality.Get % 71 + 30, 30, 100), AutoCleanTempFiles = autoClean }; // Act: 保存配置 _configService.SaveConfig(originalConfig); // 重新加载配置 var loadedConfig = _configService.LoadConfig(); // Assert: 验证所有值相等 return (loadedConfig.DefaultSavePath == originalConfig.DefaultSavePath) .Label("DefaultSavePath should match") .And(loadedConfig.ImageDownloadConcurrency == originalConfig.ImageDownloadConcurrency) .Label("ImageDownloadConcurrency should match") .And(loadedConfig.ImageCompressQuality == originalConfig.ImageCompressQuality) .Label("ImageCompressQuality should match") .And(loadedConfig.AutoCleanTempFiles == originalConfig.AutoCleanTempFiles) .Label("AutoCleanTempFiles should match"); }); } /// /// Property 12 扩展: 配置验证后的值应在有效范围内 /// [Property(MaxTest = 100)] public Property ConfigValidation_ShouldClampValuesToValidRange() { return Prop.ForAll( Arb.From(), Arb.From(), (concurrency, quality) => { // Arrange: 创建可能超出范围的配置 var config = new AppConfig { ImageDownloadConcurrency = concurrency, ImageCompressQuality = quality }; // Act: 验证配置 config.Validate(); // Assert: 值应在有效范围内 return (config.ImageDownloadConcurrency >= 1 && config.ImageDownloadConcurrency <= 10) .Label($"Concurrency {config.ImageDownloadConcurrency} should be between 1 and 10") .And(config.ImageCompressQuality >= 30 && config.ImageCompressQuality <= 100) .Label($"Quality {config.ImageCompressQuality} should be between 30 and 100"); }); } /// /// Property 12 扩展: 凭证持久化 - 保存后加载应返回相同值 /// [Property(MaxTest = 100)] public Property CredentialsPersistence_SaveThenLoad_ShouldReturnSameValues() { return Prop.ForAll( Arb.From(), Arb.From(), Arb.From(), (serverUrl, token, username) => { // 过滤掉包含特殊字符的字符串,避免 JSON 序列化问题 var safeServerUrl = new string(serverUrl.Get.Where(c => !char.IsControl(c)).ToArray()); var safeToken = new string(token.Get.Where(c => !char.IsControl(c)).ToArray()); var safeUsername = new string(username.Get.Where(c => !char.IsControl(c)).ToArray()); if (string.IsNullOrWhiteSpace(safeServerUrl) || string.IsNullOrWhiteSpace(safeToken) || string.IsNullOrWhiteSpace(safeUsername)) { return true.Label("Skipped: empty after filtering"); } // Act: 保存凭证 _configService.SaveCredentials(safeServerUrl, safeToken, safeUsername); // 重新加载凭证 var loaded = _configService.LoadCredentials(); // Assert: 验证凭证存在且值相等 return (loaded != null) .Label("Credentials should be loaded") .And(loaded?.ServerUrl == safeServerUrl) .Label("ServerUrl should match") .And(loaded?.Token == safeToken) .Label("Token should match") .And(loaded?.Username == safeUsername) .Label("Username should match"); }); } /// /// Property 12 扩展: 清除凭证后应无法加载 /// [Property(MaxTest = 100)] public Property CredentialsClear_AfterClear_ShouldReturnNull() { return Prop.ForAll( Arb.From(), Arb.From(), Arb.From(), (serverUrl, token, username) => { var safeServerUrl = new string(serverUrl.Get.Where(c => !char.IsControl(c)).ToArray()); var safeToken = new string(token.Get.Where(c => !char.IsControl(c)).ToArray()); var safeUsername = new string(username.Get.Where(c => !char.IsControl(c)).ToArray()); if (string.IsNullOrWhiteSpace(safeServerUrl) || string.IsNullOrWhiteSpace(safeToken) || string.IsNullOrWhiteSpace(safeUsername)) { return true.Label("Skipped: empty after filtering"); } // Arrange: 保存凭证 _configService.SaveCredentials(safeServerUrl, safeToken, safeUsername); // Act: 清除凭证 _configService.ClearCredentials(); // Assert: 加载应返回 null var loaded = _configService.LoadCredentials(); return (loaded == null) .Label("Credentials should be null after clear"); }); } } }