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

607 lines
19 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using HoneyBox.Core.Interfaces;
using HoneyBox.Core.Services;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using HoneyBox.Model.Models.Auth;
using HoneyBox.Model.Models.Payment;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Moq.Protected;
using System.Net;
using System.Text;
using Xunit;
namespace HoneyBox.Tests.Integration;
/// <summary>
/// 微信支付服务集成测试
/// 测试统一下单流程模拟微信API和签名生成验证
/// Requirements: 1.1-1.5, 7.1-7.4
/// </summary>
public class WechatPayServiceIntegrationTests
{
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;
public WechatPayServiceIntegrationTests()
{
_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
}
},
NotifyBaseUrl = "https://example.com"
};
_appSettings = new AppSettings { IsTestEnvironment = false };
_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>();
}
private HoneyBoxDbContext CreateInMemoryDbContext()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
return new HoneyBoxDbContext(options);
}
private WechatPayService CreateWechatPayService(HoneyBoxDbContext dbContext, HttpClient? httpClient = null)
{
var options = Options.Create(_settings);
return new WechatPayService(
dbContext,
httpClient ?? new HttpClient(),
_mockLogger.Object,
_mockConfigService.Object,
_mockWechatService.Object,
_mockRedisService.Object,
options,
_appSettings);
}
private async Task<User> CreateTestUserAsync(HoneyBoxDbContext dbContext)
{
var user = new User
{
Id = 1,
OpenId = "test_openid_123456",
Uid = "test_uid",
Nickname = "测试用户",
HeadImg = "avatar.jpg",
Mobile = "13800138000",
Money = 100,
Integral = 1000,
Money2 = 500,
IsTest = 0,
Status = 1,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
await dbContext.Users.AddAsync(user);
await dbContext.SaveChangesAsync();
return user;
}
#region (Requirements 7.1-7.4)
/// <summary>
/// 测试签名生成 - 基本功能
/// Requirements: 7.1
/// </summary>
[Fact]
public void MakeSign_BasicParameters_GeneratesValidSignature()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
var parameters = new Dictionary<string, string>
{
{ "appid", "wx1234567890abcdef" },
{ "mch_id", "1234567890" },
{ "nonce_str", "test_nonce_string" },
{ "body", "Test Payment" },
{ "out_trade_no", "TST_20250102123456" },
{ "total_fee", "100" }
};
// Act
var sign = service.MakeSign(parameters);
// Assert
Assert.NotNull(sign);
Assert.Equal(32, sign.Length); // MD5 produces 32 hex characters
Assert.Equal(sign, sign.ToUpper()); // Should be uppercase
Assert.True(sign.All(c => "0123456789ABCDEF".Contains(c))); // Should be hex
}
/// <summary>
/// 测试签名验证 - 正确签名
/// Requirements: 7.2
/// </summary>
[Fact]
public void VerifySign_ValidSignature_ReturnsTrue()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
var parameters = new Dictionary<string, string>
{
{ "appid", "wx1234567890abcdef" },
{ "mch_id", "1234567890" },
{ "nonce_str", "test_nonce" },
{ "body", "Test Payment" }
};
var sign = service.MakeSign(parameters);
// Act
var result = service.VerifySign(parameters, sign);
// Assert
Assert.True(result);
}
/// <summary>
/// 测试签名验证 - 错误签名
/// Requirements: 7.4
/// </summary>
[Fact]
public void VerifySign_InvalidSignature_ReturnsFalse()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
var parameters = new Dictionary<string, string>
{
{ "appid", "wx1234567890abcdef" },
{ "mch_id", "1234567890" },
{ "nonce_str", "test_nonce" }
};
// Act
var result = service.VerifySign(parameters, "INVALID_SIGNATURE_12345678901234");
// Assert
Assert.False(result);
}
/// <summary>
/// 测试签名验证 - 空签名
/// Requirements: 7.4
/// </summary>
[Fact]
public void VerifySign_EmptySignature_ReturnsFalse()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
var parameters = new Dictionary<string, string>
{
{ "appid", "wx1234567890abcdef" },
{ "mch_id", "1234567890" }
};
// Act & Assert
Assert.False(service.VerifySign(parameters, ""));
Assert.False(service.VerifySign(parameters, null!));
}
/// <summary>
/// 测试多商户签名支持
/// Requirements: 7.3
/// </summary>
[Fact]
public void MakeSign_DifferentMerchantKeys_ProducesDifferentSignatures()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
var parameters = new Dictionary<string, string>
{
{ "appid", "wx1234567890abcdef" },
{ "mch_id", "1234567890" },
{ "nonce_str", "test_nonce" }
};
var key1 = _settings.DefaultMerchant.Key;
var key2 = _settings.Merchants[0].Key;
// Act
var sign1 = service.MakeSign(parameters, key1);
var sign2 = service.MakeSign(parameters, key2);
// Assert
Assert.NotEqual(sign1, sign2);
Assert.True(service.VerifySign(parameters, sign1, key1));
Assert.False(service.VerifySign(parameters, sign1, key2));
}
#endregion
#region (Requirements 1.1-1.5)
/// <summary>
/// 测试统一下单 - 用户不存在
/// Requirements: 1.1
/// </summary>
[Fact]
public async Task CreatePaymentAsync_UserNotFound_ReturnsError()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
var request = new WechatPayRequest
{
OrderNo = "TST_20250102123456",
Amount = 10.00m,
Body = "Test Payment",
Attach = "order_yfs",
UserId = 999 // Non-existent user
};
// Act
var result = await service.CreatePaymentAsync(request);
// Assert
Assert.Equal(0, result.Status);
Assert.Contains("用户不存在", result.Msg);
}
/// <summary>
/// 测试统一下单 - 用户OpenId为空
/// Requirements: 1.1
/// </summary>
[Fact]
public async Task CreatePaymentAsync_EmptyOpenId_ReturnsError()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
// Create user without OpenId
var user = new User
{
Id = 1,
OpenId = "", // Empty OpenId
Uid = "test_uid",
Nickname = "测试用户",
HeadImg = "avatar.jpg",
Status = 1,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
await dbContext.Users.AddAsync(user);
await dbContext.SaveChangesAsync();
var service = CreateWechatPayService(dbContext);
var request = new WechatPayRequest
{
OrderNo = "TST_20250102123456",
Amount = 10.00m,
Body = "Test Payment",
Attach = "order_yfs",
UserId = 1
};
// Act
var result = await service.CreatePaymentAsync(request);
// Assert
Assert.Equal(0, result.Status);
Assert.Contains("OpenId", result.Msg);
}
/// <summary>
/// 测试统一下单 - 成功场景模拟微信API
/// Requirements: 1.1, 1.2, 1.4, 1.5
/// </summary>
[Fact]
public async Task CreatePaymentAsync_ValidRequest_ReturnsPaymentParams()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
await CreateTestUserAsync(dbContext);
// Mock HTTP response from WeChat API
var mockHandler = new Mock<HttpMessageHandler>();
var wechatResponse = @"<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<result_code><![CDATA[SUCCESS]]></result_code>
<appid><![CDATA[wx1234567890abcdef]]></appid>
<mch_id><![CDATA[1234567890]]></mch_id>
<nonce_str><![CDATA[test_nonce]]></nonce_str>
<sign><![CDATA[TEST_SIGN]]></sign>
<prepay_id><![CDATA[wx201501021234567890]]></prepay_id>
<trade_type><![CDATA[JSAPI]]></trade_type>
</xml>";
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(wechatResponse, Encoding.UTF8, "application/xml")
});
var httpClient = new HttpClient(mockHandler.Object);
var service = CreateWechatPayService(dbContext, httpClient);
var request = new WechatPayRequest
{
OrderNo = "TST_20250102123456",
Amount = 10.00m,
Body = "Test Payment",
Attach = "order_yfs",
UserId = 1
};
// Act
var result = await service.CreatePaymentAsync(request);
// Assert
Assert.Equal(1, result.Status);
Assert.Equal("success", result.Msg);
Assert.NotNull(result.Data);
Assert.Equal("wx1234567890abcdef", result.Data.AppId);
Assert.NotEmpty(result.Data.TimeStamp);
Assert.NotEmpty(result.Data.NonceStr);
Assert.Contains("prepay_id=", result.Data.Package);
Assert.Equal("MD5", result.Data.SignType);
Assert.NotEmpty(result.Data.PaySign);
Assert.Equal(1, result.Data.IsWeixin);
}
/// <summary>
/// 测试统一下单 - 微信API返回失败
/// Requirements: 1.3
/// </summary>
[Fact]
public async Task CreatePaymentAsync_WechatApiError_ReturnsError()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
await CreateTestUserAsync(dbContext);
// Mock HTTP response with error
var mockHandler = new Mock<HttpMessageHandler>();
var wechatResponse = @"<xml>
<return_code><![CDATA[FAIL]]></return_code>
<return_msg><![CDATA[签名错误]]></return_msg>
</xml>";
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(wechatResponse, Encoding.UTF8, "application/xml")
});
var httpClient = new HttpClient(mockHandler.Object);
var service = CreateWechatPayService(dbContext, httpClient);
var request = new WechatPayRequest
{
OrderNo = "TST_20250102123456",
Amount = 10.00m,
Body = "Test Payment",
Attach = "order_yfs",
UserId = 1
};
// Act
var result = await service.CreatePaymentAsync(request);
// Assert
Assert.Equal(0, result.Status);
Assert.Contains("签名错误", result.Msg);
}
/// <summary>
/// 测试统一下单 - 业务失败(如订单已支付)
/// Requirements: 1.3
/// </summary>
[Fact]
public async Task CreatePaymentAsync_BusinessError_ReturnsError()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
await CreateTestUserAsync(dbContext);
// Mock HTTP response with business error
var mockHandler = new Mock<HttpMessageHandler>();
var wechatResponse = @"<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<result_code><![CDATA[FAIL]]></result_code>
<err_code><![CDATA[ORDERPAID]]></err_code>
<err_code_des><![CDATA[该订单已支付]]></err_code_des>
</xml>";
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(wechatResponse, Encoding.UTF8, "application/xml")
});
var httpClient = new HttpClient(mockHandler.Object);
var service = CreateWechatPayService(dbContext, httpClient);
var request = new WechatPayRequest
{
OrderNo = "TST_20250102123456",
Amount = 10.00m,
Body = "Test Payment",
Attach = "order_yfs",
UserId = 1
};
// Act
var result = await service.CreatePaymentAsync(request);
// Assert
Assert.Equal(0, result.Status);
Assert.Contains("已支付", result.Msg);
}
#endregion
#region XML解析测试
/// <summary>
/// 测试XML解析 - 正常回调数据
/// </summary>
[Fact]
public void ParseNotifyXml_ValidXml_ReturnsCorrectData()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
var xml = @"<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<result_code><![CDATA[SUCCESS]]></result_code>
<appid><![CDATA[wx1234567890abcdef]]></appid>
<mch_id><![CDATA[1234567890]]></mch_id>
<nonce_str><![CDATA[test_nonce]]></nonce_str>
<sign><![CDATA[TEST_SIGN]]></sign>
<openid><![CDATA[test_openid_123456]]></openid>
<trade_type><![CDATA[JSAPI]]></trade_type>
<bank_type><![CDATA[CMC]]></bank_type>
<total_fee>1000</total_fee>
<fee_type><![CDATA[CNY]]></fee_type>
<cash_fee>1000</cash_fee>
<transaction_id><![CDATA[wx20250102123456]]></transaction_id>
<out_trade_no><![CDATA[TST_20250102123456]]></out_trade_no>
<attach><![CDATA[order_yfs]]></attach>
<time_end><![CDATA[20250102123456]]></time_end>
</xml>";
// Act
var result = service.ParseNotifyXml(xml);
// Assert
Assert.Equal("SUCCESS", result.ReturnCode);
Assert.Equal("SUCCESS", result.ResultCode);
Assert.Equal("wx1234567890abcdef", result.AppId);
Assert.Equal("1234567890", result.MchId);
Assert.Equal("test_openid_123456", result.OpenId);
Assert.Equal(1000, result.TotalFee);
Assert.Equal("TST_20250102123456", result.OutTradeNo);
Assert.Equal("order_yfs", result.Attach);
}
/// <summary>
/// 测试生成回调响应XML
/// </summary>
[Fact]
public void GenerateNotifyResponseXml_Success_ReturnsCorrectXml()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
// Act
var result = service.GenerateNotifyResponseXml("SUCCESS", "OK");
// Assert
Assert.Contains("<return_code><![CDATA[SUCCESS]]></return_code>", result);
Assert.Contains("<return_msg><![CDATA[OK]]></return_msg>", result);
}
#endregion
#region (Requirements 1.5)
/// <summary>
/// 测试根据订单号获取商户配置
/// Requirements: 1.5
/// </summary>
[Fact]
public void GetMerchantByOrderNo_ValidOrderNo_ReturnsCorrectMerchant()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
// Act
var merchant = service.GetMerchantByOrderNo("TST_20250102123456");
// Assert
Assert.NotNull(merchant);
Assert.Equal(_settings.DefaultMerchant.MchId, merchant.MchId);
}
/// <summary>
/// 测试根据订单号获取商户密钥
/// Requirements: 1.5
/// </summary>
[Fact]
public void GetMerchantKeyByOrderNo_ValidOrderNo_ReturnsCorrectKey()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateWechatPayService(dbContext);
// Act
var key = service.GetMerchantKeyByOrderNo("TST_20250102123456");
// Assert
Assert.Equal(_settings.DefaultMerchant.Key, key);
}
#endregion
}