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); } }