using System.Security.Cryptography; using System.Text; using FsCheck; using FsCheck.Xunit; using MiAssessment.Core.Interfaces; using MiAssessment.Core.Services; using MiAssessment.Model.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace MiAssessment.Tests.Services; /// /// 微信支付 V3 签名属性测试 /// **Feature: wechat-pay-v3-upgrade** /// public class WechatPayV3SignaturePropertyTests { // 测试用 RSA 密钥对(仅用于测试) private static readonly string TestPrivateKey; private static readonly string TestPublicKey; static WechatPayV3SignaturePropertyTests() { // 生成测试用 RSA 密钥对 using var rsa = RSA.Create(2048); TestPrivateKey = ExportPrivateKeyPem(rsa); TestPublicKey = ExportPublicKeyPem(rsa); } private static string ExportPrivateKeyPem(RSA rsa) { var privateKeyBytes = rsa.ExportRSAPrivateKey(); var base64 = Convert.ToBase64String(privateKeyBytes); var sb = new StringBuilder(); sb.AppendLine("-----BEGIN RSA PRIVATE KEY-----"); for (int i = 0; i < base64.Length; i += 64) { sb.AppendLine(base64.Substring(i, Math.Min(64, base64.Length - i))); } sb.AppendLine("-----END RSA PRIVATE KEY-----"); return sb.ToString(); } private static string ExportPublicKeyPem(RSA rsa) { var publicKeyBytes = rsa.ExportRSAPublicKey(); var base64 = Convert.ToBase64String(publicKeyBytes); var sb = new StringBuilder(); sb.AppendLine("-----BEGIN RSA PUBLIC KEY-----"); for (int i = 0; i < base64.Length; i += 64) { sb.AppendLine(base64.Substring(i, Math.Min(64, base64.Length - i))); } sb.AppendLine("-----END RSA PUBLIC KEY-----"); return sb.ToString(); } private IWechatPayV3Service CreateService() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; var dbContext = new MiAssessmentDbContext(options); var httpClient = new HttpClient(); var logger = Mock.Of>(); var configService = Mock.Of(); return new WechatPayV3Service(dbContext, httpClient, logger, configService); } #region Property 5: V3 请求签名正确性 /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// *For any* V3 请求数据,使用相同的私钥和参数生成的签名应该是确定性的 /// (相同输入产生相同输出)。 /// **Validates: Requirements 3.3** /// [Property(MaxTest = 100)] public bool V3Signature_SameInputs_ShouldProduceSameOutput( NonEmptyString method, NonEmptyString url, NonEmptyString body, PositiveInt seed) { var service = CreateService(); // 使用固定的时间戳和随机串以确保确定性 var timestamp = seed.Get.ToString(); var nonce = $"nonce{seed.Get}"; // 清理输入(移除可能导致问题的字符) var cleanMethod = method.Get.Replace("\n", "").Replace("\r", "").ToUpper(); var cleanUrl = "/" + url.Get.Replace("\n", "").Replace("\r", "").TrimStart('/'); var cleanBody = body.Get.Replace("\n", " ").Replace("\r", ""); // 生成两次签名 var signature1 = service.GenerateSignature(cleanMethod, cleanUrl, timestamp, nonce, cleanBody, TestPrivateKey); var signature2 = service.GenerateSignature(cleanMethod, cleanUrl, timestamp, nonce, cleanBody, TestPrivateKey); // 相同输入应该产生相同输出 return signature1 == signature2; } /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// *For any* V3 请求数据,生成的签名应该是有效的 Base64 字符串。 /// **Validates: Requirements 3.3** /// [Property(MaxTest = 100)] public bool V3Signature_ShouldBeValidBase64( NonEmptyString method, NonEmptyString url, NonEmptyString body, PositiveInt seed) { var service = CreateService(); var timestamp = seed.Get.ToString(); var nonce = $"nonce{seed.Get}"; var cleanMethod = method.Get.Replace("\n", "").Replace("\r", "").ToUpper(); var cleanUrl = "/" + url.Get.Replace("\n", "").Replace("\r", "").TrimStart('/'); var cleanBody = body.Get.Replace("\n", " ").Replace("\r", ""); var signature = service.GenerateSignature(cleanMethod, cleanUrl, timestamp, nonce, cleanBody, TestPrivateKey); // 验证是有效的 Base64 字符串 try { var bytes = Convert.FromBase64String(signature); return bytes.Length > 0; } catch { return false; } } /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// *For any* V3 请求数据,不同的输入应该产生不同的签名。 /// **Validates: Requirements 3.3** /// [Property(MaxTest = 100)] public bool V3Signature_DifferentInputs_ShouldProduceDifferentOutput( NonEmptyString body1, NonEmptyString body2, PositiveInt seed) { // 如果两个 body 相同,跳过测试 if (body1.Get == body2.Get) return true; var service = CreateService(); var timestamp = seed.Get.ToString(); var nonce = $"nonce{seed.Get}"; var method = "POST"; var url = "/v3/pay/transactions/jsapi"; var cleanBody1 = body1.Get.Replace("\n", " ").Replace("\r", ""); var cleanBody2 = body2.Get.Replace("\n", " ").Replace("\r", ""); var signature1 = service.GenerateSignature(method, url, timestamp, nonce, cleanBody1, TestPrivateKey); var signature2 = service.GenerateSignature(method, url, timestamp, nonce, cleanBody2, TestPrivateKey); // 不同输入应该产生不同输出 return signature1 != signature2; } /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// *For any* V3 请求数据,签名应该可以使用对应的公钥验证。 /// **Validates: Requirements 3.3** /// [Property(MaxTest = 100)] public bool V3Signature_ShouldBeVerifiableWithPublicKey( NonEmptyString method, NonEmptyString url, NonEmptyString body, PositiveInt seed) { var service = CreateService(); var timestamp = seed.Get.ToString(); var nonce = $"nonce{seed.Get}"; var cleanMethod = method.Get.Replace("\n", "").Replace("\r", "").ToUpper(); var cleanUrl = "/" + url.Get.Replace("\n", "").Replace("\r", "").TrimStart('/'); var cleanBody = body.Get.Replace("\n", " ").Replace("\r", ""); // 生成签名 var signature = service.GenerateSignature(cleanMethod, cleanUrl, timestamp, nonce, cleanBody, TestPrivateKey); // 构建签名字符串(与 GenerateSignature 方法中的格式一致) var signatureString = $"{cleanMethod}\n{cleanUrl}\n{timestamp}\n{nonce}\n{cleanBody}\n"; // 使用公钥验证签名 try { using var rsa = RSA.Create(); rsa.ImportFromPem(TestPublicKey); var signatureBytes = Convert.FromBase64String(signature); return rsa.VerifyData( Encoding.UTF8.GetBytes(signatureString), signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } catch { return false; } } #endregion #region 辅助方法测试 /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// GenerateNonceStr 应该生成指定长度的随机字符串。 /// **Validates: Requirements 3.3** /// [Property(MaxTest = 100)] public bool GenerateNonceStr_ShouldHaveCorrectLength(PositiveInt length) { var service = CreateService(); var actualLength = Math.Min(Math.Max(length.Get % 64, 1), 64); // 限制在 1-64 之间 var nonceStr = service.GenerateNonceStr(actualLength); return nonceStr.Length == actualLength; } /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// GenerateNonceStr 应该只包含字母和数字。 /// **Validates: Requirements 3.3** /// [Property(MaxTest = 100)] public bool GenerateNonceStr_ShouldContainOnlyAlphanumeric(PositiveInt seed) { var service = CreateService(); var nonceStr = service.GenerateNonceStr(32); return nonceStr.All(c => char.IsLetterOrDigit(c)); } /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// GetTimestamp 应该返回有效的 Unix 时间戳。 /// **Validates: Requirements 3.3** /// [Fact] public void GetTimestamp_ShouldReturnValidUnixTimestamp() { var service = CreateService(); var timestamp = service.GetTimestamp(); // 应该是数字字符串 Assert.True(long.TryParse(timestamp, out var timestampValue)); // 应该是合理的时间戳(2020年之后,2100年之前) var minTimestamp = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeSeconds(); var maxTimestamp = new DateTimeOffset(2100, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeSeconds(); Assert.True(timestampValue >= minTimestamp && timestampValue <= maxTimestamp); } #endregion #region 签名格式验证 /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// 签名字符串格式应该符合微信 V3 规范:HTTP方法\nURL\n时间戳\n随机串\n请求体\n /// **Validates: Requirements 3.3** /// [Fact] public void V3Signature_Format_ShouldMatchWechatSpec() { var service = CreateService(); var method = "POST"; var url = "/v3/pay/transactions/jsapi"; var timestamp = "1609459200"; var nonce = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS"; var body = "{\"appid\":\"wx1234567890\",\"mchid\":\"1234567890\"}"; // 生成签名 var signature = service.GenerateSignature(method, url, timestamp, nonce, body, TestPrivateKey); // 验证签名不为空 Assert.False(string.IsNullOrEmpty(signature)); // 验证是有效的 Base64 var signatureBytes = Convert.FromBase64String(signature); Assert.True(signatureBytes.Length > 0); // 验证签名长度(RSA-2048 签名应该是 256 字节) Assert.Equal(256, signatureBytes.Length); } /// /// **Feature: wechat-pay-v3-upgrade, Property 5: V3 请求签名正确性** /// 小程序支付签名格式应该符合微信规范:appId\n时间戳\n随机串\nprepay_id=xxx\n /// **Validates: Requirements 3.4** /// [Fact] public void GeneratePaySign_Format_ShouldMatchWechatSpec() { var service = CreateService(); var appId = "wx1234567890"; var timestamp = "1609459200"; var nonceStr = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS"; var prepayId = "wx201410272009395522657a690389285100"; // 生成支付签名 var paySign = service.GeneratePaySign(appId, timestamp, nonceStr, prepayId, TestPrivateKey); // 验证签名不为空 Assert.False(string.IsNullOrEmpty(paySign)); // 验证是有效的 Base64 var signatureBytes = Convert.FromBase64String(paySign); Assert.True(signatureBytes.Length > 0); // 验证签名长度(RSA-2048 签名应该是 256 字节) Assert.Equal(256, signatureBytes.Length); // 验证签名可以用公钥验证 var signatureString = $"{appId}\n{timestamp}\n{nonceStr}\nprepay_id={prepayId}\n"; using var rsa = RSA.Create(); rsa.ImportFromPem(TestPublicKey); var isValid = rsa.VerifyData( Encoding.UTF8.GetBytes(signatureString), signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); Assert.True(isValid); } #endregion }