using FsCheck;
using FsCheck.Xunit;
using MiAssessment.Core.Interfaces;
using MiAssessment.Core.Services;
using MiAssessment.Model.Data;
using MiAssessment.Model.Models.Payment;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Services;
///
/// 微信支付 V3 回调格式识别属性测试
/// **Feature: wechat-pay-v3-upgrade**
///
public class WechatPayV3NotifyFormatPropertyTests
{
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 8: 回调格式识别正确性
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// *For any* JSON 格式且包含 resource 字段的回调数据,应该被识别为 V3 格式。
/// **Validates: Requirements 4.1, 4.5**
///
[Property(MaxTest = 100)]
public bool V3Format_JsonWithResource_ShouldBeDetectedAsV3(
NonEmptyString notifyId,
NonEmptyString eventType,
NonEmptyString ciphertext,
PositiveInt seed)
{
var service = CreateService();
// 清理输入(移除控制字符和特殊字符)
var cleanNotifyId = CleanJsonString(RemoveControlChars(notifyId.Get));
var cleanEventType = CleanJsonString(RemoveControlChars(eventType.Get));
var cleanCiphertext = CleanJsonString(RemoveControlChars(ciphertext.Get));
// 如果清理后为空,跳过测试
if (string.IsNullOrEmpty(cleanNotifyId) ||
string.IsNullOrEmpty(cleanEventType) ||
string.IsNullOrEmpty(cleanCiphertext))
{
return true;
}
// 构建 V3 格式的回调数据(JSON 格式且包含 resource 字段)
var v3NotifyBody = $@"{{
""id"": ""{cleanNotifyId}"",
""create_time"": ""2024-01-01T12:00:00+08:00"",
""event_type"": ""{cleanEventType}"",
""resource_type"": ""encrypt-resource"",
""resource"": {{
""algorithm"": ""AEAD_AES_256_GCM"",
""ciphertext"": ""{cleanCiphertext}"",
""nonce"": ""abcdefghijkl"",
""associated_data"": ""transaction""
}}
}}";
var isV3 = service.IsV3NotifyFormat(v3NotifyBody);
var isV2 = service.IsV2NotifyFormat(v3NotifyBody);
var version = service.DetectNotifyVersion(v3NotifyBody);
// V3 格式应该被正确识别
return isV3 && !isV2 && version == NotifyVersion.V3;
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// *For any* XML 格式的回调数据,应该被识别为 V2 格式。
/// **Validates: Requirements 4.1, 4.5**
///
[Property(MaxTest = 100)]
public bool V2Format_Xml_ShouldBeDetectedAsV2(
NonEmptyString orderNo,
NonEmptyString transactionId,
PositiveInt totalFee,
PositiveInt seed)
{
var service = CreateService();
// 清理输入(移除控制字符)
var cleanOrderNo = CleanXmlString(RemoveControlChars(orderNo.Get));
var cleanTransactionId = CleanXmlString(RemoveControlChars(transactionId.Get));
// 如果清理后为空,跳过测试
if (string.IsNullOrEmpty(cleanOrderNo) || string.IsNullOrEmpty(cleanTransactionId))
{
return true;
}
// 构建 V2 格式的回调数据(XML 格式)
var v2NotifyBody = $@"
{totalFee.Get}
";
var isV3 = service.IsV3NotifyFormat(v2NotifyBody);
var isV2 = service.IsV2NotifyFormat(v2NotifyBody);
var version = service.DetectNotifyVersion(v2NotifyBody);
// V2 格式应该被正确识别
return !isV3 && isV2 && version == NotifyVersion.V2;
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// *For any* JSON 格式但不包含 resource 字段的数据,不应该被识别为 V3 格式。
/// **Validates: Requirements 4.1, 4.5**
///
[Property(MaxTest = 100)]
public bool JsonWithoutResource_ShouldNotBeV3(
NonEmptyString key,
NonEmptyString value,
PositiveInt seed)
{
var service = CreateService();
var cleanKey = CleanJsonString(RemoveControlChars(key.Get));
var cleanValue = CleanJsonString(RemoveControlChars(value.Get));
// 如果清理后为空,跳过测试
if (string.IsNullOrEmpty(cleanKey) || string.IsNullOrEmpty(cleanValue))
{
return true;
}
// 确保 key 不是 "resource"
if (cleanKey.Equals("resource", StringComparison.OrdinalIgnoreCase))
{
cleanKey = "other_key";
}
// 构建不包含 resource 字段的 JSON
var jsonBody = $@"{{
""{cleanKey}"": ""{cleanValue}"",
""other_field"": ""some_value""
}}";
var isV3 = service.IsV3NotifyFormat(jsonBody);
// 不包含 resource 字段的 JSON 不应该被识别为 V3
return !isV3;
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// *For any* 空字符串或空白字符串,应该被识别为 Unknown 格式。
/// **Validates: Requirements 4.1, 4.5**
///
[Property(MaxTest = 100)]
public bool EmptyOrWhitespace_ShouldBeUnknown(PositiveInt whitespaceCount)
{
var service = CreateService();
// 生成空白字符串
var whitespace = new string(' ', whitespaceCount.Get % 100);
var isV3Empty = service.IsV3NotifyFormat("");
var isV2Empty = service.IsV2NotifyFormat("");
var versionEmpty = service.DetectNotifyVersion("");
var isV3Whitespace = service.IsV3NotifyFormat(whitespace);
var isV2Whitespace = service.IsV2NotifyFormat(whitespace);
var versionWhitespace = service.DetectNotifyVersion(whitespace);
// 空字符串和空白字符串都应该被识别为 Unknown
return !isV3Empty && !isV2Empty && versionEmpty == NotifyVersion.Unknown &&
!isV3Whitespace && !isV2Whitespace && versionWhitespace == NotifyVersion.Unknown;
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// *For any* 非 JSON 非 XML 的数据,应该被识别为 Unknown 格式。
/// **Validates: Requirements 4.1, 4.5**
///
[Property(MaxTest = 100)]
public bool InvalidFormat_ShouldBeUnknown(NonEmptyString randomData, PositiveInt seed)
{
var service = CreateService();
// 确保数据不是以 { 或 < 开头
var data = randomData.Get.TrimStart();
if (data.StartsWith('{') || data.StartsWith('<'))
{
data = "INVALID_" + data;
}
var isV3 = service.IsV3NotifyFormat(data);
var isV2 = service.IsV2NotifyFormat(data);
var version = service.DetectNotifyVersion(data);
// 非 JSON 非 XML 的数据应该被识别为 Unknown
return !isV3 && !isV2 && version == NotifyVersion.Unknown;
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// V3 和 V2 格式应该是互斥的。
/// **Validates: Requirements 4.1, 4.5**
///
[Property(MaxTest = 100)]
public bool V3AndV2_ShouldBeMutuallyExclusive(NonEmptyString data, PositiveInt seed)
{
var service = CreateService();
var isV3 = service.IsV3NotifyFormat(data.Get);
var isV2 = service.IsV2NotifyFormat(data.Get);
// V3 和 V2 不能同时为 true
return !(isV3 && isV2);
}
#endregion
#region 边界情况测试
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// 真实的 V3 支付成功回调应该被正确识别。
/// **Validates: Requirements 4.1, 4.5**
///
[Fact]
public void RealV3PaymentSuccessNotify_ShouldBeDetectedAsV3()
{
var service = CreateService();
var v3NotifyBody = @"{
""id"": ""EV-2024010112345678901234567890"",
""create_time"": ""2024-01-01T12:00:00+08:00"",
""event_type"": ""TRANSACTION.SUCCESS"",
""resource_type"": ""encrypt-resource"",
""resource"": {
""algorithm"": ""AEAD_AES_256_GCM"",
""ciphertext"": ""base64encodedciphertext"",
""nonce"": ""abcdefghijkl"",
""associated_data"": ""transaction"",
""original_type"": ""transaction""
},
""summary"": ""支付成功""
}";
Assert.True(service.IsV3NotifyFormat(v3NotifyBody));
Assert.False(service.IsV2NotifyFormat(v3NotifyBody));
Assert.Equal(NotifyVersion.V3, service.DetectNotifyVersion(v3NotifyBody));
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// 真实的 V2 支付成功回调应该被正确识别。
/// **Validates: Requirements 4.1, 4.5**
///
[Fact]
public void RealV2PaymentSuccessNotify_ShouldBeDetectedAsV2()
{
var service = CreateService();
var v2NotifyBody = @"
100
";
Assert.False(service.IsV3NotifyFormat(v2NotifyBody));
Assert.True(service.IsV2NotifyFormat(v2NotifyBody));
Assert.Equal(NotifyVersion.V2, service.DetectNotifyVersion(v2NotifyBody));
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// 带有前导空白的 V3 回调应该被正确识别。
/// **Validates: Requirements 4.1, 4.5**
///
[Fact]
public void V3NotifyWithLeadingWhitespace_ShouldBeDetectedAsV3()
{
var service = CreateService();
var v3NotifyBody = @"
{
""id"": ""test"",
""resource"": {
""ciphertext"": ""test""
}
}";
Assert.True(service.IsV3NotifyFormat(v3NotifyBody));
Assert.Equal(NotifyVersion.V3, service.DetectNotifyVersion(v3NotifyBody));
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// 带有前导空白的 V2 回调应该被正确识别。
/// **Validates: Requirements 4.1, 4.5**
///
[Fact]
public void V2NotifyWithLeadingWhitespace_ShouldBeDetectedAsV2()
{
var service = CreateService();
var v2NotifyBody = @"
SUCCESS
";
Assert.True(service.IsV2NotifyFormat(v2NotifyBody));
Assert.Equal(NotifyVersion.V2, service.DetectNotifyVersion(v2NotifyBody));
}
///
/// **Feature: wechat-pay-v3-upgrade, Property 8: 回调格式识别正确性**
/// 无效的 JSON 不应该被识别为 V3。
/// **Validates: Requirements 4.1, 4.5**
///
[Fact]
public void InvalidJson_ShouldNotBeV3()
{
var service = CreateService();
var invalidJson = @"{ ""resource"": ""missing closing brace""";
Assert.False(service.IsV3NotifyFormat(invalidJson));
}
#endregion
#region 辅助方法
///
/// 移除控制字符
///
private static string RemoveControlChars(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
return new string(input.Where(c => !char.IsControl(c)).ToArray());
}
///
/// 清理字符串以用于 JSON
///
private static string CleanJsonString(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
return input
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r")
.Replace("\t", "\\t");
}
///
/// 清理字符串以用于 XML
///
private static string CleanXmlString(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
return input
.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'")
.Replace("\n", " ")
.Replace("\r", " ");
}
#endregion
}