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 }