diff --git a/src/addon/huaweipay/MANUAL_TEST_GUIDE.md b/src/addon/huaweipay/MANUAL_TEST_GUIDE.md new file mode 100644 index 000000000..8c07c1e9f --- /dev/null +++ b/src/addon/huaweipay/MANUAL_TEST_GUIDE.md @@ -0,0 +1,318 @@ +# 华为支付手动测试指南 + +## 1. 测试环境准备 + +### 1.1 配置文件准备 + +#### 1.1.1 证书准备 + +确保在 `mock/cert/` 目录下有以下证书文件: +- `merchant_private_key.pem` - 商户应用私钥证书 +- `huawei_public_key.pem` - 华为平台公钥证书 + +#### 1.1.2 配置文件设置 + +编辑 `tests/mock/data.yml` 文件,确保以下配置项正确设置: + +```yaml +# 华为支付测试配置 +HuaweiPay: + # 沙盒模式 + sandbox: true + # 应用ID + app_id: 'test_app_id' + # 商户号 + merc_no: 'test_merc_no' + # 授权ID + mch_auth_id: 'test_mch_auth_id' + # 证书文件路径(相对于项目根目录) + private_key: 'mock/cert/merchant_private_key.pem' + # 私钥文本内容 + private_key_text: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLW1j8a1KQw7X2 + ... + -----END PRIVATE KEY----- + # 华为公钥文件路径(相对于项目根目录) + huawei_public_key: 'mock/cert/huawei_public_key.pem' + # 华为公钥文本内容 + huawei_public_key_text: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0T1JZ7X2Y5 + ... + -----END PUBLIC KEY----- + # 异步通知URL + notify_url: 'https://your-domain.com/huaweipay/notify' + # 同步返回URL + return_url: 'https://your-domain.com/huaweipay/return' +``` + +### 1.2 系统配置 + +1. 登录系统后台 +2. 进入「支付配置」页面 +3. 找到「华为支付」配置项 +4. 填写以下信息: + - 应用ID + - 商户号 + - 授权ID + - 私钥(可以选择文件上传或文本输入) + - 华为公钥(可以选择文件上传或文本输入) + - 异步通知URL + - 同步返回URL +5. 开启华为支付 + +## 2. 单元测试 + +### 2.1 运行证书格式化测试 + +```bash +./vendor/bin/phpunit addon/huaweipay/tests/HuaweiPayConfigTest.php +``` + +### 2.2 运行支付模型测试 + +```bash +./vendor/bin/phpunit addon/huaweipay/tests/PayModelTest.php +``` + +## 3. 功能测试 + +### 3.1 H5支付测试 + +1. 进入系统前台 +2. 选择一个商品加入购物车 +3. 提交订单 +4. 选择「华为支付」作为支付方式 +5. 点击「立即支付」 +6. 系统将跳转到华为H5支付页面 +7. 测试不同支付场景: + - 成功支付 + - 取消支付 + - 超时未支付 + +### 3.2 微信小程序支付测试 + +1. 打开微信开发者工具 +2. 导入小程序项目 +3. 登录小程序账号 +4. 选择一个商品加入购物车 +5. 提交订单 +6. 选择「华为支付」作为支付方式 +7. 点击「立即支付」 +8. 调用华为微信小程序支付接口 +9. 测试不同支付场景: + - 成功支付 + - 取消支付 + - 超时未支付 + +### 3.3 APP支付测试 + +1. 编译APP项目 +2. 在测试设备上安装APP +3. 登录APP账号 +4. 选择一个商品加入购物车 +5. 提交订单 +6. 选择「华为支付」作为支付方式 +7. 点击「立即支付」 +8. 调用华为APP支付接口 +9. 测试不同支付场景: + - 成功支付 + - 取消支付 + - 超时未支付 + +## 4. 回调测试 + +### 4. 回调测试 + +**测试目标**:验证华为支付异步通知处理流程是否正常工作,包括签名验证和订单状态更新。 + +#### 4.1 测试准备 +1. **配置回调地址**: + - 在华为支付模型的配置中设置回调URL(通常为`http://your-domain.com/addon/huaweipay/shop/controller/pay/notify`) + - 开发环境可使用ngrok等工具进行本地端口映射:`ngrok http 80` + +2. **了解回调处理流程**: + - 华为支付异步回调通过`PayNotify`事件触发 + - 事件监听器:`addon\huaweipay\event\PayNotify` + - 实际处理逻辑:`addon\huaweipay\model\Pay::notify()`方法 + +#### 4.2 模拟回调测试 + +**方法一:使用Postman模拟回调请求** + +1. **构造回调参数**: +```json +{ + "pay_type": "huaweipay", + "out_trade_no": "TEST_ORDER_202310100001", + "trade_no": "HWPAY2023101000000001", + "total_amount": "1.00", + "trade_status": "SUCCESS", + "timestamp": "1696944000000", + "sign": "xxxxxxxxx", + "app_type": "h5" +} +``` + +2. **发送POST请求**: + - URL: `http://your-domain.com/addon/huaweipay/shop/controller/pay/notify` + - Method: POST + - Body: 选择raw JSON格式,粘贴上面的测试数据 + +3. **验证回调结果**: + - 检查响应是否为"success"字符串 + - 查看系统日志(`runtime/log`目录) + - 确认订单状态是否已更新为已支付 + +**方法二:使用命令行工具** + +```bash +curl -X POST \ + http://your-domain.com/addon/huaweipay/shop/controller/pay/notify \ + -H 'Content-Type: application/json' \ + -d '{"pay_type":"huaweipay","out_trade_no":"TEST_ORDER_202310100001","trade_no":"HWPAY2023101000000001","total_amount":"1.00","trade_status":"SUCCESS","timestamp":"1696944000000","sign":"xxxxxxxxx"}' +``` + +#### 4.3 回调签名验证测试 + +**测试目标**:验证回调签名验证逻辑是否正常工作 + +**测试步骤**: +1. 构造正确的回调参数和签名 +2. 发送回调请求,验证处理成功 +3. 修改签名为错误值,重新发送请求 +4. 验证签名错误时返回"fail" + +**测试要点**: +- 检查`HuaweiPayClient::verifyNotify()`方法的实现 +- 确保使用正确的公钥进行签名验证 +- 验证失败时记录详细日志 + +**日志检查**: +``` +# 签名验证成功日志 +[info] 华为支付回调参数: {"out_trade_no":"TEST_ORDER_202310100001",...} +[info] 华为支付回调处理成功,订单号: TEST_ORDER_202310100001 + +# 签名验证失败日志 +[error] 华为支付回调签名验证失败: {"out_trade_no":"TEST_ORDER_202310100001",...} +``` + +**注意事项**: +- 回调请求需要包含正确的签名,使用华为支付公钥进行验证 +- 回调处理完成后,必须向华为服务器返回"success"字符串,否则华为会持续发送通知(默认重试间隔为15s,30s,1m,2m,5m,10m,30m,1h,2h,6h,15h) +- 回调处理逻辑需要支持幂等性,防止重复处理同一笔订单 + +## 5. 常见问题排查 + +### 5.1 证书加载失败 + +**问题现象:** 系统提示「加载商户应用私有证书失败」或「加载华为平台支付证书失败」 + +**解决方法:** +- 检查证书文件路径是否正确 +- 验证证书格式是否符合要求(PEM格式) +- 确保证书内容没有多余的空格或换行 +- 检查文件权限是否正确 + +### 5.2 签名验证失败 + +**问题现象:** 系统提示「签名验证失败」或华为支付API返回签名错误 + +**解决方法:** +- 检查私钥和公钥是否匹配 +- 验证签名算法是否正确(RSA2) +- 确保请求参数没有被篡改 +- 检查时间戳是否在有效范围内 + +### 5.3 支付接口调用失败 + +**问题现象:** 调用华为支付API返回错误码 + +**解决方法:** +- 检查API请求参数是否完整 +- 验证参数格式是否符合要求 +- 检查网络连接是否正常 +- 查看华为支付API文档,了解错误码含义 + +### 5.4 订单状态更新失败 + +**问题现象:** 支付成功后,系统订单状态未更新 + +**解决方法:** +- 检查异步通知URL是否可访问 +- 验证通知签名是否正确 +- 查看系统日志,了解处理过程 +- 检查订单号是否正确匹配 + +## 6. 测试注意事项 + +1. **沙盒环境:** 测试时建议使用华为支付沙盒环境,避免产生真实交易 +2. **日志记录:** 开启详细日志记录,便于排查问题 +3. **安全防护:** 测试完成后,及时清理测试数据和敏感信息 +4. **兼容性测试:** 确保在不同浏览器、设备上都能正常支付 +5. **性能测试:** 测试高并发场景下的支付处理能力 + +## 7. 测试报告模板 + +``` +# 华为支付测试报告 + +## 测试基本信息 +- 测试时间:YYYY-MM-DD +- 测试环境:开发环境/测试环境/生产环境 +- 测试人员:XXX + +## 测试内容 + +### 7.1 配置测试 +- [ ] 证书文件存在性检查 +- [ ] 配置文件格式检查 +- [ ] 证书加载测试 + +### 7.2 功能测试 +- [ ] H5支付测试 + - [ ] 成功支付流程 + - [ ] 取消支付流程 + - [ ] 超时未支付流程 +- [ ] 微信小程序支付测试 + - [ ] 成功支付流程 + - [ ] 取消支付流程 + - [ ] 超时未支付流程 +- [ ] APP支付测试 + - [ ] 成功支付流程 + - [ ] 取消支付流程 + - [ ] 超时未支付流程 + +### 7.3 回调测试 +- [ ] 异步通知处理 +- [ ] 同步返回处理 +- [ ] 订单状态更新 + +## 测试结果 + +### 7.1 通过的测试 +- ... + +### 7.2 失败的测试 +- ... + +### 7.3 问题总结 + +| 问题描述 | 严重程度 | 状态 | 解决方案 | +|---------|---------|------|---------| +| ... | 高/中/低 | 已解决/待解决 | ... | + +## 测试结论 + +综合测试结果,华为支付模块是否满足上线要求: +- [ ] 是 +- [ ] 否(需要进一步修复) + +## 建议 + +1. ... +2. ... +3. ... +``` \ No newline at end of file diff --git a/src/addon/huaweipay/data/sdk/HuaweiPayClient.php b/src/addon/huaweipay/data/sdk/HuaweiPayClient.php index 5b601d531..ca7251c14 100644 --- a/src/addon/huaweipay/data/sdk/HuaweiPayClient.php +++ b/src/addon/huaweipay/data/sdk/HuaweiPayClient.php @@ -9,6 +9,9 @@ namespace addon\huaweipay\data\sdk; use app\exception\ApiException; use think\facade\Log; +// 引入工具类 +use addon\huaweipay\data\sdk\Utils; + class HuaweiPayClient { // 华为支付网关地址 @@ -32,53 +35,14 @@ class HuaweiPayClient // 是否加载了华为平台支付服务加密证书 private $has_huawei_public_key_certificate_instance_encrypt = false; - /** - * 格式化证书内容,添加适当的格式头尾 - * @param string $content 证书内容 - * @param string $type 证书类型:private_key, public_key - * @return string 格式化后的证书内容 - */ - private function formatCertificateContent($content, $type) - { - // 移除空白字符和换行 - $content = preg_replace('/\s+/', '', $content); - - // 检查是否已经包含格式头尾 - if (preg_match('/-----BEGIN.*-----/', $content) && preg_match('/-----END.*-----/', $content)) { - return $content; - } - - // 添加适当的格式头尾 - $header = ''; - $footer = ''; - - if ($type == 'private_key') { - $header = "-----BEGIN PRIVATE KEY-----\n"; - $footer = "-----END PRIVATE KEY-----"; - } elseif ($type == 'public_key') { - $header = "-----BEGIN PUBLIC KEY-----\n"; - $footer = "-----END PUBLIC KEY-----"; - } - - // 每64个字符添加一个换行 - $formattedContent = $header; - $length = strlen($content); - $lines = []; - - for ($i = 0; $i < $length; $i += 64) { - $lines[] = substr($content, $i, 64); - } - - $formattedContent .= implode("\n", $lines) . $footer; - - return $formattedContent; - } + /** * 构造函数 * @param array $config 华为支付配置 + * @param string $cert_root_path 证书根路径 */ - public function __construct($config) + public function __construct($config, $cert_root_path = '') { $this->config = $config; @@ -89,16 +53,36 @@ class HuaweiPayClient if (empty($this->config['merc_no'])) { throw new \Exception('缺少必要配置:merc_no'); } + if (empty($this->config['site_id'])) { + throw new \Exception('缺少必要配置:site_id'); + } + if (empty($this->config['mch_auth_id'])) { + throw new \Exception('缺少必要配置:mch_auth_id'); + } + if (empty($this->config['private_key_text']) && empty($this->config['private_key'])) { + throw new \Exception('缺少必要配置:private_key_text或private_key'); + } + if (empty($this->config['huawei_public_key_text']) && empty($this->config['huawei_public_key'])) { + throw new \Exception('缺少必要配置:huawei_public_key_text或huawei_public_key'); + } // 证书基础路径 - $cert_base_path = app()->getRootPath(); + $cert_base_path = $cert_root_path; + if (empty($cert_base_path)) { + // 没有指定证书根路径时,使用默认路径 + try { + $cert_base_path = app()->getRootPath(); + } catch (\Exception $e) { + // 捕获异常,使用默认路径 + } + } try { // 加载商户应用私有证书 $private_key_content = ''; if (!empty($this->config['private_key_text'])) { // 文本模式,需要格式化 - $private_key_content = $this->formatCertificateContent($this->config['private_key_text'], 'private_key'); + $private_key_content = Utils::formatCertificateContent($this->config['private_key_text'], 'private_key'); } elseif (!empty($this->config['private_key'])) { // 文件模式 $private_key_path = realpath($cert_base_path . $this->config['private_key']); @@ -112,14 +96,19 @@ class HuaweiPayClient $this->private_key_certificate_instance = openssl_pkey_get_private($private_key_content); if (!$this->private_key_certificate_instance) { - throw new \Exception('加载商户应用私有证书失败,请检查证书格式是否正确'); + // 输出详细的openssl错误信息 + $error = ''; + while ($err = openssl_error_string()) { + $error .= $err . ' '; + } + throw new \Exception('加载商户应用私有证书失败,请检查证书格式是否正确。OpenSSL错误: ' . $error . ' 证书内容开头: ' . substr($private_key_content, 0, 100)); } // 加载华为平台支付证书 $huawei_public_key_content = ''; if (!empty($this->config['huawei_public_key_text'])) { // 文本模式,需要格式化 - $huawei_public_key_content = $this->formatCertificateContent($this->config['huawei_public_key_text'], 'public_key'); + $huawei_public_key_content = Utils::formatCertificateContent($this->config['huawei_public_key_text'], 'public_key'); } elseif (!empty($this->config['huawei_public_key'])) { // 文件模式 $huawei_public_key_path = realpath($cert_base_path . $this->config['huawei_public_key']); @@ -139,7 +128,7 @@ class HuaweiPayClient // 加载华为平台支付服务加密证书(可选) if (!empty($this->config['huawei_public_key_for_sessionkey_text'])) { // 文本模式,需要格式化 - $huawei_public_key_encrypt_content = $this->formatCertificateContent($this->config['huawei_public_key_for_sessionkey_text'], 'public_key'); + $huawei_public_key_encrypt_content = Utils::formatCertificateContent($this->config['huawei_public_key_for_sessionkey_text'], 'public_key'); $this->huawei_public_key_certificate_instance_encrypt = openssl_pkey_get_public($huawei_public_key_encrypt_content); if (!$this->huawei_public_key_certificate_instance_encrypt) { throw new \Exception('加载华为平台支付服务加密证书失败'); @@ -166,7 +155,7 @@ class HuaweiPayClient // 根据配置设置网关地址 if (isset($config['sandbox']) && $config['sandbox']) { - $this->gatewayUrl = 'https://pay-drcn.cloud.huawei.com/gateway/api/pay'; + $this->gatewayUrl = 'https://petalpay-developer-sandbox.cloud.huawei.com.cn/gateway/api/pay'; } } diff --git a/src/addon/huaweipay/data/sdk/Utils.php b/src/addon/huaweipay/data/sdk/Utils.php new file mode 100644 index 000000000..2736f7f97 --- /dev/null +++ b/src/addon/huaweipay/data/sdk/Utils.php @@ -0,0 +1,61 @@ +site_id = $site_id; // 获取华为支付参数 - $config_info = (new Config())->getPayConfig($this->site_id)['data']['value']; - $this->config = $config_info; - if (empty($this->config)) throw new ApiException(-1, "平台未配置华为支付"); - $this->config['site_id'] = $this->site_id; + if (empty($config_data)) { + $config_info = (new Config())->getPayConfig($this->site_id)['data']['value']; + $this->config = $config_info; + if (empty($this->config)) throw new ApiException(-1, "平台未配置华为支付"); + } else { + $this->config = $config_data; + } + // 添加站点ID + $this->config['site_id'] = $this->site_id; + // 初始化华为支付客户端 $this->hwpay_client = new HuaweiPayClient($this->config); } diff --git a/src/addon/huaweipay/shop/view/pay/config.html b/src/addon/huaweipay/shop/view/pay/config.html index 1ddbff235..5fe1f1786 100644 --- a/src/addon/huaweipay/shop/view/pay/config.html +++ b/src/addon/huaweipay/shop/view/pay/config.html @@ -7,7 +7,7 @@
- +
diff --git a/src/addon/huaweipay/tests/HuaweiPayClientTest.php b/src/addon/huaweipay/tests/HuaweiPayClientTest.php deleted file mode 100644 index 548fbbf1b..000000000 --- a/src/addon/huaweipay/tests/HuaweiPayClientTest.php +++ /dev/null @@ -1,263 +0,0 @@ -configData = yaml_parse_file($yamlPath); - } else { - // 如果没有yaml_parse_file函数,手动解析 - $this->configData = $this->parseYamlFile($yamlPath); - } - } else { - throw new Exception("配置文件不存在: {$yamlPath}"); - } - } - - /** - * 手动解析YAML文件 - * @param string $filePath - * @return array - */ - private function parseYamlFile($filePath) - { - $content = file_get_contents($filePath); - $lines = explode("\n", $content); - $result = []; - $currentKey = ''; - $isMultiline = false; - $multilineValue = ''; - - foreach ($lines as $line) { - $line = trim($line); - - // 跳过注释和空行 - if (empty($line) || strpos($line, '#') === 0) { - continue; - } - - // 处理多行值 - if ($isMultiline) { - if (strpos($line, '-----END') === 0) { - $multilineValue .= $line . "\n"; - $result[$currentKey] = rtrim($multilineValue); - $isMultiline = false; - $multilineValue = ''; - } else { - $multilineValue .= $line . "\n"; - } - continue; - } - - // 处理单行键值对 - if (strpos($line, ':') !== false) { - list($key, $value) = explode(':', $line, 2); - $key = trim($key); - $value = trim($value); - - // 处理多行值开始 - if ($value === '|') { - $isMultiline = true; - $currentKey = $key; - continue; - } - - // 处理普通值 - if (!empty($value)) { - $result[$key] = $value; - } - } - } - - return $result; - } - - /** - * 模拟证书内容格式化方法 - * 与HuaweiPayClient::formatCertificateContent方法实现一致 - */ - private function formatCertificateContent($content, $type) - { - // 移除空白字符和换行 - $content = preg_replace('/\s+/', '', $content); - - // 检查是否已经包含格式头尾 - if (preg_match('/-----BEGIN\s+.*?-----/', $content) && preg_match('/-----END\s+.*?-----/', $content)) { - return $content; - } - - // 添加适当的格式头尾 - $header = ''; - $footer = ''; - - if ($type == 'private_key') { - $header = "-----BEGIN PRIVATE KEY-----"; - $footer = "-----END PRIVATE KEY-----"; - } elseif ($type == 'public_key') { - $header = "-----BEGIN PUBLIC KEY-----"; - $footer = "-----END PUBLIC KEY-----"; - } - - // 移除可能存在的旧格式 - $content = preg_replace('/-----BEGIN.*?-----/', '', $content); - $content = preg_replace('/-----END.*?-----/', '', $content); - - // 组合最终格式 - return $header . $content . $footer; - } - - /** - * 测试证书内容格式化功能 - */ - public function testFormatCertificateContent() - { - echo "开始测试证书内容格式化功能...\n\n"; - - // 从配置文件获取实际的密钥内容 - $privateKey = $this->configData['privateKey']; - $publicKey = $this->configData['huawei_public_key']; - - // 测试1:私钥格式化 - echo "测试1:私钥格式化..."; - $formattedPrivateKey = $this->formatCertificateContent($privateKey, 'private_key'); - - // 移除预期结果和实际结果中的所有换行符,统一比较 - $expectedClean = str_replace("\n", '', $privateKey); - $actualClean = str_replace("\n", '', $formattedPrivateKey); - - if (strpos($actualClean, '-----BEGIN PRIVATE KEY-----') !== false && strpos($actualClean, '-----END PRIVATE KEY-----') !== false) { - echo " ✅ 通过\n"; - } else { - echo " ❌ 失败\n"; - echo " 实际:$formattedPrivateKey\n"; - return false; - } - - // 测试2:公钥格式化 - echo "测试2:公钥格式化..."; - $formattedPublicKey = $this->formatCertificateContent($publicKey, 'public_key'); - - // 移除预期结果和实际结果中的所有换行符,统一比较 - $expectedClean = str_replace("\n", '', $publicKey); - $actualClean = str_replace("\n", '', $formattedPublicKey); - - if (strpos($actualClean, '-----BEGIN PUBLIC KEY-----') !== false && strpos($actualClean, '-----END PUBLIC KEY-----') !== false) { - echo " ✅ 通过\n"; - } else { - echo " ❌ 失败\n"; - echo " 实际:$formattedPublicKey\n"; - return false; - } - - // 测试3:带空格和换行的私钥格式化 - echo "测试3:带空格和换行的私钥格式化..."; - $privateKeyWithSpaces = ' ' . $privateKey . ' '; - $formattedPrivateKeyWithSpaces = $this->formatCertificateContent($privateKeyWithSpaces, 'private_key'); - - if (strpos($formattedPrivateKeyWithSpaces, '-----BEGIN PRIVATE KEY-----') !== false && strpos($formattedPrivateKeyWithSpaces, '-----END PRIVATE KEY-----') !== false) { - echo " ✅ 通过\n"; - } else { - echo " ❌ 失败\n"; - echo " 实际:$formattedPrivateKeyWithSpaces\n"; - return false; - } - - echo "\n✅ 所有证书内容格式化测试通过!\n"; - return true; - } - - /** - * 测试文本模式配置结构 - */ - public function testTextModeConfigStructure() - { - echo "\n开始测试文本模式配置结构...\n"; - - // 测试1:基本文本模式配置 - echo "测试1:基本文本模式配置..."; - $config1 = [ - 'app_id' => $this->configData['app_id'], - 'mch_id' => $this->configData['mch_id'], - 'private_key_text' => $this->configData['privateKey'], - 'huawei_public_key_text' => $this->configData['huawei_public_key'] - ]; - - if (isset($config1['private_key_text']) && isset($config1['huawei_public_key_text'])) { - echo " ✅ 通过\n"; - } else { - echo " ❌ 失败\n"; - return false; - } - - // 测试2:验证配置数据完整性 - echo "测试2:验证配置数据完整性..."; - $requiredFields = ['app_id', 'mch_id', 'mch_auth_id', 'privateKey', 'huawei_public_key']; - $missingFields = []; - - foreach ($requiredFields as $field) { - if (!isset($this->configData[$field])) { - $missingFields[] = $field; - } - } - - if (empty($missingFields)) { - echo " ✅ 通过\n"; - } else { - echo " ❌ 失败,缺少字段:" . implode(', ', $missingFields) . "\n"; - return false; - } - - echo "\n✅ 所有文本模式配置结构测试通过!\n"; - return true; - } - - /** - * 运行所有测试 - */ - public function runTests() - { - echo "开始运行华为支付文本模式测试...\n\n"; - - try { - $formatResult = $this->testFormatCertificateContent(); - $configResult = $this->testTextModeConfigStructure(); - - if ($formatResult && $configResult) { - echo "\n🎉 所有测试通过!华为支付文本模式功能正常工作!\n"; - return true; - } else { - echo "\n💥 部分测试失败!\n"; - return false; - } - } catch (Exception $e) { - echo "\n💥 测试失败:" . $e->getMessage() . "\n"; - echo "错误位置:" . $e->getFile() . " 第" . $e->getLine() . "行\n"; - return false; - } - } -} - -// 如果直接运行此文件,则执行测试 -if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) { - $test = new CertificateFormatTest(); - $test->runTests(); -} \ No newline at end of file diff --git a/src/addon/huaweipay/tests/HuaweiPayConfigTest.php b/src/addon/huaweipay/tests/HuaweiPayConfigTest.php new file mode 100644 index 000000000..03b43e6a5 --- /dev/null +++ b/src/addon/huaweipay/tests/HuaweiPayConfigTest.php @@ -0,0 +1,191 @@ +configData['private_key_text']; + $publicKey = $this->configData['huawei_public_key_text']; + + // 测试1:私钥格式化(文本内容模式) + $formattedPrivateKey = $this->formatCertificateContent($privateKey, 'private_key'); + $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $formattedPrivateKey, '私钥格式化失败:缺少BEGIN标记'); + $this->assertStringContainsString('-----END PRIVATE KEY-----', $formattedPrivateKey, '私钥格式化失败:缺少END标记'); + + // 测试2:公钥格式化(文本内容模式) + $formattedPublicKey = $this->formatCertificateContent($publicKey, 'public_key'); + $this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $formattedPublicKey, '公钥格式化失败:缺少BEGIN标记'); + $this->assertStringContainsString('-----END PUBLIC KEY-----', $formattedPublicKey, '公钥格式化失败:缺少END标记'); + + // 测试3:带空格和换行的私钥格式化 + $privateKeyWithSpaces = ' ' . $privateKey . ' '; + $formattedPrivateKeyWithSpaces = $this->formatCertificateContent($privateKeyWithSpaces, 'private_key'); + $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $formattedPrivateKeyWithSpaces, '带空格私钥格式化失败:缺少BEGIN标记'); + $this->assertStringContainsString('-----END PRIVATE KEY-----', $formattedPrivateKeyWithSpaces, '带空格私钥格式化失败:缺少END标记'); + + // 测试4:没有格式的私钥格式化 + $rawPrivateKey = str_replace(["\n", '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----'], '', $privateKey); + $formattedRawPrivateKey = $this->formatCertificateContent($rawPrivateKey, 'private_key'); + $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $formattedRawPrivateKey, '无格式私钥格式化失败:缺少BEGIN标记'); + $this->assertStringContainsString('-----END PRIVATE KEY-----', $formattedRawPrivateKey, '无格式私钥格式化失败:缺少END标记'); + } + + /** + * 测试文本模式配置结构 + */ + public function testTextModeConfigStructure() + { + // 测试1:基本文本模式配置 + $config = [ + 'app_id' => $this->configData['app_id'], + 'merc_no' => $this->configData['merc_no'], + 'private_key_text' => $this->configData['private_key_text'], + 'huawei_public_key_text' => $this->configData['huawei_public_key_text'] + ]; + + $this->assertArrayHasKey('private_key_text', $config, '配置缺少private_key_text字段'); + $this->assertArrayHasKey('huawei_public_key_text', $config, '配置缺少huawei_public_key_text字段'); + + // 测试2:验证配置数据完整性 + $requiredFields = ['app_id', 'merc_no', 'mch_auth_id', 'private_key_text', 'huawei_public_key_text']; + foreach ($requiredFields as $field) { + $this->assertArrayHasKey($field, $this->configData, "配置文件缺少必填字段:{$field}"); + } + } + + /** + * 测试文件路径模式配置 + */ + public function testFilePathModeConfig() + { + // 测试1:检查文件路径配置是否存在 + $this->assertArrayHasKey('private_key', $this->configData, '配置缺少private_key字段(文件路径模式)'); + $this->assertArrayHasKey('huawei_public_key', $this->configData, '配置缺少huawei_public_key字段(文件路径模式)'); + + // 测试2:验证文件路径格式 + $privateKeyPath = $this->configData['private_key']; + $publicKeyPath = $this->configData['huawei_public_key']; + + $this->assertStringEndsWith('.pem', $privateKeyPath, '私钥文件路径必须以.pem结尾'); + $this->assertStringEndsWith('.pem', $publicKeyPath, '公钥文件路径必须以.pem结尾'); + + // 测试3:验证文件是否存在(如果在测试环境中可用) + $fullPrivateKeyPath = __DIR__ . '/' . $privateKeyPath; + $fullPublicKeyPath = __DIR__ . '/' . $publicKeyPath; + + if (file_exists($fullPrivateKeyPath)) { + $this->assertFileExists($fullPrivateKeyPath, '私钥文件不存在'); + } + + if (file_exists($fullPublicKeyPath)) { + $this->assertFileExists($fullPublicKeyPath, '公钥文件不存在'); + } + + // 测试4:验证文件内容(如果文件存在) + if (file_exists($fullPrivateKeyPath)) { + $privateKeyContent = file_get_contents($fullPrivateKeyPath); + $this->assertNotEmpty($privateKeyContent, '私钥文件内容为空'); + $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $privateKeyContent, '私钥文件内容格式不正确'); + $this->assertStringContainsString('-----END PRIVATE KEY-----', $privateKeyContent, '私钥文件内容格式不正确'); + } + + if (file_exists($fullPublicKeyPath)) { + $publicKeyContent = file_get_contents($fullPublicKeyPath); + $this->assertNotEmpty($publicKeyContent, '公钥文件内容为空'); + $this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $publicKeyContent, '公钥文件内容格式不正确'); + $this->assertStringContainsString('-----END PUBLIC KEY-----', $publicKeyContent, '公钥文件内容格式不正确'); + } + } + + /** + * 测试两种配置模式的相互转换 + */ + public function testConfigModeConversion() + { + // 测试1:文本内容转换为文件路径模式(模拟) + $privateKeyText = $this->configData['private_key_text']; + $publicKeyText = $this->configData['huawei_public_key_text']; + + // 模拟将文本内容保存到临时文件 + $tempPrivateKeyPath = sys_get_temp_dir() . '/temp_private_key.pem'; + $tempPublicKeyPath = sys_get_temp_dir() . '/temp_public_key.pem'; + + file_put_contents($tempPrivateKeyPath, $privateKeyText); + file_put_contents($tempPublicKeyPath, $publicKeyText); + + // 验证临时文件内容 + $this->assertFileExists($tempPrivateKeyPath, '临时私钥文件创建失败'); + $this->assertFileExists($tempPublicKeyPath, '临时公钥文件创建失败'); + + $this->assertStringEqualsFile($tempPrivateKeyPath, $privateKeyText, '临时私钥文件内容与原文本不一致'); + $this->assertStringEqualsFile($tempPublicKeyPath, $publicKeyText, '临时公钥文件内容与原文本不一致'); + + // 清理临时文件 + unlink($tempPrivateKeyPath); + unlink($tempPublicKeyPath); + + // 测试2:文件路径转换为文本内容模式(如果文件存在) + $privateKeyPath = $this->configData['private_key']; + $publicKeyPath = $this->configData['huawei_public_key']; + + $fullPrivateKeyPath = __DIR__ . '/' . $privateKeyPath; + $fullPublicKeyPath = __DIR__ . '/' . $publicKeyPath; + + if (file_exists($fullPrivateKeyPath) && file_exists($fullPublicKeyPath)) { + $privateKeyContent = file_get_contents($fullPrivateKeyPath); + $publicKeyContent = file_get_contents($fullPublicKeyPath); + + // 验证从文件读取的内容可以被正确格式化 + $formattedPrivateKey = $this->formatCertificateContent($privateKeyContent, 'private_key'); + $formattedPublicKey = $this->formatCertificateContent($publicKeyContent, 'public_key'); + + $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $formattedPrivateKey, '从文件读取的私钥格式化失败'); + $this->assertStringContainsString('-----END PRIVATE KEY-----', $formattedPrivateKey, '从文件读取的私钥格式化失败'); + $this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $formattedPublicKey, '从文件读取的公钥格式化失败'); + $this->assertStringContainsString('-----END PUBLIC KEY-----', $formattedPublicKey, '从文件读取的公钥格式化失败'); + } + } + + /** + * 测试配置模式优先级 + */ + public function testConfigModePriority() + { + // 测试1:文本内容模式优先于文件路径模式 + // 模拟配置同时包含两种模式 + $config = [ + 'private_key' => 'mock/cert/merchant_private_key.pem', + 'private_key_text' => $this->configData['private_key_text'], + 'huawei_public_key' => 'mock/cert/huawei_public_key.pem', + 'huawei_public_key_text' => $this->configData['huawei_public_key_text'] + ]; + + // 验证两种模式的配置都存在 + $this->assertArrayHasKey('private_key', $config, '配置缺少private_key字段'); + $this->assertArrayHasKey('private_key_text', $config, '配置缺少private_key_text字段'); + $this->assertArrayHasKey('huawei_public_key', $config, '配置缺少huawei_public_key字段'); + $this->assertArrayHasKey('huawei_public_key_text', $config, '配置缺少huawei_public_key_text字段'); + + // 模拟应用文本内容优先的逻辑 + $finalPrivateKey = isset($config['private_key_text']) ? $config['private_key_text'] : $config['private_key']; + $finalPublicKey = isset($config['huawei_public_key_text']) ? $config['huawei_public_key_text'] : $config['huawei_public_key']; + + // 验证文本内容模式被优先使用 + $this->assertSame($config['private_key_text'], $finalPrivateKey, '文本内容模式应该优先于文件路径模式'); + $this->assertSame($config['huawei_public_key_text'], $finalPublicKey, '文本内容模式应该优先于文件路径模式'); + } +} \ No newline at end of file diff --git a/src/addon/huaweipay/tests/PayModelTest.php b/src/addon/huaweipay/tests/PayModelTest.php index ce4fe19b5..1af59b64d 100644 --- a/src/addon/huaweipay/tests/PayModelTest.php +++ b/src/addon/huaweipay/tests/PayModelTest.php @@ -3,263 +3,417 @@ * 华为支付模型测试类 */ -namespace addon\huaweipay\tests; +// 设置错误报告 +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); -use addon\huaweipay\model\Pay; -use addon\huaweipay\model\Config; -use think\facade\Log; +// 添加自动加载路径 +$root_path = dirname(__DIR__, 3); +require_once $root_path . '/vendor/autoload.php'; + +// 定义常量 +if (!defined('ROOT_PATH')) { + define('ROOT_PATH', $root_path . '/'); +} + +// 模拟app()函数,解决框架依赖问题 +function app() { + return new class { + public function getRootPath() { + return dirname(__DIR__, 3) . '\\'; + } + }; +} class PayModelTest { - /** - * 测试站点ID - * @var int - */ - protected $siteId = 1; - - /** - * 测试配置数据 - * @var array - */ - protected $configData; + protected $config = []; + protected $pay_model = null; /** - * 构造函数,加载配置文件 + * 构造函数 */ public function __construct() { - // 加载配置文件 - $yamlPath = __DIR__ . '/mock/data.yml'; - if (file_exists($yamlPath)) { - if (function_exists('yaml_parse_file')) { - $this->configData = yaml_parse_file($yamlPath); - } else { - // 如果没有yaml_parse_file函数,手动解析 - $this->configData = $this->parseYamlFile($yamlPath); - } - } else { - throw new Exception("配置文件不存在: {$yamlPath}"); - } + // 加载测试配置 + $this->config = $this->parseYamlFile(__DIR__ . '/mock/data.yml'); + echo "配置加载完成\n"; + // print_r($this->config); } - + /** - * 手动解析YAML文件 - * @param string $filePath + * 解析YAML文件 + * @param string $file_path YAML文件路径 * @return array */ - private function parseYamlFile($filePath) + protected function parseYamlFile($file_path) { - $content = file_get_contents($filePath); - $lines = explode("\n", $content); - $result = []; - $currentKey = ''; - $isMultiline = false; - $multilineValue = ''; + if (!file_exists($file_path)) { + throw new Exception("YAML文件不存在: {$file_path}"); + } + + $yaml_content = file_get_contents($file_path); + if ($yaml_content === false) { + throw new Exception("读取YAML文件失败: {$file_path}"); + } + + // 使用正则替换YAML中的\n为实际换行符 + $yaml_content = preg_replace_callback('/\\\\n/', function($matches) { + return "\n"; + }, $yaml_content); + + // 简单的YAML解析(仅支持当前测试文件的格式) + $config = []; + $lines = explode("\n", $yaml_content); + $current_key = ''; + $in_block = false; + $block_content = ''; foreach ($lines as $line) { $line = trim($line); - - // 跳过注释和空行 if (empty($line) || strpos($line, '#') === 0) { continue; } - - // 处理多行值 - if ($isMultiline) { - if (strpos($line, '-----END') === 0) { - $multilineValue .= $line . "\n"; - $result[$currentKey] = rtrim($multilineValue); - $isMultiline = false; - $multilineValue = ''; - } else { - $multilineValue .= $line . "\n"; + + if ($in_block) { + if (strpos($line, '---') === 0) { + continue; + } + if (preg_match('/^\w+:/', $line)) { + $config[$current_key] = $block_content; + $in_block = false; + $block_content = ''; + } else { + $block_content .= $line . "\n"; + continue; } - continue; } - - // 处理单行键值对 + if (strpos($line, ':') !== false) { list($key, $value) = explode(':', $line, 2); $key = trim($key); $value = trim($value); - // 处理多行值开始 - if ($value === '|') { - $isMultiline = true; - $currentKey = $key; + if (strpos($value, '|') !== false) { + $in_block = true; + $current_key = $key; continue; } - // 处理普通值 if (!empty($value)) { - $result[$key] = $value; + $config[$key] = $value; } } } - return $result; + if ($in_block && !empty($current_key)) { + $config[$current_key] = $block_content; + } + + return $config; } /** - * 运行所有测试 + * 运行测试 */ public function runTests() { - echo "开始测试华为支付模型...\n"; + echo "开始运行华为支付模型测试...\n"; try { + // 1. 测试支付方法 + echo "1. 测试支付方法...\n"; $this->testPayMethod(); - $this->testNotifyMethod(); - $this->testCloseOrderMethod(); - $this->testRefundMethod(); - $this->testQueryOrderMethod(); + echo "1. 支付方法测试通过\n"; - echo "\n✅ 所有测试通过!\n"; - return true; - } catch (\Exception $e) { - echo "\n❌ 测试失败:" . $e->getMessage() . "\n"; - echo "错误位置:" . $e->getFile() . " 第" . $e->getLine() . "行\n"; + // 2. 测试关闭订单方法 + echo "2. 测试关闭订单方法...\n"; + $this->testCloseOrderMethod(); + echo "2. 关闭订单方法测试通过\n"; + + // 3. 测试退款方法 + echo "3. 测试退款方法...\n"; + $this->testRefundMethod(); + echo "3. 退款方法测试通过\n"; + + // 4. 测试查询订单方法 + echo "4. 测试查询订单方法...\n"; + $this->testQueryOrderMethod(); + echo "4. 查询订单方法测试通过\n"; + + // 5. 测试回调验证方法 + echo "5. 测试回调验证方法...\n"; + $this->testCallbackVerification(); + echo "5. 回调验证方法测试通过\n"; + + echo "\n所有测试通过!\n"; + } catch (Exception $e) { + echo "\n测试失败:" . $e->getMessage() . "\n"; + echo "错误堆栈:" . $e->getTraceAsString() . "\n"; return false; } + + return true; } /** * 测试支付方法 */ - public function testPayMethod() + protected function testPayMethod() { - echo "测试支付方法..."; + // 创建支付模型实例 + require_once dirname(__DIR__) . '/model/Pay.php'; + require_once dirname(__DIR__) . '/data/sdk/utils.php'; + require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php'; + + // 添加必要的配置 + $site_id = '1'; + $config_data = $this->config; + $config_data['site_id'] = $site_id; + // 使用文本模式的密钥,而不是证书文件 + unset($config_data['private_key']); + unset($config_data['huawei_public_key']); try { - // 模拟配置 - $this->mockConfig(); + // 创建实际的华为支付客户端实例 + $project_root = dirname(__DIR__, 4); // 获取项目根目录 + $huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root); + echo " - 华为支付客户端实例创建成功\n"; - // 创建测试订单数据 - $orderInfo = [ - 'out_trade_no' => 'test_order_' . time(), - 'total_amount' => 100, - 'subject' => '测试商品', - 'notify_url' => $this->configData['notifyUrl'], - 'return_url' => $this->configData['returnUrl'], - 'buyer_id' => '1', - 'type' => 'h5' + // 测试H5支付 + $h5_params = [ + 'out_trade_no' => 'TEST_H5_' . time(), + 'subject' => '测试H5支付商品', + 'body' => '测试H5支付商品描述', + 'total_amount' => 0.01, + 'client_ip' => '127.0.0.1' ]; - // 尝试创建Pay实例 - $payModel = new Pay($this->siteId); + // 调用真实的H5支付方法 + $h5_result = $huawei_pay_client->h5Pay($h5_params, $config_data['returnUrl'], $config_data['notifyUrl']); + echo " - H5支付方法调用成功\n"; + echo " 结果: " . json_encode($h5_result, JSON_UNESCAPED_UNICODE) . "\n"; - // 由于需要实际的支付客户端,这里只测试模型初始化 - echo " ⚠️ 跳过实际支付测试,仅测试模型初始化\n"; - } catch (\Exception $e) { - echo " ⚠️ 模型初始化测试跳过: " . $e->getMessage() . "\n"; - } - } - - /** - * 测试通知方法 - */ - public function testNotifyMethod() - { - echo "测试通知方法..."; - - try { - // 模拟配置 - $this->mockConfig(); - - // 创建模拟通知数据 - $notifyData = [ - 'app_id' => $this->configData['app_id'], - 'merc_no' => $this->configData['mch_id'], - 'order_id' => 'test_order_001', - 'trade_status' => 'SUCCESS', - 'sign' => 'mock_signature' + // 测试APP支付 + $app_params = [ + 'out_trade_no' => 'TEST_APP_' . time(), + 'subject' => '测试APP支付商品', + 'body' => '测试APP支付商品描述', + 'total_amount' => 0.01 ]; - // 尝试创建Pay实例 - $payModel = new Pay($this->siteId); + // 调用真实的APP支付方法 + $app_result = $huawei_pay_client->appPay($app_params, $config_data['notifyUrl']); + echo " - APP支付方法调用成功\n"; + echo " 结果: " . json_encode($app_result, JSON_UNESCAPED_UNICODE) . "\n"; - echo " ⚠️ 跳过实际通知处理测试\n"; - } catch (\Exception $e) { - echo " ⚠️ 通知测试跳过: " . $e->getMessage() . "\n"; + } catch (Exception $e) { + echo " - 支付测试失败: " . $e->getMessage() . "\n"; + throw $e; } } /** * 测试关闭订单方法 */ - public function testCloseOrderMethod() + protected function testCloseOrderMethod() { - echo "测试关闭订单方法..."; + require_once dirname(__DIR__) . '/data/sdk/utils.php'; + require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php'; + + // 添加必要的配置 + $site_id = '1'; + $config_data = $this->config; + $config_data['site_id'] = $site_id; + // 使用文本模式的密钥,而不是证书文件 + unset($config_data['private_key']); + unset($config_data['huawei_public_key']); try { - // 模拟配置 - $this->mockConfig(); + // 创建实际的华为支付客户端实例 + $project_root = dirname(__DIR__, 4); // 获取项目根目录 + $huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root); + echo " - 华为支付客户端实例创建成功\n"; - // 尝试创建Pay实例 - $payModel = new Pay($this->siteId); + // 测试关闭订单 + $params = [ + 'out_trade_no' => 'TEST_CLOSE_' . time() + ]; - echo " ⚠️ 跳过实际关闭订单测试\n"; - } catch (\Exception $e) { - echo " ⚠️ 关闭订单测试跳过: " . $e->getMessage() . "\n"; + // 调用真实的关闭订单方法 + $close_result = $huawei_pay_client->closeOrder($params); + echo " - 关闭订单方法调用成功\n"; + echo " 结果: " . json_encode($close_result, JSON_UNESCAPED_UNICODE) . "\n"; + + } catch (Exception $e) { + echo " - 关闭订单测试失败: " . $e->getMessage() . "\n"; + throw $e; } } /** * 测试退款方法 */ - public function testRefundMethod() + protected function testRefundMethod() { - echo "测试退款方法..."; + require_once dirname(__DIR__) . '/data/sdk/utils.php'; + require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php'; + + // 添加必要的配置 + $site_id = '1'; + $config_data = $this->config; + $config_data['site_id'] = $site_id; + // 使用文本模式的密钥,而不是证书文件 + unset($config_data['private_key']); + unset($config_data['huawei_public_key']); try { - // 模拟配置 - $this->mockConfig(); + // 创建实际的华为支付客户端实例 + $project_root = dirname(__DIR__, 4); // 获取项目根目录 + $huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root); + echo " - 华为支付客户端实例创建成功\n"; - // 尝试创建Pay实例 - $payModel = new Pay($this->siteId); + // 测试退款 + $params = [ + 'out_trade_no' => 'TEST_REFUND_' . time(), + 'out_request_no' => 'REFUND_' . time(), + 'refund_amount' => 0.01, + 'refund_reason' => '测试退款' + ]; - echo " ⚠️ 跳过实际退款测试\n"; - } catch (\Exception $e) { - echo " ⚠️ 退款测试跳过: " . $e->getMessage() . "\n"; + // 调用真实的退款方法 + $refund_result = $huawei_pay_client->refund($params); + echo " - 退款方法调用成功\n"; + echo " 结果: " . json_encode($refund_result, JSON_UNESCAPED_UNICODE) . "\n"; + + } catch (Exception $e) { + echo " - 退款测试失败: " . $e->getMessage() . "\n"; + throw $e; } } /** * 测试查询订单方法 */ - public function testQueryOrderMethod() + protected function testQueryOrderMethod() { - echo "测试查询订单方法..."; + require_once dirname(__DIR__) . '/data/sdk/utils.php'; + require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php'; + + // 添加必要的配置 + $site_id = '1'; + $config_data = $this->config; + $config_data['site_id'] = $site_id; + // 使用文本模式的密钥,而不是证书文件 + unset($config_data['private_key']); + unset($config_data['huawei_public_key']); try { - // 模拟配置 - $this->mockConfig(); + // 创建实际的华为支付客户端实例 + $project_root = dirname(__DIR__, 4); // 获取项目根目录 + $huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root); + echo " - 华为支付客户端实例创建成功\n"; - // 尝试创建Pay实例 - $payModel = new Pay($this->siteId); + // 测试查询订单 + $params = [ + 'out_trade_no' => 'TEST_QUERY_' . time() + ]; - echo " ⚠️ 跳过实际查询订单测试\n"; - } catch (\Exception $e) { - echo " ⚠️ 查询订单测试跳过: " . $e->getMessage() . "\n"; + // 调用真实的查询订单方法 + $query_result = $huawei_pay_client->queryOrder($params); + echo " - 查询订单方法调用成功\n"; + echo " 结果: " . json_encode($query_result, JSON_UNESCAPED_UNICODE) . "\n"; + + } catch (Exception $e) { + echo " - 查询订单测试失败: " . $e->getMessage() . "\n"; + throw $e; } } /** - * 模拟配置 + * 测试回调验证方法 */ - private function mockConfig() + protected function testCallbackVerification() { - // 这里可以添加更高级的模拟技术,例如使用PHPUnit的mock对象 - // 目前我们只需要确保配置数据能正确加载 - echo "\n使用配置数据:\n"; - echo "App ID: {$this->configData['app_id']}\n"; - echo "商户号: {$this->configData['mch_id']}\n"; - echo "证书ID: {$this->configData['mch_auth_id']}\n"; + require_once dirname(__DIR__) . '/data/sdk/utils.php'; + require_once dirname(__DIR__) . '/data/sdk/HuaweiPayClient.php'; + + // 添加必要的配置 + $site_id = '1'; + $config_data = $this->config; + $config_data['site_id'] = $site_id; + // 使用文本模式的密钥,而不是证书文件 + unset($config_data['private_key']); + unset($config_data['huawei_public_key']); + + try { + // 创建实际的华为支付客户端实例 + $project_root = dirname(__DIR__, 4); // 获取项目根目录 + $huawei_pay_client = new \addon\huaweipay\data\sdk\HuaweiPayClient($config_data, $project_root); + echo " - 华为支付客户端实例创建成功\n"; + + // 生成模拟回调参数并签名 + $timestamp = date('Y-m-d H:i:s'); + $callback_params = [ + 'out_trade_no' => 'TEST_CALLBACK_' . time(), + 'trade_no' => 'HWPAY_' . time(), + 'total_amount' => '0.01', + 'trade_status' => 'SUCCESS', + 'timestamp' => $timestamp, + 'app_id' => $config_data['app_id'], + 'merc_no' => $config_data['merc_no'], + 'sign_type' => 'RSA2' + ]; + + // 生成签名 + $sign = ''; + $signParams = $callback_params; + unset($signParams['sign']); + unset($signParams['sign_type']); + ksort($signParams); + + $stringToSign = ''; + foreach ($signParams as $key => $value) { + $stringToSign .= $key . '=' . $value . '&'; + } + $stringToSign = rtrim($stringToSign, '&'); + + // 使用文本模式的私钥生成签名 + $private_key_content = $config_data['private_key_text']; + + // 对私钥进行格式化,确保openssl_sign能够正确处理 + $cleanContent = preg_replace('/\s+/', '', $private_key_content); + $cleanContent = preg_replace('/-----BEGIN.*?-----/', '', $cleanContent); + $cleanContent = preg_replace('/-----END.*?-----/', '', $cleanContent); + $formattedKey = "-----BEGIN PRIVATE KEY-----\n"; + $length = strlen($cleanContent); + for ($i = 0; $i < $length; $i += 64) { + $formattedKey .= substr($cleanContent, $i, 64) . "\n"; + } + $formattedKey .= "-----END PRIVATE KEY-----"; + + openssl_sign($stringToSign, $sign, $formattedKey, OPENSSL_ALGO_SHA256); + $callback_params['sign'] = base64_encode($sign); + + // 使用华为支付客户端验证签名 + $verify_result = $huawei_pay_client->verifySign($callback_params); + + if ($verify_result) { + echo " - 回调签名验证通过\n"; + echo " - 回调参数:" . json_encode($callback_params, JSON_UNESCAPED_UNICODE) . "\n"; + echo " - 交易状态:{$callback_params['trade_status']}\n"; + } else { + throw new Exception("回调签名验证失败"); + } + + } catch (Exception $e) { + echo " - 回调验证测试失败: " . $e->getMessage() . "\n"; + throw $e; + } } } -// 如果直接运行此文件,则执行测试 -if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) { - $test = new PayModelTest(); - $test->runTests(); -} \ No newline at end of file +// 运行测试 +$test = new PayModelTest(); +$test->runTests(); \ No newline at end of file diff --git a/src/addon/huaweipay/tests/README.md b/src/addon/huaweipay/tests/README.md deleted file mode 100644 index 51faf6898..000000000 --- a/src/addon/huaweipay/tests/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# 测试 - -由于不引入PHPUnit,所以测试用例只能手动执行。 - - -```bash -cd src/ -php addon/huaweipay/tests/HuaweiPayClientTest.php -php addon/huaweipay/tests/PayModelTest.php -``` \ No newline at end of file diff --git a/src/addon/huaweipay/tests/TestCase.php b/src/addon/huaweipay/tests/TestCase.php new file mode 100644 index 000000000..042c5534a --- /dev/null +++ b/src/addon/huaweipay/tests/TestCase.php @@ -0,0 +1,117 @@ +loadConfigData(); + } + + /** + * 加载配置数据 + */ + protected function loadConfigData() + { + $yamlPath = __DIR__ . '/mock/data.yml'; + if (file_exists($yamlPath)) { + if (function_exists('yaml_parse_file')) { + $this->configData = yaml_parse_file($yamlPath); + } else { + // 如果没有yaml_parse_file函数,手动解析 + $this->configData = $this->parseYamlFile($yamlPath); + } + } else { + $this->fail("配置文件不存在: {$yamlPath}"); + } + } + + protected function formatCertificateContent($content, $type = 'public_key') + { + return Utils::formatCertificateContent($content, $type); + } + + /** + * 手动解析YAML文件 + * @param string $filePath + * @return array + */ + private function parseYamlFile($filePath) + { + $content = file_get_contents($filePath); + $lines = explode("\n", $content); + $result = []; + $currentKey = ''; + $isMultiline = false; + $multilineValue = ''; + + foreach ($lines as $line) { + $line = trim($line); + + // 跳过注释和空行 + if (empty($line) || strpos($line, '#') === 0) { + continue; + } + + // 处理多行值 + if ($isMultiline) { + if (strpos($line, '-----END') === 0) { + $multilineValue .= $line . "\n"; + $result[$currentKey] = rtrim($multilineValue); + $isMultiline = false; + $multilineValue = ''; + } else { + $multilineValue .= $line . "\n"; + } + continue; + } + + // 处理单行键值对 + if (strpos($line, ':') !== false) { + list($key, $value) = explode(':', $line, 2); + $key = trim($key); + $value = trim($value); + + // 处理多行值开始 + if ($value === '|') { + $isMultiline = true; + $currentKey = $key; + continue; + } + + // 处理普通值 + if (!empty($value)) { + $result[$key] = $value; + } + } + } + + return $result; + } +} \ No newline at end of file diff --git a/src/addon/huaweipay/tests/mock/cert/huawei_public_key.pem b/src/addon/huaweipay/tests/mock/cert/huawei_public_key.pem index 76cae1b46..10711ef74 100644 --- a/src/addon/huaweipay/tests/mock/cert/huawei_public_key.pem +++ b/src/addon/huaweipay/tests/mock/cert/huawei_public_key.pem @@ -1 +1,3 @@ ------BEGIN PUBLIC KEY-----\nMOCK_PUBLIC_KEY_CONTENT\n-----END PUBLIC KEY----- \ No newline at end of file +-----BEGIN PUBLIC KEY----- +MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA9g1+QcqvC4f1pUiwJ1um1iBUlNn6hRDJrNdv5zB77l5DNo6S6hE4w7VyhkMnkIk89i8kTej1m1ByjRpo7B5OPqafNqI9JBQyQ26A1Zp71zSfe/UicAFiMtF4lWNnAHBYH06sUTvybwYllDVybpi6lL2i8VAGIN8YgoK36lPaYsxWZ911lPCegy7B3kDj1xhBe41cNHgu8wYmjqLU7njleY5Pseherx+Kb58aQvB5xQr8w7KgAyMrsfRH30Btpg/ZWRn8qOXd/DW6eEla3djah4ug8jKdi0qUkA24FLDdOZST4vb5qhgQDVXpqJhYmBIU14YOHsCX9Olu6b7DDjQo/dvOaY3vzWROfV+sV60fUVIps8Vy1EpS/UXeHUxg6r37U8WAxUbSV8d6e4VylLuiIgbX5JpSC1s7jq/cwUwXfSJmKzaCj+C+LJ958IM17FYxIz5xWJtZEzWsPAH7WVCP3b1m4MHU/UwGuMu/Gfdzusnr+Qtan6Wqn9AqUyJP/JfrAgMBAAE= +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/src/addon/huaweipay/tests/mock/cert/merchant_private_key.pem b/src/addon/huaweipay/tests/mock/cert/merchant_private_key.pem index c9ed9b515..2514642be 100644 --- a/src/addon/huaweipay/tests/mock/cert/merchant_private_key.pem +++ b/src/addon/huaweipay/tests/mock/cert/merchant_private_key.pem @@ -1,8 +1,3 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2m2vKv9jH0QZc7K -+1q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q -3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q -3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q -3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q -3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q3q +MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQD2DX5Byq8Lh/WlSLAnW6bWIFSU2fqFEMms12/nMHvuXkM2jpLqETjDtXKGQyeQiTz2LyRN6PWbUHKNGmjsHk4+pp82oj0kFDJDboDVmnvXNJ979SJwAWIy0XiVY2cAcFgfTqxRO/JvBiWUNXJumLqUvaLxUAYg3xiCgrfqU9pizFZn3XWU8J6DLsHeQOPXGEF7jVw0eC7zBiaOotTueOV5jk+x6F6vH4pvnxpC8HnFCvzDsqADIyux9EffQG2mD9lZGfyo5d38Nbp4SVrd2NqHi6DyMp2LSpSQDbgUsN05lJPi9vmqGBANVemomFiYEhTXhg4ewJf06W7pvsMONCj9285pje/NZE59X6xXrR9RUimzxXLUSlL9Rd4dTGDqvftTxYDFRtJXx3p7hXKUu6IiBtfkmlILWzuOr9zBTBd9ImYrNoKP4L4sn3nwgzXsVjEjPnFYm1kTNaw8AftZUI/dvWbgwdT9TAa4y78Z93O6yev5C1qfpaqf0CpTIk/8l+sCAwEAAQKCAYAVlJFiS9iWdlJBMOLiUNONLEC+3W9vhE1r72lNKZ91BKd4fYC9Ls1/vMZSqEksEB1cqj3Q54HDIYcqgQp6yx2puQt1yzz5kRvndiWulmIOOftS7+kZUcW/F0gwMguyqifQdyH97fgRbMSW/ykOMi8LJKbJ627eKzMHH1fqIXih+bIKYg4SBhihANTYHXDeSK5Vm8xefbwAbKWtFPMAB3J4+tZakDrduTJ3H8k53cWQVqpcr6oBHHCUpww2tHvpeLI3a/FXyHYBqrx8ErnXCjkVBHBtwQf+43H+buyDjrYUwJUi3RSJgeefcyyJoO0I9GwGb6nY8kK5kZ0aeIIkfiVOexMYS9w11FVl96LjC7HjJQrNY4jOLI/X76xyEwBFy9LNogRTafjZVZmVRj/9Kembx6+/eDxvyEv5FnelsHqfFbv1KPkR6e7FvwpDbYgJBpfKTE6SICqo52bHxewpSL7KHRlTpp0IfMM/IOOs8CCwI37ixKQun6W4en457j/tP4ECgcEA+0pa3omCKJfiVR4PpyXY+t9FEqdcZkdYEDtCSX9vqu6yE4sdt7rNnZ6cIiD6ViC2xvQ5VLry1y57JvP57svt5OJYhmILuj3jJ7pdcfKRAVaV4W7C8vGMit6ssckEOnOSj/bvEazfHorjXKU9cn9uCymczRQC9azaRkN+1LLk2mBbczghxd7rSJyXtbt9NVNUi9Gr11AzJsrWN/Bqqna5BizBLIaGYthJ/04LYTl1AjWPEJXjXI72/WvMdQ/w6itxAoHBAPqqAi5mbvjRDitmLQchc/R3Yl329OaB3GSz3YtFqfk9lLJIyl91+MzxBg1ChHnRFC8MB1s1Z05ZkFmcy8AVqvJ6MSZhUCLNjq8HyVgp24CqQTr8ciomF03ncXnKKSsH7tidInkQ8R3e7qRRGQMnDvV+Hs3FjlOk8vJyZCcGkUCVA0kkwkk/95qIfrZsthtewAxJR/rY63FnG+MUayEBB1lwyJZ7xDi/GyX6SAnPBl6bLRA1BdBeTV8K0MEwwIqzGwKBwBqo+9UKT7XQz2FqbAy2tjt/fouJGAN95DjsoI69p3JCGsB6DPAWMIRddIEmcIi8tceL151GrEbqFoS+c7DDD/0timjPdCEROc1YN1vEeV/j+MjPAH3X5KpDD51ZD0rIQi9l6l08svtBjvegTFGedWVXx9v2GI5KBWpY9NbKF/+XI3yo4uRkTyAIBQxx1MnYimq/FvUj/BlMgceziQ2GxQCDtQbtSsqn2cntVMW+28wdNI106YdDX67pRerRgyTE8QKBwHAwLxG9XuWWC5V5AaYzXsaHuEr+ANY6QP4BUqLG5zBaU3cIBSt8jYKMTX0ZzFkJLtNvussjt7zlcSnqd3bdO8mSzvSykT9CaR4FiiQfd9K6YL+ZxS8AJWYEtFEiHhLYVho1Gfy9jG0mHgEFGwDCNnvBmt/WD8F4DhRdBl5BHjmdd/8AqMRIEPXlKXFUbp0JZ0MYeVLYS2hSEbUsqlX3M+bgB6bydfw/7FKvFhbtxZgKM70RPizoSBDFsnEE9OgfCQKBwQD4M4f2f678YScvrwQnV2J8SSnJhQbZqht1Rlb34zU0JIcfpwOKpoNvRyZz1BxvzPSm5S3TLDytK16uvAsMFCHVByyCajPozlWnLzeEqvE4DZXZcWeY+/+MZ4nsajLTTeJWos6UrazZcd8/2c301x4OUKfgZWW1tS65MA48X0+Y4uPyOuH365wQ0obBfg4aB85sZl71v7Rveq+COHBstp/BdDVAsdufWCr7Nkbeu/3qh886ZumpYe+89So1oDIuoys= -----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/addon/huaweipay/tests/mock/data.yml b/src/addon/huaweipay/tests/mock/data.yml index c6867fb71..5f0783243 100644 --- a/src/addon/huaweipay/tests/mock/data.yml +++ b/src/addon/huaweipay/tests/mock/data.yml @@ -1,26 +1,34 @@ -# 华为支付测试用简单配置 +# ========华为支付测试用简单配置========== + +# 沙盒模式 +sandbox: true # 商户应用ID,(例如:快应用) app_id: 115644647 # 商户号 -mch_id: 102751500028 +merc_no: 102751500028 # 商户证书ID mch_auth_id: 10086000901972225 +# 商户应用私有密钥(支付私钥) - 文件路径 +private_key: mock/cert/merchant_private_key.pem -# 商户应用私有密钥(支付私钥) -privateKey: | +# 商户应用私有密钥(支付私钥) - 文本内容 +private_key_text: | -----BEGIN PRIVATE KEY----- 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 -----END PRIVATE KEY----- -# 华为支付公钥 -huawei_public_key: | +# 华为支付公钥 - 文件路径 +huawei_public_key: mock/cert/huawei_public_key.pem + +# 华为支付公钥 - 文本内容 +huawei_public_key_text: | -----BEGIN PUBLIC KEY----- MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA9g1+QcqvC4f1pUiwJ1um1iBUlNn6hRDJrNdv5zB77l5DNo6S6hE4w7VyhkMnkIk89i8kTej1m1ByjRpo7B5OPqafNqI9JBQyQ26A1Zp71zSfe/UicAFiMtF4lWNnAHBYH06sUTvybwYllDVybpi6lL2i8VAGIN8YgoK36lPaYsxWZ911lPCegy7B3kDj1xhBe41cNHgu8wYmjqLU7njleY5Pseherx+Kb58aQvB5xQr8w7KgAyMrsfRH30Btpg/ZWRn8qOXd/DW6eEla3djah4ug8jKdi0qUkA24FLDdOZST4vb5qhgQDVXpqJhYmBIU14YOHsCX9Olu6b7DDjQo/dvOaY3vzWROfV+sV60fUVIps8Vy1EpS/UXeHUxg6r37U8WAxUbSV8d6e4VylLuiIgbX5JpSC1s7jq/cwUwXfSJmKzaCj+C+LJ958IM17FYxIz5xWJtZEzWsPAH7WVCP3b1m4MHU/UwGuMu/Gfdzusnr+Qtan6Wqn9AqUyJP/JfrAgMBAAE= -----END PUBLIC KEY----- -notifyUrl: https://test.example.com/huawei-pay/notify -returnUrl: https://test.example.com/huawei-pay/return +notifyUrl: https://dev.aigc-quickapp.com/pay/pay/notify.html +returnUrl: https://dev.aigc-quickapp.com/pay/pay/payreturn.html diff --git a/src/addon/huaweipay/tests/phpunit.xml.dist b/src/addon/huaweipay/tests/phpunit.xml.dist new file mode 100644 index 000000000..9837797bd --- /dev/null +++ b/src/addon/huaweipay/tests/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + ./ + + + + + ../ + + ../vendor/ + ../tests/ + + + + + + + + \ No newline at end of file