603 lines
19 KiB
C#
603 lines
19 KiB
C#
using HoneyBox.Core.Interfaces;
|
||
using HoneyBox.Core.Services;
|
||
using HoneyBox.Model.Data;
|
||
using HoneyBox.Model.Entities;
|
||
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 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"
|
||
};
|
||
|
||
_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);
|
||
}
|
||
|
||
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
|
||
}
|