using System.Text.Json; using FsCheck; using FsCheck.Xunit; using HoneyBox.Admin.Business.Models.Config; using HoneyBox.Admin.Business.Services; using HoneyBox.Core.Interfaces; using HoneyBox.Model.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace HoneyBox.Tests.Services; /// /// AdminConfigService 属性测试 /// public class AdminConfigServicePropertyTests { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; private readonly Mock> _mockLogger = new(); #region Property 1: Configuration Round-Trip Consistency /// /// **Feature: admin-business-migration, Property 1: Configuration Round-Trip Consistency** /// For any valid configuration object, saving it via UpdateConfigAsync and then retrieving it /// via GetConfigAsync should produce an equivalent object. /// Validates: Requirements 3.1, 3.2 /// [Property(MaxTest = 100)] public bool ConfigRoundTrip_ShouldPreserveData(NonEmptyString name, int value, bool enabled) { // Create test config data var configData = new TestConfigData { Name = name.Get, Value = value, Enabled = enabled }; // Create a fresh service instance for each test var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var mockRedis = new Mock(); mockRedis.Setup(x => x.GetStringAsync(It.IsAny())).ReturnsAsync((string?)null); var service = new AdminConfigService(dbContext, mockRedis.Object, _mockLogger.Object); var configKey = $"test_config_{Guid.NewGuid():N}"; // Save the config var saved = service.UpdateConfigAsync(configKey, configData).GetAwaiter().GetResult(); if (!saved) return false; // Retrieve the config var retrieved = service.GetConfigAsync(configKey).GetAwaiter().GetResult(); if (retrieved == null) return false; // Verify round-trip consistency return configData.Name == retrieved.Name && configData.Value == retrieved.Value && configData.Enabled == retrieved.Enabled; } #endregion #region Property 2: Merchant Prefix Uniqueness Validation /// /// **Feature: admin-business-migration, Property 2: Merchant Prefix Uniqueness Validation** /// For any weixinpay_setting configuration with duplicate merchant prefixes, /// the validation should fail and return an error. /// Validates: Requirements 3.4 /// [Property(MaxTest = 100)] public bool WeixinPaySetting_WithDuplicatePrefixes_ShouldFailValidation(PositiveInt seed) { var prefixes = new[] { "ABC", "DEF", "GHI", "JKL" }; var prefix = prefixes[seed.Get % prefixes.Length]; var setting = new WeixinPaySetting { Merchants = new List { new() { Name = "商户1", MchId = "123456", OrderPrefix = prefix }, new() { Name = "商户2", MchId = "789012", OrderPrefix = prefix } // 重复前缀 } }; var service = CreateService(); var json = JsonSerializer.Serialize(setting, JsonOptions); var result = service.ValidateConfigAsync(ConfigKeys.WeixinPaySetting, json).GetAwaiter().GetResult(); // Should return an error message containing "重复" return result != null && result.Contains("重复"); } /// /// **Feature: admin-business-migration, Property 2: Merchant Prefix Uniqueness Validation** /// For any weixinpay_setting configuration with prefixes not exactly 3 characters, /// the validation should fail and return an error. /// Validates: Requirements 3.4 /// [Property(MaxTest = 100)] public bool WeixinPaySetting_WithInvalidPrefixLength_ShouldFailValidation(PositiveInt seed) { var invalidPrefixes = new[] { "AB", "A", "ABCD", "ABCDE", "" }; var invalidPrefix = invalidPrefixes[seed.Get % invalidPrefixes.Length]; var setting = new WeixinPaySetting { Merchants = new List { new() { Name = "商户1", MchId = "123456", OrderPrefix = invalidPrefix } } }; var service = CreateService(); var json = JsonSerializer.Serialize(setting, JsonOptions); var result = service.ValidateConfigAsync(ConfigKeys.WeixinPaySetting, json).GetAwaiter().GetResult(); // Should return an error message containing "3位字符" return result != null && result.Contains("3位字符"); } /// /// **Feature: admin-business-migration, Property 2: Merchant Prefix Uniqueness Validation** /// For any weixinpay_setting configuration with valid unique 3-character prefixes, /// the validation should pass. /// Validates: Requirements 3.4 /// [Property(MaxTest = 100)] public bool WeixinPaySetting_WithValidUniquePrefixes_ShouldPassValidation(PositiveInt seed) { var allPrefixes = new[] { "ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VWX" }; var count = (seed.Get % 3) + 1; // 1 to 3 merchants var startIndex = seed.Get % (allPrefixes.Length - count); var merchants = new List(); for (int i = 0; i < count; i++) { merchants.Add(new WeixinPayMerchant { Name = $"商户{i + 1}", MchId = $"mch{i + 1}", OrderPrefix = allPrefixes[startIndex + i] }); } var setting = new WeixinPaySetting { Merchants = merchants }; var service = CreateService(); var json = JsonSerializer.Serialize(setting, JsonOptions); var result = service.ValidateConfigAsync(ConfigKeys.WeixinPaySetting, json).GetAwaiter().GetResult(); // Should return null (no error) return result == null; } #endregion #region Property 3: Default App Validation /// /// **Feature: admin-business-migration, Property 3: Default App Validation** /// For any miniprogram_setting configuration without at least one default app, /// the validation should fail and return an error. /// Validates: Requirements 3.5 /// [Property(MaxTest = 100)] public bool MiniprogramSetting_WithoutDefault_ShouldFailValidation(PositiveInt seed) { var prefixes = new[] { "AB", "CD", "EF", "GH" }; var count = (seed.Get % 3) + 1; // 1 to 3 miniprograms var miniprograms = new List(); for (int i = 0; i < count; i++) { miniprograms.Add(new MiniprogramConfig { Name = $"小程序{i + 1}", AppId = $"wx{i + 1}", OrderPrefix = prefixes[i % prefixes.Length], IsDefault = 0 // 没有默认 }); } var setting = new MiniprogramSetting { Miniprograms = miniprograms }; var service = CreateService(); var json = JsonSerializer.Serialize(setting, JsonOptions); var result = service.ValidateConfigAsync(ConfigKeys.MiniprogramSetting, json).GetAwaiter().GetResult(); // Should return an error message containing "默认" return result != null && result.Contains("默认"); } /// /// **Feature: admin-business-migration, Property 3: Default App Validation** /// For any miniprogram_setting configuration with at least one default app, /// the validation should pass. /// Validates: Requirements 3.5 /// [Property(MaxTest = 100)] public bool MiniprogramSetting_WithDefault_ShouldPassValidation(PositiveInt seed) { var prefixes = new[] { "AB", "CD", "EF", "GH" }; var count = (seed.Get % 3) + 1; // 1 to 3 miniprograms var defaultIndex = seed.Get % count; var miniprograms = new List(); for (int i = 0; i < count; i++) { miniprograms.Add(new MiniprogramConfig { Name = $"小程序{i + 1}", AppId = $"wx{i + 1}", OrderPrefix = prefixes[i % prefixes.Length], IsDefault = i == defaultIndex ? 1 : 0 // 设置一个默认 }); } var setting = new MiniprogramSetting { Miniprograms = miniprograms }; var service = CreateService(); var json = JsonSerializer.Serialize(setting, JsonOptions); var result = service.ValidateConfigAsync(ConfigKeys.MiniprogramSetting, json).GetAwaiter().GetResult(); // Should return null (no error) return result == null; } /// /// **Feature: admin-business-migration, Property 3: Default App Validation** /// For any h5_setting configuration without at least one default app, /// the validation should fail and return an error. /// Validates: Requirements 3.6 /// [Property(MaxTest = 100)] public bool H5Setting_WithoutDefault_ShouldFailValidation(PositiveInt seed) { var prefixes = new[] { "AB", "CD", "EF", "GH" }; var count = (seed.Get % 3) + 1; // 1 to 3 H5 apps var h5Apps = new List(); for (int i = 0; i < count; i++) { h5Apps.Add(new H5AppConfig { Name = $"H5应用{i + 1}", OrderPrefix = prefixes[i % prefixes.Length], IsDefault = 0 // 没有默认 }); } var setting = new H5Setting { H5Apps = h5Apps }; var service = CreateService(); var json = JsonSerializer.Serialize(setting, JsonOptions); var result = service.ValidateConfigAsync(ConfigKeys.H5Setting, json).GetAwaiter().GetResult(); // Should return an error message containing "默认" return result != null && result.Contains("默认"); } /// /// **Feature: admin-business-migration, Property 3: Default App Validation** /// For any h5_setting configuration with at least one default app, /// the validation should pass. /// Validates: Requirements 3.6 /// [Property(MaxTest = 100)] public bool H5Setting_WithDefault_ShouldPassValidation(PositiveInt seed) { var prefixes = new[] { "AB", "CD", "EF", "GH" }; var count = (seed.Get % 3) + 1; // 1 to 3 H5 apps var defaultIndex = seed.Get % count; var h5Apps = new List(); for (int i = 0; i < count; i++) { h5Apps.Add(new H5AppConfig { Name = $"H5应用{i + 1}", OrderPrefix = prefixes[i % prefixes.Length], IsDefault = i == defaultIndex ? 1 : 0 // 设置一个默认 }); } var setting = new H5Setting { H5Apps = h5Apps }; var service = CreateService(); var json = JsonSerializer.Serialize(setting, JsonOptions); var result = service.ValidateConfigAsync(ConfigKeys.H5Setting, json).GetAwaiter().GetResult(); // Should return null (no error) return result == null; } #endregion #region Helper Methods private AdminConfigService CreateService() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; var dbContext = new HoneyBoxDbContext(options); var mockRedis = new Mock(); mockRedis.Setup(x => x.GetStringAsync(It.IsAny())).ReturnsAsync((string?)null); return new AdminConfigService(dbContext, mockRedis.Object, _mockLogger.Object); } #endregion } /// /// 测试用配置数据模型 /// public class TestConfigData { public string Name { get; set; } = string.Empty; public int Value { get; set; } public bool Enabled { get; set; } }