using FsCheck;
using FsCheck.Xunit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MiAssessment.Admin.Business.Data;
using MiAssessment.Admin.Business.Models;
using MiAssessment.Admin.Business.Models.Common;
using MiAssessment.Admin.Business.Services;
using Moq;
using Xunit;
// 使用别名解决命名冲突
using ConfigEntity = MiAssessment.Admin.Business.Entities.Config;
namespace MiAssessment.Tests.Admin;
///
/// ConfigService 属性测试
/// 验证系统配置服务的正确性属性
///
public class ConfigServicePropertyTests
{
private readonly Mock> _mockLogger = new();
#region Property 6: Config Value Validation - Price
///
/// Property 6: 价格配置值必须是正数
/// *For any* price configuration update, the value SHALL be a positive decimal number.
///
/// **Validates: Requirements 1.3**
///
[Property(MaxTest = 100)]
public bool PriceConfigMustBePositive_ValidPositiveValues(PositiveInt seed)
{
// Arrange: 创建包含价格配置的数据库
using var dbContext = CreateDbContext();
var configKey = $"test_price_{seed.Get}";
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "100.00",
ConfigType = "price",
Description = "测试价格配置",
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
// 生成正数价格值
var positivePrice = (seed.Get % 10000) + 0.01m;
var priceValue = positivePrice.ToString("F2");
// Act: 更新价格配置
var result = service.UpdateConfigAsync(configKey, priceValue).GetAwaiter().GetResult();
// Assert: 正数价格应该更新成功
var updatedConfig = dbContext.Configs.First(c => c.ConfigKey == configKey);
return result && updatedConfig.ConfigValue == priceValue;
}
///
/// Property 6: 价格配置值必须是正数 - 非正数值应抛出异常
/// *For any* price configuration update with non-positive value, the service SHALL throw BusinessException.
///
/// **Validates: Requirements 1.3**
///
[Property(MaxTest = 100)]
public bool PriceConfigMustBePositive_NonPositiveValuesThrowException(PositiveInt seed)
{
// Arrange: 创建包含价格配置的数据库
using var dbContext = CreateDbContext();
var configKey = $"test_price_invalid_{seed.Get}";
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "100.00",
ConfigType = "price",
Description = "测试价格配置",
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
// 生成非正数价格值(0 或负数)
var nonPositivePrice = -(seed.Get % 1000);
var priceValue = nonPositivePrice.ToString();
// Act & Assert: 非正数价格应抛出 BusinessException
try
{
service.UpdateConfigAsync(configKey, priceValue).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.ConfigValueInvalid;
}
}
///
/// Property 6: 价格配置值必须是正数 - 零值应抛出异常
///
/// **Validates: Requirements 1.3**
///
[Property(MaxTest = 50)]
public bool PriceConfigMustBePositive_ZeroValueThrowsException(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var configKey = $"test_price_zero_{seed.Get}";
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "100.00",
ConfigType = "price",
Description = "测试价格配置",
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
// Act & Assert: 零值应抛出 BusinessException
try
{
service.UpdateConfigAsync(configKey, "0").GetAwaiter().GetResult();
return false;
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.ConfigValueInvalid;
}
}
#endregion
#region Property 6: Config Value Validation - Commission Rate
///
/// Property 6: 佣金比例配置值必须在 0-1 之间
/// *For any* commission rate configuration update, the value SHALL be between 0 and 1 (inclusive).
///
/// **Validates: Requirements 1.4**
///
[Property(MaxTest = 100)]
public bool CommissionRateMustBeBetweenZeroAndOne_ValidValues(PositiveInt seed)
{
// Arrange: 创建包含佣金配置的数据库
using var dbContext = CreateDbContext();
var configKey = $"test_commission_{seed.Get}";
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "0.10",
ConfigType = "commission",
Description = "测试佣金配置",
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
// 生成 0-1 之间的佣金比例
var validRate = (seed.Get % 101) / 100.0m; // 0.00 到 1.00
var rateValue = validRate.ToString("F2");
// Act: 更新佣金配置
var result = service.UpdateConfigAsync(configKey, rateValue).GetAwaiter().GetResult();
// Assert: 有效佣金比例应该更新成功
var updatedConfig = dbContext.Configs.First(c => c.ConfigKey == configKey);
return result && updatedConfig.ConfigValue == rateValue;
}
///
/// Property 6: 佣金比例配置值必须在 0-1 之间 - 大于1的值应抛出异常
///
/// **Validates: Requirements 1.4**
///
[Property(MaxTest = 100)]
public bool CommissionRateMustBeBetweenZeroAndOne_GreaterThanOneThrowsException(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var configKey = $"test_commission_gt1_{seed.Get}";
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "0.10",
ConfigType = "commission",
Description = "测试佣金配置",
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
// 生成大于1的佣金比例
var invalidRate = 1.0m + ((seed.Get % 100) + 1) / 100.0m; // 1.01 到 2.00
var rateValue = invalidRate.ToString("F2");
// Act & Assert: 大于1的佣金比例应抛出 BusinessException
try
{
service.UpdateConfigAsync(configKey, rateValue).GetAwaiter().GetResult();
return false;
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.ConfigValueInvalid;
}
}
///
/// Property 6: 佣金比例配置值必须在 0-1 之间 - 负数值应抛出异常
///
/// **Validates: Requirements 1.4**
///
[Property(MaxTest = 100)]
public bool CommissionRateMustBeBetweenZeroAndOne_NegativeValuesThrowException(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var configKey = $"test_commission_neg_{seed.Get}";
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "0.10",
ConfigType = "commission",
Description = "测试佣金配置",
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
// 生成负数佣金比例
var negativeRate = -((seed.Get % 100) + 1) / 100.0m; // -0.01 到 -1.00
var rateValue = negativeRate.ToString("F2");
// Act & Assert: 负数佣金比例应抛出 BusinessException
try
{
service.UpdateConfigAsync(configKey, rateValue).GetAwaiter().GetResult();
return false;
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.ConfigValueInvalid;
}
}
///
/// Property 6: 佣金比例边界值测试 - 0 和 1 应该是有效值
///
/// **Validates: Requirements 1.4**
///
[Fact]
public async Task CommissionRateBoundaryValues_ZeroAndOneAreValid()
{
// Arrange
using var dbContext = CreateDbContext();
var configZero = new ConfigEntity
{
ConfigKey = "test_commission_zero",
ConfigValue = "0.50",
ConfigType = "commission",
Description = "测试佣金配置",
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
var configOne = new ConfigEntity
{
ConfigKey = "test_commission_one",
ConfigValue = "0.50",
ConfigType = "commission",
Description = "测试佣金配置",
Sort = 2,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Configs.AddRange(configZero, configOne);
await dbContext.SaveChangesAsync();
var service = new ConfigService(dbContext, _mockLogger.Object);
// Act & Assert: 0 应该是有效值
var resultZero = await service.UpdateConfigAsync("test_commission_zero", "0");
Assert.True(resultZero);
// Act & Assert: 1 应该是有效值
var resultOne = await service.UpdateConfigAsync("test_commission_one", "1");
Assert.True(resultOne);
}
#endregion
#region Property 17: Config Update Timestamp
///
/// Property 17: 配置更新时自动设置 UpdateTime
/// *For any* configuration update, the UpdateTime field SHALL be automatically set to the current timestamp.
///
/// **Validates: Requirements 1.7**
///
[Property(MaxTest = 100)]
public bool ConfigUpdateSetsTimestamp(PositiveInt seed)
{
// Arrange: 创建配置
using var dbContext = CreateDbContext();
var configKey = $"test_timestamp_{seed.Get}";
var originalUpdateTime = DateTime.Now.AddDays(-1); // 设置为昨天
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "original_value",
ConfigType = "content", // 使用不需要特殊验证的类型
Description = "测试时间戳配置",
Sort = 1,
CreateTime = originalUpdateTime,
UpdateTime = originalUpdateTime,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
var beforeUpdate = DateTime.Now;
// Act: 更新配置
var newValue = $"updated_value_{seed.Get}";
var result = service.UpdateConfigAsync(configKey, newValue).GetAwaiter().GetResult();
var afterUpdate = DateTime.Now;
// Assert: UpdateTime 应该在更新前后的时间范围内
var updatedConfig = dbContext.Configs.First(c => c.ConfigKey == configKey);
return result
&& updatedConfig.UpdateTime >= beforeUpdate
&& updatedConfig.UpdateTime <= afterUpdate
&& updatedConfig.UpdateTime > originalUpdateTime;
}
///
/// Property 17: 价格配置更新时自动设置 UpdateTime
///
/// **Validates: Requirements 1.7**
///
[Property(MaxTest = 50)]
public bool PriceConfigUpdateSetsTimestamp(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var configKey = $"test_price_timestamp_{seed.Get}";
var originalUpdateTime = DateTime.Now.AddHours(-1);
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "100.00",
ConfigType = "price",
Description = "测试价格时间戳",
Sort = 1,
CreateTime = originalUpdateTime,
UpdateTime = originalUpdateTime,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
var beforeUpdate = DateTime.Now;
// Act: 更新价格配置
var newPrice = ((seed.Get % 1000) + 1).ToString("F2");
var result = service.UpdateConfigAsync(configKey, newPrice).GetAwaiter().GetResult();
var afterUpdate = DateTime.Now;
// Assert
var updatedConfig = dbContext.Configs.First(c => c.ConfigKey == configKey);
return result
&& updatedConfig.UpdateTime >= beforeUpdate
&& updatedConfig.UpdateTime <= afterUpdate;
}
///
/// Property 17: 佣金配置更新时自动设置 UpdateTime
///
/// **Validates: Requirements 1.7**
///
[Property(MaxTest = 50)]
public bool CommissionConfigUpdateSetsTimestamp(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var configKey = $"test_commission_timestamp_{seed.Get}";
var originalUpdateTime = DateTime.Now.AddHours(-1);
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "0.10",
ConfigType = "commission",
Description = "测试佣金时间戳",
Sort = 1,
CreateTime = originalUpdateTime,
UpdateTime = originalUpdateTime,
IsDeleted = false
};
dbContext.Configs.Add(config);
dbContext.SaveChanges();
var service = new ConfigService(dbContext, _mockLogger.Object);
var beforeUpdate = DateTime.Now;
// Act: 更新佣金配置
var newRate = ((seed.Get % 101) / 100.0m).ToString("F2");
var result = service.UpdateConfigAsync(configKey, newRate).GetAwaiter().GetResult();
var afterUpdate = DateTime.Now;
// Assert
var updatedConfig = dbContext.Configs.First(c => c.ConfigKey == configKey);
return result
&& updatedConfig.UpdateTime >= beforeUpdate
&& updatedConfig.UpdateTime <= afterUpdate;
}
///
/// Property 17: 多次更新配置时 UpdateTime 应该递增
///
/// **Validates: Requirements 1.7**
///
[Fact]
public async Task MultipleConfigUpdates_UpdateTimeShouldIncrease()
{
// Arrange
using var dbContext = CreateDbContext();
var configKey = "test_multiple_updates";
var config = new ConfigEntity
{
ConfigKey = configKey,
ConfigValue = "initial_value",
ConfigType = "content",
Description = "测试多次更新",
Sort = 1,
CreateTime = DateTime.Now.AddDays(-1),
UpdateTime = DateTime.Now.AddDays(-1),
IsDeleted = false
};
dbContext.Configs.Add(config);
await dbContext.SaveChangesAsync();
var service = new ConfigService(dbContext, _mockLogger.Object);
// Act: 第一次更新
await service.UpdateConfigAsync(configKey, "value_1");
var firstUpdateTime = dbContext.Configs.First(c => c.ConfigKey == configKey).UpdateTime;
// 等待一小段时间确保时间戳不同
await Task.Delay(10);
// Act: 第二次更新
await service.UpdateConfigAsync(configKey, "value_2");
var secondUpdateTime = dbContext.Configs.First(c => c.ConfigKey == configKey).UpdateTime;
// Assert: 第二次更新时间应该大于等于第一次
Assert.True(secondUpdateTime >= firstUpdateTime);
}
#endregion
#region 辅助方法
///
/// 创建内存数据库上下文
///
private AdminBusinessDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AdminBusinessDbContext(options);
}
#endregion
}