394 lines
13 KiB
C#
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;
|
|
}
|
|
}
|