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 WechatPayV3DecryptionPropertyTests
{
///
/// 生成有效的 32 字节 APIv3 密钥
///
private static string GenerateApiV3Key()
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var result = new char[32];
for (int i = 0; i < 32; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
///
/// 生成有效的 12 字节 nonce
///
private static string GenerateNonce()
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var result = new char[12];
for (int i = 0; i < 12; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
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 9: V3 回调解密 Round-Trip
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* 有效的支付结果数据,使用 AES-256-GCM 加密后再解密,
/// 应该得到与原始数据等价的结果。
/// **Validates: Requirements 4.3**
///
[Property(MaxTest = 100)]
public bool DecryptRoundTrip_ShouldReturnOriginalData(NonEmptyString plaintext, PositiveInt seed)
{
var service = CreateService();
// 生成固定的密钥和 nonce(基于 seed 确保可重复)
var random = new Random(seed.Get);
var apiV3Key = GenerateApiV3KeyWithSeed(random);
var nonce = GenerateNonceWithSeed(random);
var associatedData = "transaction";
// 清理输入(移除可能导致问题的字符)
var cleanPlaintext = plaintext.Get.Replace("\0", "");
if (string.IsNullOrEmpty(cleanPlaintext))
{
return true; // 跳过空字符串
}
try
{
// 加密
var ciphertext = service.EncryptNotifyResource(cleanPlaintext, nonce, associatedData, apiV3Key);
// 解密
var decrypted = service.DecryptNotifyResource(ciphertext, nonce, associatedData, apiV3Key);
// 验证 round-trip
return cleanPlaintext == decrypted;
}
catch
{
return false;
}
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* JSON 格式的支付结果数据,加密后再解密应该保持 JSON 结构不变。
/// **Validates: Requirements 4.3**
///
[Property(MaxTest = 100)]
public bool DecryptRoundTrip_JsonData_ShouldPreserveStructure(
NonEmptyString orderNo,
NonEmptyString transactionId,
PositiveInt amount,
PositiveInt seed)
{
var service = CreateService();
var random = new Random(seed.Get);
var apiV3Key = GenerateApiV3KeyWithSeed(random);
var nonce = GenerateNonceWithSeed(random);
var associatedData = "transaction";
// 构建类似微信支付回调的 JSON 数据
var cleanOrderNo = orderNo.Get.Replace("\"", "").Replace("\\", "").Replace("\n", "").Replace("\r", "");
var cleanTransactionId = transactionId.Get.Replace("\"", "").Replace("\\", "").Replace("\n", "").Replace("\r", "");
var jsonData = $"{{\"out_trade_no\":\"{cleanOrderNo}\",\"transaction_id\":\"{cleanTransactionId}\",\"trade_state\":\"SUCCESS\",\"amount\":{{\"total\":{amount.Get}}}}}";
try
{
// 加密
var ciphertext = service.EncryptNotifyResource(jsonData, nonce, associatedData, apiV3Key);
// 解密
var decrypted = service.DecryptNotifyResource(ciphertext, nonce, associatedData, apiV3Key);
// 验证 round-trip
return jsonData == decrypted;
}
catch
{
return false;
}
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* 有效数据,使用不同的密钥解密应该失败。
/// **Validates: Requirements 4.3**
///
[Property(MaxTest = 100)]
public bool Decrypt_WithWrongKey_ShouldFail(NonEmptyString plaintext, PositiveInt seed)
{
var service = CreateService();
var random = new Random(seed.Get);
var apiV3Key1 = GenerateApiV3KeyWithSeed(random);
var apiV3Key2 = GenerateApiV3KeyWithSeed(new Random(seed.Get + 1)); // 不同的密钥
var nonce = GenerateNonceWithSeed(random);
var associatedData = "transaction";
// 确保两个密钥不同
if (apiV3Key1 == apiV3Key2)
{
return true; // 跳过相同密钥的情况
}
var cleanPlaintext = plaintext.Get.Replace("\0", "");
if (string.IsNullOrEmpty(cleanPlaintext))
{
return true;
}
try
{
// 使用密钥1加密
var ciphertext = service.EncryptNotifyResource(cleanPlaintext, nonce, associatedData, apiV3Key1);
// 使用密钥2解密应该失败
try
{
service.DecryptNotifyResource(ciphertext, nonce, associatedData, apiV3Key2);
return false; // 如果没有抛出异常,测试失败
}
catch (InvalidOperationException)
{
return true; // 预期的异常
}
catch (CryptographicException)
{
return true; // 预期的异常
}
}
catch
{
return false;
}
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* 有效数据,使用不同的 nonce 解密应该失败。
/// **Validates: Requirements 4.3**
///
[Property(MaxTest = 100)]
public bool Decrypt_WithWrongNonce_ShouldFail(NonEmptyString plaintext, PositiveInt seed)
{
var service = CreateService();
var random = new Random(seed.Get);
var apiV3Key = GenerateApiV3KeyWithSeed(random);
var nonce1 = GenerateNonceWithSeed(random);
var nonce2 = GenerateNonceWithSeed(new Random(seed.Get + 1)); // 不同的 nonce
var associatedData = "transaction";
// 确保两个 nonce 不同
if (nonce1 == nonce2)
{
return true;
}
var cleanPlaintext = plaintext.Get.Replace("\0", "");
if (string.IsNullOrEmpty(cleanPlaintext))
{
return true;
}
try
{
// 使用 nonce1 加密
var ciphertext = service.EncryptNotifyResource(cleanPlaintext, nonce1, associatedData, apiV3Key);
// 使用 nonce2 解密应该失败
try
{
service.DecryptNotifyResource(ciphertext, nonce2, associatedData, apiV3Key);
return false;
}
catch (InvalidOperationException)
{
return true;
}
catch (CryptographicException)
{
return true;
}
}
catch
{
return false;
}
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* 有效数据,篡改密文后解密应该失败。
/// **Validates: Requirements 4.3**
///
[Property(MaxTest = 100)]
public bool Decrypt_WithTamperedCiphertext_ShouldFail(NonEmptyString plaintext, PositiveInt seed)
{
var service = CreateService();
var random = new Random(seed.Get);
var apiV3Key = GenerateApiV3KeyWithSeed(random);
var nonce = GenerateNonceWithSeed(random);
var associatedData = "transaction";
var cleanPlaintext = plaintext.Get.Replace("\0", "");
if (string.IsNullOrEmpty(cleanPlaintext))
{
return true;
}
try
{
// 加密
var ciphertext = service.EncryptNotifyResource(cleanPlaintext, nonce, associatedData, apiV3Key);
// 篡改密文(修改一个字符)
var ciphertextBytes = Convert.FromBase64String(ciphertext);
if (ciphertextBytes.Length > 0)
{
ciphertextBytes[0] = (byte)(ciphertextBytes[0] ^ 0xFF);
}
var tamperedCiphertext = Convert.ToBase64String(ciphertextBytes);
// 解密篡改后的密文应该失败
try
{
service.DecryptNotifyResource(tamperedCiphertext, nonce, associatedData, apiV3Key);
return false;
}
catch (InvalidOperationException)
{
return true;
}
catch (CryptographicException)
{
return true;
}
}
catch
{
return false;
}
}
#endregion
#region 辅助方法
private static string GenerateApiV3KeyWithSeed(Random random)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var result = new char[32];
for (int i = 0; i < 32; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
private static string GenerateNonceWithSeed(Random random)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var result = new char[12];
for (int i = 0; i < 12; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
#endregion
#region 边界情况测试
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// 空的 associated data 应该正常工作。
/// **Validates: Requirements 4.3**
///
[Fact]
public void DecryptRoundTrip_EmptyAssociatedData_ShouldWork()
{
var service = CreateService();
var apiV3Key = "d1cxc0vXCUH2984901DxddPJMYqcwcnd";
var nonce = "abcdefghijkl";
var plaintext = "{\"out_trade_no\":\"TEST123\",\"trade_state\":\"SUCCESS\"}";
// 加密(空 associated data)
var ciphertext = service.EncryptNotifyResource(plaintext, nonce, "", apiV3Key);
// 解密
var decrypted = service.DecryptNotifyResource(ciphertext, nonce, "", apiV3Key);
Assert.Equal(plaintext, decrypted);
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// 中文内容应该正常加解密。
/// **Validates: Requirements 4.3**
///
[Fact]
public void DecryptRoundTrip_ChineseContent_ShouldWork()
{
var service = CreateService();
var apiV3Key = "d1cxc0vXCUH2984901DxddPJMYqcwcnd";
var nonce = "abcdefghijkl";
var plaintext = "{\"description\":\"商品购买-测试商品\",\"trade_state_desc\":\"支付成功\"}";
// 加密
var ciphertext = service.EncryptNotifyResource(plaintext, nonce, "transaction", apiV3Key);
// 解密
var decrypted = service.DecryptNotifyResource(ciphertext, nonce, "transaction", apiV3Key);
Assert.Equal(plaintext, decrypted);
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// 无效的 APIv3 密钥长度应该抛出异常。
/// **Validates: Requirements 4.3**
///
[Fact]
public void Decrypt_InvalidKeyLength_ShouldThrow()
{
var service = CreateService();
var invalidKey = "shortkey"; // 不是 32 字节
var nonce = "abcdefghijkl";
var ciphertext = "dGVzdA=="; // 随便一个 base64
Assert.Throws(() =>
service.DecryptNotifyResource(ciphertext, nonce, "transaction", invalidKey));
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// 空密文应该抛出异常。
/// **Validates: Requirements 4.3**
///
[Fact]
public void Decrypt_EmptyCiphertext_ShouldThrow()
{
var service = CreateService();
var apiV3Key = "d1cxc0vXCUH2984901DxddPJMYqcwcnd";
var nonce = "abcdefghijkl";
Assert.Throws(() =>
service.DecryptNotifyResource("", nonce, "transaction", apiV3Key));
}
#endregion
}