HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/WechatPayV3NotifyFormatPropertyTests.cs
2026-01-25 20:28:11 +08:00

411 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;")
.Replace("\"", "&quot;")
.Replace("'", "&apos;")
.Replace("\n", " ")
.Replace("\r", " ");
}
#endregion
}