using System.Text.Json;
using FsCheck;
using FsCheck.Xunit;
using MiAssessment.Model.Models.Payment;
using Xunit;
namespace MiAssessment.Tests.Services;
///
/// 微信支付 V3 请求字段完整性属性测试
/// **Feature: wechat-pay-v3-upgrade**
///
public class WechatPayV3RequestPropertyTests
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
#region Property 6: V3 请求字段完整性
///
/// **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**
///
[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;
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
/// *For any* V3 JSAPI 请求,金额字段应该是正整数(单位:分)。
/// **Validates: Requirements 3.2**
///
[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;
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
/// *For any* V3 JSAPI 请求,货币类型默认应该是 CNY。
/// **Validates: Requirements 3.2**
///
[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";
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
/// *For any* V3 JSAPI 请求,可选字段 attach 为 null 时不应该出现在 JSON 中。
/// **Validates: Requirements 3.2**
///
[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 _));
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
/// *For any* V3 JSAPI 请求,可选字段 attach 有值时应该出现在 JSON 中。
/// **Validates: Requirements 3.2**
///
[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 支付参数完整性
///
/// **Feature: wechat-pay-v3-upgrade, Property 7: V3 支付参数完整性**
/// *For any* 成功的 V3 下单响应,返回给前端的支付参数应该包含:
/// timeStamp、nonceStr、package、signType(RSA)、paySign。
/// **Validates: Requirements 3.4**
///
[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);
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 7: V3 支付参数完整性**
/// V3 支付参数的 signType 应该是 RSA(而不是 V2 的 MD5)。
/// **Validates: Requirements 3.4**
///
[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);
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 7: V3 支付参数完整性**
/// V3 支付参数的 package 格式应该是 prepay_id=xxx。
/// **Validates: Requirements 3.4**
///
[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 请求序列化测试
///
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
/// V3 请求序列化后的 JSON 字段名应该使用 snake_case 格式。
/// **Validates: Requirements 3.2**
///
[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);
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 6: V3 请求字段完整性**
/// V3 请求序列化后应该是有效的 JSON。
/// **Validates: Requirements 3.2**
///
[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
}