411 lines
14 KiB
C#
411 lines
14 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using HoneyBox.Core.Interfaces;
|
||
using HoneyBox.Core.Services;
|
||
using HoneyBox.Model.Data;
|
||
using HoneyBox.Model.Models.Payment;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using Moq;
|
||
using Xunit;
|
||
|
||
namespace HoneyBox.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// 微信支付 V3 回调格式识别属性测试
|
||
/// **Feature: wechat-pay-v3-upgrade**
|
||
/// </summary>
|
||
public class WechatPayV3NotifyFormatPropertyTests
|
||
{
|
||
private IWechatPayV3Service CreateService()
|
||
{
|
||
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
|
||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||
.Options;
|
||
var dbContext = new HoneyBoxDbContext(options);
|
||
var httpClient = new HttpClient();
|
||
var logger = Mock.Of<ILogger<WechatPayV3Service>>();
|
||
var configService = Mock.Of<IWechatPayConfigService>();
|
||
|
||
return new WechatPayV3Service(dbContext, httpClient, logger, configService);
|
||
}
|
||
|
||
#region Property 8: 回调格式识别正确性
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// *For any* JSON 格式且包含 resource 字段的回调数据,应该被识别为 V3 格式。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3Format_JsonWithResource_ShouldBeDetectedAsV3(
|
||
NonEmptyString notifyId,
|
||
NonEmptyString eventType,
|
||
NonEmptyString ciphertext,
|
||
PositiveInt seed)
|
||
{
|
||
var service = CreateService();
|
||
|
||
// 清理输入(移除控制字符和特殊字符)
|
||
var cleanNotifyId = CleanJsonString(RemoveControlChars(notifyId.Get));
|
||
var cleanEventType = CleanJsonString(RemoveControlChars(eventType.Get));
|
||
var cleanCiphertext = CleanJsonString(RemoveControlChars(ciphertext.Get));
|
||
|
||
// 如果清理后为空,跳过测试
|
||
if (string.IsNullOrEmpty(cleanNotifyId) ||
|
||
string.IsNullOrEmpty(cleanEventType) ||
|
||
string.IsNullOrEmpty(cleanCiphertext))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// 构建 V3 格式的回调数据(JSON 格式且包含 resource 字段)
|
||
var v3NotifyBody = $@"{{
|
||
""id"": ""{cleanNotifyId}"",
|
||
""create_time"": ""2024-01-01T12:00:00+08:00"",
|
||
""event_type"": ""{cleanEventType}"",
|
||
""resource_type"": ""encrypt-resource"",
|
||
""resource"": {{
|
||
""algorithm"": ""AEAD_AES_256_GCM"",
|
||
""ciphertext"": ""{cleanCiphertext}"",
|
||
""nonce"": ""abcdefghijkl"",
|
||
""associated_data"": ""transaction""
|
||
}}
|
||
}}";
|
||
|
||
var isV3 = service.IsV3NotifyFormat(v3NotifyBody);
|
||
var isV2 = service.IsV2NotifyFormat(v3NotifyBody);
|
||
var version = service.DetectNotifyVersion(v3NotifyBody);
|
||
|
||
// V3 格式应该被正确识别
|
||
return isV3 && !isV2 && version == NotifyVersion.V3;
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// *For any* XML 格式的回调数据,应该被识别为 V2 格式。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V2Format_Xml_ShouldBeDetectedAsV2(
|
||
NonEmptyString orderNo,
|
||
NonEmptyString transactionId,
|
||
PositiveInt totalFee,
|
||
PositiveInt seed)
|
||
{
|
||
var service = CreateService();
|
||
|
||
// 清理输入(移除控制字符)
|
||
var cleanOrderNo = CleanXmlString(RemoveControlChars(orderNo.Get));
|
||
var cleanTransactionId = CleanXmlString(RemoveControlChars(transactionId.Get));
|
||
|
||
// 如果清理后为空,跳过测试
|
||
if (string.IsNullOrEmpty(cleanOrderNo) || string.IsNullOrEmpty(cleanTransactionId))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// 构建 V2 格式的回调数据(XML 格式)
|
||
var v2NotifyBody = $@"<xml>
|
||
<return_code><![CDATA[SUCCESS]]></return_code>
|
||
<result_code><![CDATA[SUCCESS]]></result_code>
|
||
<out_trade_no><![CDATA[{cleanOrderNo}]]></out_trade_no>
|
||
<transaction_id><![CDATA[{cleanTransactionId}]]></transaction_id>
|
||
<total_fee>{totalFee.Get}</total_fee>
|
||
</xml>";
|
||
|
||
var isV3 = service.IsV3NotifyFormat(v2NotifyBody);
|
||
var isV2 = service.IsV2NotifyFormat(v2NotifyBody);
|
||
var version = service.DetectNotifyVersion(v2NotifyBody);
|
||
|
||
// V2 格式应该被正确识别
|
||
return !isV3 && isV2 && version == NotifyVersion.V2;
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// *For any* JSON 格式但不包含 resource 字段的数据,不应该被识别为 V3 格式。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool JsonWithoutResource_ShouldNotBeV3(
|
||
NonEmptyString key,
|
||
NonEmptyString value,
|
||
PositiveInt seed)
|
||
{
|
||
var service = CreateService();
|
||
|
||
var cleanKey = CleanJsonString(RemoveControlChars(key.Get));
|
||
var cleanValue = CleanJsonString(RemoveControlChars(value.Get));
|
||
|
||
// 如果清理后为空,跳过测试
|
||
if (string.IsNullOrEmpty(cleanKey) || string.IsNullOrEmpty(cleanValue))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// 确保 key 不是 "resource"
|
||
if (cleanKey.Equals("resource", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
cleanKey = "other_key";
|
||
}
|
||
|
||
// 构建不包含 resource 字段的 JSON
|
||
var jsonBody = $@"{{
|
||
""{cleanKey}"": ""{cleanValue}"",
|
||
""other_field"": ""some_value""
|
||
}}";
|
||
|
||
var isV3 = service.IsV3NotifyFormat(jsonBody);
|
||
|
||
// 不包含 resource 字段的 JSON 不应该被识别为 V3
|
||
return !isV3;
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// *For any* 空字符串或空白字符串,应该被识别为 Unknown 格式。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool EmptyOrWhitespace_ShouldBeUnknown(PositiveInt whitespaceCount)
|
||
{
|
||
var service = CreateService();
|
||
|
||
// 生成空白字符串
|
||
var whitespace = new string(' ', whitespaceCount.Get % 100);
|
||
|
||
var isV3Empty = service.IsV3NotifyFormat("");
|
||
var isV2Empty = service.IsV2NotifyFormat("");
|
||
var versionEmpty = service.DetectNotifyVersion("");
|
||
|
||
var isV3Whitespace = service.IsV3NotifyFormat(whitespace);
|
||
var isV2Whitespace = service.IsV2NotifyFormat(whitespace);
|
||
var versionWhitespace = service.DetectNotifyVersion(whitespace);
|
||
|
||
// 空字符串和空白字符串都应该被识别为 Unknown
|
||
return !isV3Empty && !isV2Empty && versionEmpty == NotifyVersion.Unknown &&
|
||
!isV3Whitespace && !isV2Whitespace && versionWhitespace == NotifyVersion.Unknown;
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// *For any* 非 JSON 非 XML 的数据,应该被识别为 Unknown 格式。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool InvalidFormat_ShouldBeUnknown(NonEmptyString randomData, PositiveInt seed)
|
||
{
|
||
var service = CreateService();
|
||
|
||
// 确保数据不是以 { 或 < 开头
|
||
var data = randomData.Get.TrimStart();
|
||
if (data.StartsWith('{') || data.StartsWith('<'))
|
||
{
|
||
data = "INVALID_" + data;
|
||
}
|
||
|
||
var isV3 = service.IsV3NotifyFormat(data);
|
||
var isV2 = service.IsV2NotifyFormat(data);
|
||
var version = service.DetectNotifyVersion(data);
|
||
|
||
// 非 JSON 非 XML 的数据应该被识别为 Unknown
|
||
return !isV3 && !isV2 && version == NotifyVersion.Unknown;
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// V3 和 V2 格式应该是互斥的。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3AndV2_ShouldBeMutuallyExclusive(NonEmptyString data, PositiveInt seed)
|
||
{
|
||
var service = CreateService();
|
||
|
||
var isV3 = service.IsV3NotifyFormat(data.Get);
|
||
var isV2 = service.IsV2NotifyFormat(data.Get);
|
||
|
||
// V3 和 V2 不能同时为 true
|
||
return !(isV3 && isV2);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 边界情况测试
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// 真实的 V3 支付成功回调应该被正确识别。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Fact]
|
||
public void RealV3PaymentSuccessNotify_ShouldBeDetectedAsV3()
|
||
{
|
||
var service = CreateService();
|
||
|
||
var v3NotifyBody = @"{
|
||
""id"": ""EV-2024010112345678901234567890"",
|
||
""create_time"": ""2024-01-01T12:00:00+08:00"",
|
||
""event_type"": ""TRANSACTION.SUCCESS"",
|
||
""resource_type"": ""encrypt-resource"",
|
||
""resource"": {
|
||
""algorithm"": ""AEAD_AES_256_GCM"",
|
||
""ciphertext"": ""base64encodedciphertext"",
|
||
""nonce"": ""abcdefghijkl"",
|
||
""associated_data"": ""transaction"",
|
||
""original_type"": ""transaction""
|
||
},
|
||
""summary"": ""支付成功""
|
||
}";
|
||
|
||
Assert.True(service.IsV3NotifyFormat(v3NotifyBody));
|
||
Assert.False(service.IsV2NotifyFormat(v3NotifyBody));
|
||
Assert.Equal(NotifyVersion.V3, service.DetectNotifyVersion(v3NotifyBody));
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// 真实的 V2 支付成功回调应该被正确识别。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Fact]
|
||
public void RealV2PaymentSuccessNotify_ShouldBeDetectedAsV2()
|
||
{
|
||
var service = CreateService();
|
||
|
||
var v2NotifyBody = @"<xml>
|
||
<return_code><![CDATA[SUCCESS]]></return_code>
|
||
<return_msg><![CDATA[OK]]></return_msg>
|
||
<appid><![CDATA[wx1234567890abcdef]]></appid>
|
||
<mch_id><![CDATA[1234567890]]></mch_id>
|
||
<nonce_str><![CDATA[5K8264ILTKCH16CQ2502SI8ZNMTM67VS]]></nonce_str>
|
||
<sign><![CDATA[C380BEC2BFD727A4B6845133519F3AD6]]></sign>
|
||
<result_code><![CDATA[SUCCESS]]></result_code>
|
||
<openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
|
||
<trade_type><![CDATA[JSAPI]]></trade_type>
|
||
<bank_type><![CDATA[CMC]]></bank_type>
|
||
<total_fee>100</total_fee>
|
||
<fee_type><![CDATA[CNY]]></fee_type>
|
||
<transaction_id><![CDATA[1217752501201407033233368018]]></transaction_id>
|
||
<out_trade_no><![CDATA[MYH20240101120000001]]></out_trade_no>
|
||
<attach><![CDATA[order_yfs]]></attach>
|
||
<time_end><![CDATA[20240101120000]]></time_end>
|
||
</xml>";
|
||
|
||
Assert.False(service.IsV3NotifyFormat(v2NotifyBody));
|
||
Assert.True(service.IsV2NotifyFormat(v2NotifyBody));
|
||
Assert.Equal(NotifyVersion.V2, service.DetectNotifyVersion(v2NotifyBody));
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// 带有前导空白的 V3 回调应该被正确识别。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Fact]
|
||
public void V3NotifyWithLeadingWhitespace_ShouldBeDetectedAsV3()
|
||
{
|
||
var service = CreateService();
|
||
|
||
var v3NotifyBody = @"
|
||
{
|
||
""id"": ""test"",
|
||
""resource"": {
|
||
""ciphertext"": ""test""
|
||
}
|
||
}";
|
||
|
||
Assert.True(service.IsV3NotifyFormat(v3NotifyBody));
|
||
Assert.Equal(NotifyVersion.V3, service.DetectNotifyVersion(v3NotifyBody));
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// 带有前导空白的 V2 回调应该被正确识别。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Fact]
|
||
public void V2NotifyWithLeadingWhitespace_ShouldBeDetectedAsV2()
|
||
{
|
||
var service = CreateService();
|
||
|
||
var v2NotifyBody = @"
|
||
<xml>
|
||
<return_code>SUCCESS</return_code>
|
||
</xml>";
|
||
|
||
Assert.True(service.IsV2NotifyFormat(v2NotifyBody));
|
||
Assert.Equal(NotifyVersion.V2, service.DetectNotifyVersion(v2NotifyBody));
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
|
||
/// 无效的 JSON 不应该被识别为 V3。
|
||
/// **Validates: Requirements 4.1, 4.5**
|
||
/// </summary>
|
||
[Fact]
|
||
public void InvalidJson_ShouldNotBeV3()
|
||
{
|
||
var service = CreateService();
|
||
|
||
var invalidJson = @"{ ""resource"": ""missing closing brace""";
|
||
|
||
Assert.False(service.IsV3NotifyFormat(invalidJson));
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助方法
|
||
|
||
/// <summary>
|
||
/// 移除控制字符
|
||
/// </summary>
|
||
private static string RemoveControlChars(string input)
|
||
{
|
||
if (string.IsNullOrEmpty(input))
|
||
{
|
||
return input;
|
||
}
|
||
return new string(input.Where(c => !char.IsControl(c)).ToArray());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理字符串以用于 JSON
|
||
/// </summary>
|
||
private static string CleanJsonString(string input)
|
||
{
|
||
if (string.IsNullOrEmpty(input))
|
||
{
|
||
return input;
|
||
}
|
||
return input
|
||
.Replace("\\", "\\\\")
|
||
.Replace("\"", "\\\"")
|
||
.Replace("\n", "\\n")
|
||
.Replace("\r", "\\r")
|
||
.Replace("\t", "\\t");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理字符串以用于 XML
|
||
/// </summary>
|
||
private static string CleanXmlString(string input)
|
||
{
|
||
if (string.IsNullOrEmpty(input))
|
||
{
|
||
return input;
|
||
}
|
||
return input
|
||
.Replace("&", "&")
|
||
.Replace("<", "<")
|
||
.Replace(">", ">")
|
||
.Replace("\"", """)
|
||
.Replace("'", "'")
|
||
.Replace("\n", " ")
|
||
.Replace("\r", " ");
|
||
}
|
||
|
||
#endregion
|
||
}
|