mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Services/AdminConfigServicePropertyTests.cs
zpc 8489b4300c refactor(config): 统一配置读取架构,运营配置从Admin库读取
- Model层新增AdminConfig实体和AdminConfigReadDbContext(只读连接Admin库)
- API项目新增AdminConnection连接字符串,注册AdminConfigReadDbContext
- Core层ConfigService按key路由:运营配置走Admin库,业务配置走业务库
- WechatPayConfigService改为从Admin库读取支付/小程序配置
- WechatService新增AdminConfigReadDbContext注入,配置读取改为Admin库
- Autofac注册同步更新三个服务的依赖注入
- Admin.Business的AdminConfigService改用AdminConfigDbContext连接Admin库
2026-02-20 15:48:16 +08:00

339 lines
12 KiB
C#

using System.Text.Json;
using FsCheck;
using FsCheck.Xunit;
using MiAssessment.Admin.Business.Data;
using MiAssessment.Admin.Business.Models.Config;
using MiAssessment.Admin.Business.Services;
using MiAssessment.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Services;
/// <summary>
/// AdminConfigService 属性测试
/// </summary>
public class AdminConfigServicePropertyTests
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
private readonly Mock<ILogger<AdminConfigService>> _mockLogger = new();
#region Property 1: Configuration Round-Trip Consistency
/// <summary>
/// **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
/// </summary>
[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<AdminConfigDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new AdminConfigDbContext(options);
var mockRedis = new Mock<IRedisService>();
mockRedis.Setup(x => x.GetStringAsync(It.IsAny<string>())).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<TestConfigData>(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
/// <summary>
/// **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
/// </summary>
[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<WeixinPayMerchant>
{
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("重复");
}
/// <summary>
/// **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
/// </summary>
[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<WeixinPayMerchant>
{
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位字符");
}
/// <summary>
/// **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
/// </summary>
[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<WeixinPayMerchant>();
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
/// <summary>
/// **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
/// </summary>
[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<MiniprogramConfig>();
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("默认");
}
/// <summary>
/// **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
/// </summary>
[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<MiniprogramConfig>();
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;
}
/// <summary>
/// **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
/// </summary>
[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<H5AppConfig>();
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("默认");
}
/// <summary>
/// **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
/// </summary>
[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<H5AppConfig>();
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<AdminConfigDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var dbContext = new AdminConfigDbContext(options);
var mockRedis = new Mock<IRedisService>();
mockRedis.Setup(x => x.GetStringAsync(It.IsAny<string>())).ReturnsAsync((string?)null);
return new AdminConfigService(dbContext, mockRedis.Object, _mockLogger.Object);
}
#endregion
}
/// <summary>
/// 测试用配置数据模型
/// </summary>
public class TestConfigData
{
public string Name { get; set; } = string.Empty;
public int Value { get; set; }
public bool Enabled { get; set; }
}