dpl/tests/Unit/Payments/EpayCallbackTest.php

96 lines
4.4 KiB
PHP

<?php
namespace Tests\Unit\Payments;
use App\Models\Order;
use PHPUnit\Framework\TestCase;
use Plugin\Epay\Plugin;
require_once __DIR__ . '/../../../plugins-core/Epay/Plugin.php';
class EpayCallbackTest extends TestCase
{
private function plugin(): Plugin
{
$plugin = new class('epay') extends Plugin {
public int $orderStatus = Order::STATUS_PENDING;
public ?string $callbackNo = null;
protected function findOrder(string $tradeNo): ?Order
{
if ($tradeNo !== 'test-order') return null;
$order = new Order();
$order->setRawAttributes(['payment_id' => 7, 'total_amount' => 1000, 'handling_amount' => 50, 'status' => $this->orderStatus, 'callback_no' => $this->callbackNo]);
return $order;
}
};
$plugin->setConfig(['id' => 7, 'pid' => 'test-merchant', 'key' => 'test-secret', 'type' => 'alipay', 'url' => 'https://example.test/']);
return $plugin;
}
private function signed(array $changes = []): array
{
$params = array_replace(['pid' => 'test-merchant', 'type' => 'alipay', 'out_trade_no' => 'test-order', 'trade_no' => 'gateway-order', 'money' => '10.50', 'trade_status' => 'TRADE_SUCCESS', 'param' => ''], $changes);
unset($params['sign'], $params['sign_type']);
ksort($params, SORT_STRING);
$parts = [];
foreach ($params as $k => $v) if ($v !== '' && $v !== null) $parts[] = $k . '=' . $v;
$params['sign'] = md5(implode('&', $parts) . 'test-secret');
$params['sign_type'] = 'MD5';
return $params;
}
public function test_accepts_successful_matching_order_including_handling_fee_and_empty_extension(): void
{
$this->assertSame(['trade_no' => 'test-order', 'callback_no' => 'gateway-order'], $this->plugin()->notify($this->signed()));
$this->assertIsArray($this->plugin()->notify($this->signed(['money' => '10.5'])));
}
public function test_signed_but_invalid_callbacks_cannot_mark_order_paid(): void
{
foreach ([['trade_status' => 'WAIT_BUYER_PAY'], ['money' => '10.00'], ['money' => '10.501'], ['money' => '1.05e1'], ['money' => '-10.50'], ['pid' => 'other'], ['type' => 'wxpay'], ['out_trade_no' => 'missing'], ['trade_no' => '']] as $change) {
$this->assertFalse($this->plugin()->notify($this->signed($change)), json_encode($change));
}
$plugin = $this->plugin();
$plugin->setConfig(['id' => 8, 'pid' => 'test-merchant', 'key' => 'test-secret', 'type' => 'alipay']);
$this->assertFalse($plugin->notify($this->signed()));
}
public function test_repeated_success_is_accepted_but_cancelled_or_conflicting_receipt_is_rejected(): void
{
$plugin = $this->plugin();
$plugin->orderStatus = Order::STATUS_COMPLETED;
$plugin->callbackNo = 'gateway-order';
$this->assertIsArray($plugin->notify($this->signed()));
$plugin->callbackNo = 'different-receipt';
$this->assertFalse($plugin->notify($this->signed()));
$plugin->callbackNo = null;
$plugin->orderStatus = Order::STATUS_CANCELLED;
$this->assertFalse($plugin->notify($this->signed()));
}
public function test_rejects_bad_missing_and_structured_signature_data(): void
{
$params = $this->signed();
$params['sign'] = str_repeat('0', 32);
$this->assertFalse($this->plugin()->notify($params));
unset($params['sign']);
$this->assertFalse($this->plugin()->notify($params));
$params = $this->signed();
$params['money'] = ['10.50'];
$this->assertFalse($this->plugin()->notify($params));
}
public function test_pay_signs_unescaped_values_and_normalizes_gateway_slash(): void
{
$result = $this->plugin()->pay(['total_amount' => 1050, 'trade_no' => 'test-order', 'notify_url' => 'https://example.test/notify?x=1&y=2', 'return_url' => 'https://example.test/#/order/test-order']);
$this->assertStringStartsWith('https://example.test/submit.php?', $result['data']);
parse_str(parse_url($result['data'], PHP_URL_QUERY), $params);
$sign = $params['sign'];
unset($params['sign'], $params['sign_type']);
ksort($params, SORT_STRING);
$parts = [];
foreach ($params as $k => $v) $parts[] = $k . '=' . $v;
$this->assertSame(md5(implode('&', $parts) . 'test-secret'), $sign);
}
}