- Model层新增AdminConfig实体和AdminConfigReadDbContext(只读连接Admin库) - API项目新增AdminConnection连接字符串,注册AdminConfigReadDbContext - Core层ConfigService按key路由:运营配置走Admin库,业务配置走业务库 - WechatPayConfigService改为从Admin库读取支付/小程序配置 - WechatService新增AdminConfigReadDbContext注入,配置读取改为Admin库 - Autofac注册同步更新三个服务的依赖注入 - Admin.Business的AdminConfigService改用AdminConfigDbContext连接Admin库
339 lines
12 KiB
C#
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; }
|
|
}
|