using FsCheck; using FsCheck.Xunit; using HoneyBox.Core.Interfaces; using HoneyBox.Core.Services; using HoneyBox.Model.Data; using HoneyBox.Model.Models.Auth; using HoneyBox.Model.Models.Payment; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; namespace HoneyBox.Tests.Services; /// /// 微信支付签名服务属性测试 /// Feature: payment-integration, Property 1: 支付签名正确性 /// public class WechatPayServiceSignaturePropertyTests { private readonly WechatPaySettings _settings; private readonly AppSettings _appSettings; private readonly Mock> _mockLogger; private readonly Mock _mockConfigService; private readonly Mock _mockWechatService; private readonly Mock _mockRedisService; private readonly WechatPayService _wechatPayService; public WechatPayServiceSignaturePropertyTests() { _settings = new WechatPaySettings { DefaultMerchant = new WechatPayMerchantConfig { Name = "TestMerchant", MchId = "1234567890", AppId = "wx1234567890abcdef", Key = "test_secret_key_32_characters_ok", OrderPrefix = "TST", Weight = 1, NotifyUrl = "https://example.com/notify" }, Merchants = new List { new WechatPayMerchantConfig { Name = "Merchant1", MchId = "1111111111", AppId = "wx1111111111111111", Key = "merchant1_secret_key_32_chars_ok", OrderPrefix = "M01", Weight = 1 }, new WechatPayMerchantConfig { Name = "Merchant2", MchId = "2222222222", AppId = "wx2222222222222222", Key = "merchant2_secret_key_32_chars_ok", OrderPrefix = "M02", Weight = 1 } } }; _mockLogger = new Mock>(); _mockConfigService = new Mock(); _mockConfigService.Setup(x => x.GetMerchantByOrderNo(It.IsAny())) .Returns(_settings.DefaultMerchant); _mockWechatService = new Mock(); _mockRedisService = new Mock(); _appSettings = new AppSettings { IsTestEnvironment = false }; var options = Options.Create(_settings); var dbOptions = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; var dbContext = new HoneyBoxDbContext(dbOptions); _wechatPayService = new WechatPayService( dbContext, new HttpClient(), _mockLogger.Object, _mockConfigService.Object, _mockWechatService.Object, _mockRedisService.Object, options, _appSettings); } /// /// Property 1: 支付签名正确性 /// For any set of payment parameters and secret key, generating a signature and then /// verifying it with the same parameters should return true. /// Validates: Requirements 1.4, 7.1, 7.2 /// [Property(MaxTest = 100)] public bool SignatureRoundTrip_ShouldVerifySuccessfully( NonEmptyString appId, NonEmptyString mchId, NonEmptyString nonceStr, PositiveInt totalFee, NonEmptyString outTradeNo) { // Arrange: Create a set of payment parameters var parameters = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item }, { "nonce_str", nonceStr.Item }, { "body", "Test Payment" }, { "out_trade_no", outTradeNo.Item }, { "total_fee", totalFee.Item.ToString() }, { "spbill_create_ip", "127.0.0.1" }, { "trade_type", "JSAPI" }, { "openid", "test_openid_123" } }; // Act: Generate signature var sign = _wechatPayService.MakeSign(parameters); // Assert: Verify the signature return _wechatPayService.VerifySign(parameters, sign); } /// /// Property 1 (continued): 签名一致性 /// For any set of parameters, generating the signature twice should produce the same result. /// Validates: Requirements 1.4, 7.1 /// [Property(MaxTest = 100)] public bool SignatureConsistency_SameParametersShouldProduceSameSignature( NonEmptyString appId, NonEmptyString mchId, NonEmptyString nonceStr) { // Arrange var parameters = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item }, { "nonce_str", nonceStr.Item }, { "body", "Test Payment" } }; // Act: Generate signature twice var sign1 = _wechatPayService.MakeSign(parameters); var sign2 = _wechatPayService.MakeSign(parameters); // Assert: Both signatures should be identical return sign1 == sign2; } /// /// Property 1 (continued): 签名唯一性 /// For different parameters, the signatures should be different. /// Validates: Requirements 7.1, 7.2 /// [Property(MaxTest = 100)] public bool SignatureUniqueness_DifferentParametersShouldProduceDifferentSignatures( NonEmptyString appId1, NonEmptyString appId2) { // Skip if the two appIds are the same if (appId1.Item == appId2.Item) return true; // Arrange var parameters1 = new Dictionary { { "appid", appId1.Item }, { "mch_id", "1234567890" }, { "nonce_str", "test_nonce" }, { "body", "Test Payment" } }; var parameters2 = new Dictionary { { "appid", appId2.Item }, { "mch_id", "1234567890" }, { "nonce_str", "test_nonce" }, { "body", "Test Payment" } }; // Act var sign1 = _wechatPayService.MakeSign(parameters1); var sign2 = _wechatPayService.MakeSign(parameters2); // Assert: Signatures should be different return sign1 != sign2; } /// /// Property 1 (continued): 签名验证失败 /// For any valid signature, modifying it should cause verification to fail. /// Validates: Requirements 7.2, 7.4 /// [Property(MaxTest = 100)] public bool SignatureVerification_ModifiedSignatureShouldFail( NonEmptyString appId, NonEmptyString mchId) { // Arrange var parameters = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item }, { "nonce_str", "test_nonce" }, { "body", "Test Payment" } }; // Act: Generate valid signature var validSign = _wechatPayService.MakeSign(parameters); // Modify the signature (change first character) var modifiedSign = validSign.Length > 0 ? (validSign[0] == 'A' ? 'B' : 'A') + validSign.Substring(1) : "INVALID"; // Assert: Modified signature should fail verification return !_wechatPayService.VerifySign(parameters, modifiedSign); } /// /// Property 1 (continued): 空签名验证 /// Empty or null signatures should always fail verification. /// Validates: Requirements 7.2, 7.4 /// [Property(MaxTest = 100)] public bool SignatureVerification_EmptySignatureShouldFail( NonEmptyString appId, NonEmptyString mchId) { // Arrange var parameters = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item }, { "nonce_str", "test_nonce" } }; // Assert: Empty and null signatures should fail var emptyFails = !_wechatPayService.VerifySign(parameters, ""); var nullFails = !_wechatPayService.VerifySign(parameters, null!); return emptyFails && nullFails; } /// /// Property 1 (continued): 多商户签名支持 /// For any merchant key, signatures generated with that key should only verify with the same key. /// Validates: Requirements 1.5, 7.3 /// [Property(MaxTest = 100)] public bool SignatureMultiMerchant_DifferentKeysShouldProduceDifferentSignatures( NonEmptyString appId, NonEmptyString mchId) { // Arrange var parameters = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item }, { "nonce_str", "test_nonce" }, { "body", "Test Payment" } }; var key1 = _settings.DefaultMerchant.Key; var key2 = _settings.Merchants[0].Key; // Act: Generate signatures with different keys var sign1 = _wechatPayService.MakeSign(parameters, key1); var sign2 = _wechatPayService.MakeSign(parameters, key2); // Assert: Signatures should be different // And each signature should only verify with its own key var sign1VerifiesWithKey1 = _wechatPayService.VerifySign(parameters, sign1, key1); var sign1FailsWithKey2 = !_wechatPayService.VerifySign(parameters, sign1, key2); var sign2VerifiesWithKey2 = _wechatPayService.VerifySign(parameters, sign2, key2); var sign2FailsWithKey1 = !_wechatPayService.VerifySign(parameters, sign2, key1); return sign1 != sign2 && sign1VerifiesWithKey1 && sign1FailsWithKey2 && sign2VerifiesWithKey2 && sign2FailsWithKey1; } /// /// Property 1 (continued): 签名格式正确性 /// Generated signatures should be 32-character uppercase hexadecimal strings (MD5 format). /// Validates: Requirements 1.4, 7.1 /// [Property(MaxTest = 100)] public bool SignatureFormat_ShouldBe32CharUppercaseHex( NonEmptyString appId, NonEmptyString mchId, NonEmptyString nonceStr) { // Arrange var parameters = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item }, { "nonce_str", nonceStr.Item } }; // Act var sign = _wechatPayService.MakeSign(parameters); // Assert: Signature should be 32 characters, uppercase, hexadecimal var isCorrectLength = sign.Length == 32; var isUppercase = sign == sign.ToUpper(); var isHexadecimal = sign.All(c => "0123456789ABCDEF".Contains(c)); return isCorrectLength && isUppercase && isHexadecimal; } /// /// Property 1 (continued): 参数顺序无关性 /// The order of parameters should not affect the signature (due to sorting). /// Validates: Requirements 7.1 /// [Property(MaxTest = 100)] public bool SignatureOrderIndependence_ParameterOrderShouldNotAffectSignature( NonEmptyString appId, NonEmptyString mchId, NonEmptyString nonceStr) { // Arrange: Create parameters in different orders var parameters1 = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item }, { "nonce_str", nonceStr.Item } }; var parameters2 = new Dictionary { { "nonce_str", nonceStr.Item }, { "appid", appId.Item }, { "mch_id", mchId.Item } }; var parameters3 = new Dictionary { { "mch_id", mchId.Item }, { "nonce_str", nonceStr.Item }, { "appid", appId.Item } }; // Act var sign1 = _wechatPayService.MakeSign(parameters1); var sign2 = _wechatPayService.MakeSign(parameters2); var sign3 = _wechatPayService.MakeSign(parameters3); // Assert: All signatures should be identical return sign1 == sign2 && sign2 == sign3; } /// /// Property 1 (continued): 空值参数过滤 /// Empty values should be filtered out and not affect the signature. /// Validates: Requirements 7.1 /// [Property(MaxTest = 100)] public bool SignatureEmptyValueFiltering_EmptyValuesShouldBeIgnored( NonEmptyString appId, NonEmptyString mchId) { // Arrange: Parameters with and without empty values var parametersWithEmpty = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item }, { "empty_field", "" }, { "null_field", null! } }; var parametersWithoutEmpty = new Dictionary { { "appid", appId.Item }, { "mch_id", mchId.Item } }; // Act var signWithEmpty = _wechatPayService.MakeSign(parametersWithEmpty); var signWithoutEmpty = _wechatPayService.MakeSign(parametersWithoutEmpty); // Assert: Signatures should be identical return signWithEmpty == signWithoutEmpty; } }