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 }