HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/WechatPayServiceSignaturePropertyTests.cs
2026-02-01 19:30:51 +08:00

394 lines
13 KiB
C#

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;
/// <summary>
/// 微信支付签名服务属性测试
/// Feature: payment-integration, Property 1: 支付签名正确性
/// </summary>
public class WechatPayServiceSignaturePropertyTests
{
private readonly WechatPaySettings _settings;
private readonly AppSettings _appSettings;
private readonly Mock<ILogger<WechatPayService>> _mockLogger;
private readonly Mock<IWechatPayConfigService> _mockConfigService;
private readonly Mock<IWechatService> _mockWechatService;
private readonly Mock<IRedisService> _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<WechatPayMerchantConfig>
{
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<ILogger<WechatPayService>>();
_mockConfigService = new Mock<IWechatPayConfigService>();
_mockConfigService.Setup(x => x.GetMerchantByOrderNo(It.IsAny<string>()))
.Returns(_settings.DefaultMerchant);
_mockWechatService = new Mock<IWechatService>();
_mockRedisService = new Mock<IRedisService>();
_appSettings = new AppSettings { IsTestEnvironment = false };
var options = Options.Create(_settings);
var dbOptions = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.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);
}
/// <summary>
/// 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
/// </summary>
[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<string, string>
{
{ "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);
}
/// <summary>
/// Property 1 (continued): 签名一致性
/// For any set of parameters, generating the signature twice should produce the same result.
/// Validates: Requirements 1.4, 7.1
/// </summary>
[Property(MaxTest = 100)]
public bool SignatureConsistency_SameParametersShouldProduceSameSignature(
NonEmptyString appId,
NonEmptyString mchId,
NonEmptyString nonceStr)
{
// Arrange
var parameters = new Dictionary<string, string>
{
{ "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;
}
/// <summary>
/// Property 1 (continued): 签名唯一性
/// For different parameters, the signatures should be different.
/// Validates: Requirements 7.1, 7.2
/// </summary>
[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<string, string>
{
{ "appid", appId1.Item },
{ "mch_id", "1234567890" },
{ "nonce_str", "test_nonce" },
{ "body", "Test Payment" }
};
var parameters2 = new Dictionary<string, string>
{
{ "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;
}
/// <summary>
/// Property 1 (continued): 签名验证失败
/// For any valid signature, modifying it should cause verification to fail.
/// Validates: Requirements 7.2, 7.4
/// </summary>
[Property(MaxTest = 100)]
public bool SignatureVerification_ModifiedSignatureShouldFail(
NonEmptyString appId,
NonEmptyString mchId)
{
// Arrange
var parameters = new Dictionary<string, string>
{
{ "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);
}
/// <summary>
/// Property 1 (continued): 空签名验证
/// Empty or null signatures should always fail verification.
/// Validates: Requirements 7.2, 7.4
/// </summary>
[Property(MaxTest = 100)]
public bool SignatureVerification_EmptySignatureShouldFail(
NonEmptyString appId,
NonEmptyString mchId)
{
// Arrange
var parameters = new Dictionary<string, string>
{
{ "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;
}
/// <summary>
/// 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
/// </summary>
[Property(MaxTest = 100)]
public bool SignatureMultiMerchant_DifferentKeysShouldProduceDifferentSignatures(
NonEmptyString appId,
NonEmptyString mchId)
{
// Arrange
var parameters = new Dictionary<string, string>
{
{ "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;
}
/// <summary>
/// Property 1 (continued): 签名格式正确性
/// Generated signatures should be 32-character uppercase hexadecimal strings (MD5 format).
/// Validates: Requirements 1.4, 7.1
/// </summary>
[Property(MaxTest = 100)]
public bool SignatureFormat_ShouldBe32CharUppercaseHex(
NonEmptyString appId,
NonEmptyString mchId,
NonEmptyString nonceStr)
{
// Arrange
var parameters = new Dictionary<string, string>
{
{ "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;
}
/// <summary>
/// Property 1 (continued): 参数顺序无关性
/// The order of parameters should not affect the signature (due to sorting).
/// Validates: Requirements 7.1
/// </summary>
[Property(MaxTest = 100)]
public bool SignatureOrderIndependence_ParameterOrderShouldNotAffectSignature(
NonEmptyString appId,
NonEmptyString mchId,
NonEmptyString nonceStr)
{
// Arrange: Create parameters in different orders
var parameters1 = new Dictionary<string, string>
{
{ "appid", appId.Item },
{ "mch_id", mchId.Item },
{ "nonce_str", nonceStr.Item }
};
var parameters2 = new Dictionary<string, string>
{
{ "nonce_str", nonceStr.Item },
{ "appid", appId.Item },
{ "mch_id", mchId.Item }
};
var parameters3 = new Dictionary<string, string>
{
{ "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;
}
/// <summary>
/// Property 1 (continued): 空值参数过滤
/// Empty values should be filtered out and not affect the signature.
/// Validates: Requirements 7.1
/// </summary>
[Property(MaxTest = 100)]
public bool SignatureEmptyValueFiltering_EmptyValuesShouldBeIgnored(
NonEmptyString appId,
NonEmptyString mchId)
{
// Arrange: Parameters with and without empty values
var parametersWithEmpty = new Dictionary<string, string>
{
{ "appid", appId.Item },
{ "mch_id", mchId.Item },
{ "empty_field", "" },
{ "null_field", null! }
};
var parametersWithoutEmpty = new Dictionary<string, string>
{
{ "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;
}
}