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 }