190 lines
7.8 KiB
C#
190 lines
7.8 KiB
C#
using FsCheck;
|
|
using FsCheck.Xunit;
|
|
using WorkCameraExport.Models;
|
|
using WorkCameraExport.Services;
|
|
|
|
namespace WorkCameraExport.Tests
|
|
{
|
|
/// <summary>
|
|
/// ConfigService 属性测试
|
|
/// Feature: work-camera-2.0.1, Property 12: 配置持久化
|
|
/// Validates: Requirements 12.2
|
|
/// </summary>
|
|
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
|
|
{
|
|
// 忽略清理错误
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property 12: 配置持久化
|
|
/// For any 配置修改操作,保存后重新加载配置应得到相同的值。
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property ConfigPersistence_SaveThenLoad_ShouldReturnSameValues()
|
|
{
|
|
return Prop.ForAll(
|
|
Arb.From<PositiveInt>(),
|
|
Arb.From<PositiveInt>(),
|
|
Arb.From<bool>(),
|
|
(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");
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property 12 扩展: 配置验证后的值应在有效范围内
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property ConfigValidation_ShouldClampValuesToValidRange()
|
|
{
|
|
return Prop.ForAll(
|
|
Arb.From<int>(),
|
|
Arb.From<int>(),
|
|
(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");
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property 12 扩展: 凭证持久化 - 保存后加载应返回相同值
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property CredentialsPersistence_SaveThenLoad_ShouldReturnSameValues()
|
|
{
|
|
return Prop.ForAll(
|
|
Arb.From<NonEmptyString>(),
|
|
Arb.From<NonEmptyString>(),
|
|
Arb.From<NonEmptyString>(),
|
|
(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");
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property 12 扩展: 清除凭证后应无法加载
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property CredentialsClear_AfterClear_ShouldReturnNull()
|
|
{
|
|
return Prop.ForAll(
|
|
Arb.From<NonEmptyString>(),
|
|
Arb.From<NonEmptyString>(),
|
|
Arb.From<NonEmptyString>(),
|
|
(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");
|
|
});
|
|
}
|
|
}
|
|
}
|