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