368 lines
13 KiB
C#
368 lines
13 KiB
C#
using System.Text.Json;
|
||
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using HoneyBox.Model.Models.Payment;
|
||
using Xunit;
|
||
|
||
namespace HoneyBox.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// 微信支付 V3 请求字段完整性属性测试
|
||
/// **Feature: wechat-pay-v3-upgrade**
|
||
/// </summary>
|
||
public class WechatPayV3RequestPropertyTests
|
||
{
|
||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||
{
|
||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||
};
|
||
|
||
#region Property 6: V3 请求字段完整性
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
|
||
/// *For any* V3 JSAPI 下单请求,构建的请求体应该包含所有必要字段:
|
||
/// appid、mchid、description、out_trade_no、notify_url、amount、payer。
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3JsapiRequest_ShouldContainAllRequiredFields(
|
||
NonEmptyString appId,
|
||
NonEmptyString mchId,
|
||
NonEmptyString description,
|
||
NonEmptyString outTradeNo,
|
||
NonEmptyString notifyUrl,
|
||
PositiveInt totalAmount,
|
||
NonEmptyString openId)
|
||
{
|
||
// 创建 V3 JSAPI 请求
|
||
var request = new WechatPayV3JsapiRequest
|
||
{
|
||
AppId = appId.Get,
|
||
MchId = mchId.Get,
|
||
Description = description.Get,
|
||
OutTradeNo = outTradeNo.Get,
|
||
NotifyUrl = notifyUrl.Get,
|
||
Amount = new WechatPayV3Amount
|
||
{
|
||
Total = totalAmount.Get,
|
||
Currency = "CNY"
|
||
},
|
||
Payer = new WechatPayV3Payer
|
||
{
|
||
OpenId = openId.Get
|
||
}
|
||
};
|
||
|
||
// 序列化为 JSON
|
||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||
|
||
// 验证所有必要字段都存在
|
||
using var doc = JsonDocument.Parse(json);
|
||
var root = doc.RootElement;
|
||
|
||
// 检查所有必要字段
|
||
var hasAppId = root.TryGetProperty("appid", out var appIdProp) && !string.IsNullOrEmpty(appIdProp.GetString());
|
||
var hasMchId = root.TryGetProperty("mchid", out var mchIdProp) && !string.IsNullOrEmpty(mchIdProp.GetString());
|
||
var hasDescription = root.TryGetProperty("description", out var descProp) && !string.IsNullOrEmpty(descProp.GetString());
|
||
var hasOutTradeNo = root.TryGetProperty("out_trade_no", out var tradeProp) && !string.IsNullOrEmpty(tradeProp.GetString());
|
||
var hasNotifyUrl = root.TryGetProperty("notify_url", out var notifyProp) && !string.IsNullOrEmpty(notifyProp.GetString());
|
||
var hasAmount = root.TryGetProperty("amount", out var amountProp) && amountProp.ValueKind == JsonValueKind.Object;
|
||
var hasPayer = root.TryGetProperty("payer", out var payerProp) && payerProp.ValueKind == JsonValueKind.Object;
|
||
|
||
// 检查 amount 子字段
|
||
var hasTotal = hasAmount && amountProp.TryGetProperty("total", out var totalProp) && totalProp.ValueKind == JsonValueKind.Number;
|
||
var hasCurrency = hasAmount && amountProp.TryGetProperty("currency", out var currencyProp) && !string.IsNullOrEmpty(currencyProp.GetString());
|
||
|
||
// 检查 payer 子字段
|
||
var hasOpenId = hasPayer && payerProp.TryGetProperty("openid", out var openIdProp) && !string.IsNullOrEmpty(openIdProp.GetString());
|
||
|
||
return hasAppId && hasMchId && hasDescription && hasOutTradeNo && hasNotifyUrl &&
|
||
hasAmount && hasTotal && hasCurrency && hasPayer && hasOpenId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
|
||
/// *For any* V3 JSAPI 请求,金额字段应该是正整数(单位:分)。
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3JsapiRequest_AmountShouldBePositiveInteger(PositiveInt totalAmount)
|
||
{
|
||
var request = new WechatPayV3JsapiRequest
|
||
{
|
||
AppId = "wx1234567890",
|
||
MchId = "1234567890",
|
||
Description = "测试商品",
|
||
OutTradeNo = "ORDER123456",
|
||
NotifyUrl = "https://example.com/notify",
|
||
Amount = new WechatPayV3Amount
|
||
{
|
||
Total = totalAmount.Get,
|
||
Currency = "CNY"
|
||
},
|
||
Payer = new WechatPayV3Payer
|
||
{
|
||
OpenId = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
|
||
}
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||
using var doc = JsonDocument.Parse(json);
|
||
var root = doc.RootElement;
|
||
|
||
var amount = root.GetProperty("amount");
|
||
var total = amount.GetProperty("total").GetInt32();
|
||
|
||
return total > 0 && total == totalAmount.Get;
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
|
||
/// *For any* V3 JSAPI 请求,货币类型默认应该是 CNY。
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3JsapiRequest_CurrencyShouldDefaultToCNY(PositiveInt totalAmount)
|
||
{
|
||
var request = new WechatPayV3JsapiRequest
|
||
{
|
||
AppId = "wx1234567890",
|
||
MchId = "1234567890",
|
||
Description = "测试商品",
|
||
OutTradeNo = "ORDER123456",
|
||
NotifyUrl = "https://example.com/notify",
|
||
Amount = new WechatPayV3Amount
|
||
{
|
||
Total = totalAmount.Get
|
||
// Currency 使用默认值
|
||
},
|
||
Payer = new WechatPayV3Payer
|
||
{
|
||
OpenId = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
|
||
}
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||
using var doc = JsonDocument.Parse(json);
|
||
var root = doc.RootElement;
|
||
|
||
var amount = root.GetProperty("amount");
|
||
var currency = amount.GetProperty("currency").GetString();
|
||
|
||
return currency == "CNY";
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
|
||
/// *For any* V3 JSAPI 请求,可选字段 attach 为 null 时不应该出现在 JSON 中。
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Fact]
|
||
public void V3JsapiRequest_NullAttach_ShouldNotAppearInJson()
|
||
{
|
||
var request = new WechatPayV3JsapiRequest
|
||
{
|
||
AppId = "wx1234567890",
|
||
MchId = "1234567890",
|
||
Description = "测试商品",
|
||
OutTradeNo = "ORDER123456",
|
||
NotifyUrl = "https://example.com/notify",
|
||
Amount = new WechatPayV3Amount { Total = 100, Currency = "CNY" },
|
||
Payer = new WechatPayV3Payer { OpenId = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o" },
|
||
Attach = null // 可选字段为 null
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||
using var doc = JsonDocument.Parse(json);
|
||
var root = doc.RootElement;
|
||
|
||
// attach 为 null 时不应该出现在 JSON 中
|
||
Assert.False(root.TryGetProperty("attach", out _));
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
|
||
/// *For any* V3 JSAPI 请求,可选字段 attach 有值时应该出现在 JSON 中。
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3JsapiRequest_NonNullAttach_ShouldAppearInJson(NonEmptyString attach)
|
||
{
|
||
var request = new WechatPayV3JsapiRequest
|
||
{
|
||
AppId = "wx1234567890",
|
||
MchId = "1234567890",
|
||
Description = "测试商品",
|
||
OutTradeNo = "ORDER123456",
|
||
NotifyUrl = "https://example.com/notify",
|
||
Amount = new WechatPayV3Amount { Total = 100, Currency = "CNY" },
|
||
Payer = new WechatPayV3Payer { OpenId = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o" },
|
||
Attach = attach.Get
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||
using var doc = JsonDocument.Parse(json);
|
||
var root = doc.RootElement;
|
||
|
||
// attach 有值时应该出现在 JSON 中
|
||
return root.TryGetProperty("attach", out var attachProp) &&
|
||
attachProp.GetString() == attach.Get;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 7: V3 支付参数完整性
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 7: V3 支付参数完整性**
|
||
/// *For any* 成功的 V3 下单响应,返回给前端的支付参数应该包含:
|
||
/// timeStamp、nonceStr、package、signType(RSA)、paySign。
|
||
/// **Validates: Requirements 3.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3PayData_ShouldContainAllRequiredFields(
|
||
NonEmptyString appId,
|
||
NonEmptyString timeStamp,
|
||
NonEmptyString nonceStr,
|
||
NonEmptyString prepayId,
|
||
NonEmptyString paySign)
|
||
{
|
||
// 模拟 V3 支付数据
|
||
var payData = new WechatPayData
|
||
{
|
||
AppId = appId.Get,
|
||
TimeStamp = timeStamp.Get,
|
||
NonceStr = nonceStr.Get,
|
||
Package = $"prepay_id={prepayId.Get}",
|
||
SignType = "RSA",
|
||
PaySign = paySign.Get,
|
||
IsWeixin = 1
|
||
};
|
||
|
||
// 验证所有必要字段
|
||
return !string.IsNullOrEmpty(payData.AppId) &&
|
||
!string.IsNullOrEmpty(payData.TimeStamp) &&
|
||
!string.IsNullOrEmpty(payData.NonceStr) &&
|
||
!string.IsNullOrEmpty(payData.Package) &&
|
||
payData.Package.StartsWith("prepay_id=") &&
|
||
payData.SignType == "RSA" &&
|
||
!string.IsNullOrEmpty(payData.PaySign);
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 7: V3 支付参数完整性**
|
||
/// V3 支付参数的 signType 应该是 RSA(而不是 V2 的 MD5)。
|
||
/// **Validates: Requirements 3.4**
|
||
/// </summary>
|
||
[Fact]
|
||
public void V3PayData_SignType_ShouldBeRSA()
|
||
{
|
||
var payData = new WechatPayData
|
||
{
|
||
AppId = "wx1234567890",
|
||
TimeStamp = "1609459200",
|
||
NonceStr = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
|
||
Package = "prepay_id=wx201410272009395522657a690389285100",
|
||
SignType = "RSA", // V3 使用 RSA
|
||
PaySign = "base64signature",
|
||
IsWeixin = 1
|
||
};
|
||
|
||
Assert.Equal("RSA", payData.SignType);
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 7: V3 支付参数完整性**
|
||
/// V3 支付参数的 package 格式应该是 prepay_id=xxx。
|
||
/// **Validates: Requirements 3.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3PayData_Package_ShouldHaveCorrectFormat(NonEmptyString prepayId)
|
||
{
|
||
var package = $"prepay_id={prepayId.Get}";
|
||
|
||
return package.StartsWith("prepay_id=") &&
|
||
package.Length > "prepay_id=".Length;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 请求序列化测试
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
|
||
/// V3 请求序列化后的 JSON 字段名应该使用 snake_case 格式。
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Fact]
|
||
public void V3JsapiRequest_Serialization_ShouldUseSnakeCase()
|
||
{
|
||
var request = new WechatPayV3JsapiRequest
|
||
{
|
||
AppId = "wx1234567890",
|
||
MchId = "1234567890",
|
||
Description = "测试商品",
|
||
OutTradeNo = "ORDER123456",
|
||
NotifyUrl = "https://example.com/notify",
|
||
Amount = new WechatPayV3Amount { Total = 100, Currency = "CNY" },
|
||
Payer = new WechatPayV3Payer { OpenId = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o" }
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||
|
||
// 验证使用 snake_case
|
||
Assert.Contains("\"appid\"", json);
|
||
Assert.Contains("\"mchid\"", json);
|
||
Assert.Contains("\"description\"", json);
|
||
Assert.Contains("\"out_trade_no\"", json);
|
||
Assert.Contains("\"notify_url\"", json);
|
||
Assert.Contains("\"amount\"", json);
|
||
Assert.Contains("\"payer\"", json);
|
||
Assert.Contains("\"total\"", json);
|
||
Assert.Contains("\"currency\"", json);
|
||
Assert.Contains("\"openid\"", json);
|
||
}
|
||
|
||
/// <summary>
|
||
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
|
||
/// V3 请求序列化后应该是有效的 JSON。
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool V3JsapiRequest_Serialization_ShouldProduceValidJson(
|
||
NonEmptyString appId,
|
||
NonEmptyString mchId,
|
||
NonEmptyString description,
|
||
NonEmptyString outTradeNo,
|
||
NonEmptyString notifyUrl,
|
||
PositiveInt totalAmount,
|
||
NonEmptyString openId)
|
||
{
|
||
var request = new WechatPayV3JsapiRequest
|
||
{
|
||
AppId = appId.Get,
|
||
MchId = mchId.Get,
|
||
Description = description.Get,
|
||
OutTradeNo = outTradeNo.Get,
|
||
NotifyUrl = notifyUrl.Get,
|
||
Amount = new WechatPayV3Amount { Total = totalAmount.Get, Currency = "CNY" },
|
||
Payer = new WechatPayV3Payer { OpenId = openId.Get }
|
||
};
|
||
|
||
try
|
||
{
|
||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||
using var doc = JsonDocument.Parse(json);
|
||
return doc.RootElement.ValueKind == JsonValueKind.Object;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|