Skip to content

Commit

Permalink
optim the pay client and server on APIv2 specials, ref efedd21 (#2888)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheNorthMemory authored Feb 13, 2025
1 parent b49c484 commit 42cb7ab
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 7 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ name: Deploy

# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the 6.x branch
# Triggers the workflow on push event but only for the 6.x branch(required the secrets environment)
push:
branches: [ 6.x ]
pull_request:
branches: [ 6.x ]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
Expand Down
10 changes: 7 additions & 3 deletions src/Pay/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

use function array_key_exists;
use function is_array;
use function is_string;
use function ltrim;
use function str_starts_with;
use function strcasecmp;

Expand Down Expand Up @@ -164,9 +166,11 @@ public function request(string $method, string $url, array $options = []): Respo
failureJudge: $this->isV3Request($url) ? null : function (Response $response) use ($url): bool {
$arr = $response->toArray();

if ($url === self::V2_URI_OVER_GETS[0]) {
return ! (array_key_exists('retcode', $arr) && $arr['retcode'] === 0);
}

return ! (
$url === self::V2_URI_OVER_GETS[0] && array_key_exists('retcode', $arr) && $arr['retcode'] === 0
) || ! (
// protocol code, most similar to the HTTP status code in APIv3
array_key_exists('return_code', $arr) && $arr['return_code'] === 'SUCCESS'
) || (
Expand All @@ -180,7 +184,7 @@ public function request(string $method, string $url, array $options = []): Respo

protected function isV3Request(string $url): bool
{
$uri = (new Uri($url))->getPath();
$uri = '/'.ltrim((new Uri($url))->getPath(), '/');

foreach (self::V3_URI_PREFIXES as $prefix) {
if (str_starts_with($uri, $prefix)) {
Expand Down
16 changes: 16 additions & 0 deletions src/Pay/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

use function array_key_exists;
use function is_array;
use function is_string;
use function json_decode;
use function json_encode;
use function str_contains;
Expand Down Expand Up @@ -141,6 +143,20 @@ protected function decodeXmlMessage(string $contents): array
$attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));
}

if (
is_array($attributes)
&& array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])
&& array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])
&& array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])
) {
$attributes += Xml::parse(AesGcm::decrypt(
$attributes['event_ciphertext'],
$this->merchant->getSecretKey(),
$attributes['event_nonce'],
$attributes['event_associated_data'] // maybe empty string
));
}

if (! is_array($attributes)) {
throw new RuntimeException('Failed to decrypt request message.');
}
Expand Down
17 changes: 17 additions & 0 deletions tests/Pay/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ public function test_v2_request_with_xml_string_as_body()
$this->assertSame(Xml::build(['foo' => 'bar']), $client->getRequestOptions()['body']);
}

public function test_v2_request_appauth_getaccesstoken()
{
$client = Client::mock('{"retcode":-1,"access_token":"mock-token"}', 200, ['Content-Type' => 'application/json']);
$client->shouldReceive('createSignature')->never();
$client->shouldReceive('isV3Request')->andReturn(false);
$client->shouldReceive('attachLegacySignature')->with([
'foo' => 'bar',
])->andReturn(['foo' => 'bar', 'sign' => 'mock-signature']);

$response = $client->get('/appauth/getaccesstoken', ['query' => ['foo' => 'bar']]);

$this->assertSame('GET', $client->getRequestMethod());
$this->assertEquals(['foo' => 'bar', 'sign' => 'mock-signature'], $client->getRequestOptions()['query']);
$this->assertSame('https://api.mch.weixin.qq.com/appauth/getaccesstoken?foo=bar&sign=mock-signature', $client->getRequestUrl());
$this->assertContains('Content-Type: text/xml', $client->getRequestOptions()['headers']);
}

public function test_v3_upload_media()
{
$client = Client::mock();
Expand Down
119 changes: 118 additions & 1 deletion tests/Pay/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,22 @@

namespace EasyWeChat\Tests\Pay;

use EasyWeChat\Kernel\Support\AesEcb;
use EasyWeChat\Kernel\Support\AesGcm;
use EasyWeChat\Kernel\Support\Xml;
use EasyWeChat\Pay\Contracts\Merchant;
use EasyWeChat\Pay\Message;
use EasyWeChat\Pay\Server;
use EasyWeChat\Tests\TestCase;
use Mockery\LegacyMockInterface;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;

use function bin2hex;
use function fopen;
use function md5;
use function random_bytes;

class ServerTest extends TestCase
{
Expand All @@ -16,10 +28,13 @@ public function test_it_will_handle_validation_request()
$request = (new ServerRequest(
'POST',
'http://easywechat.com/',
[],
[
'Content-Type' => 'application/json',
],
fopen(__DIR__.'/../fixtures/files/pay_demo.json', 'r')
));

/** @var Merchant&LegacyMockInterface $merchant */
$merchant = \Mockery::mock(Merchant::class);
$merchant->shouldReceive('getSecretKey')->andReturn('key');

Expand All @@ -28,4 +43,106 @@ public function test_it_will_handle_validation_request()
$response = $server->serve();
$this->assertSame('{"code":"SUCCESS","message":"成功"}', \strval($response->getBody()));
}

public function test_legacy_encryped_by_aesecb_refund_request()
{
/** @var Merchant&LegacyMockInterface $merchant */
$merchant = \Mockery::mock(Merchant::class);
$merchant->shouldReceive(['getV2SecretKey' => random_bytes(32)]);
$symmtricKey = $merchant->getV2SecretKey();

$server = new Server($merchant, new ServerRequest(
'POST',
'http://easywechat.com/sample-webhook-handler',
[
'Content-Type' => 'text/xml',
],
Xml::build([
'return_code' => 'SUCCESS',
'req_info' => AesEcb::encrypt(Xml::build([
'refund_id' => '50000408942018111907145868882',
'transaction_id' => '4200000215201811190261405420',
]), md5($symmtricKey), ''),
])
));

$response = $server->with(function (Message $message): ResponseInterface {
$source = $message->getOriginalContents();
$parsed = $message->toArray();

$this->assertStringContainsString('<xml>', $source);
$this->assertStringContainsString('<req_info>', $source);
$this->assertStringNotContainsString('<refund_id>', $source);
$this->assertStringNotContainsString('<transaction_id>', $source);
$this->assertArrayNotHasKey('return_code', $parsed);
$this->assertArrayNotHasKey('req_info', $parsed);
$this->assertArrayHasKey('refund_id', $parsed);
$this->assertArrayHasKey('transaction_id', $parsed);

return new Response(
200,
['Content-Type' => 'text/xml'],
'<xml><return_code>SUCCESS</return_code></xml>'
);
})->serve();

$this->assertEquals(200, $response->getStatusCode());
$this->assertSame('<xml><return_code>SUCCESS</return_code></xml>', \strval($response->getBody()));
}

public function test_legacy_encryped_by_aesgcm_notification_request()
{
/** @var Merchant&LegacyMockInterface $merchant */
$merchant = \Mockery::mock(Merchant::class);
$merchant->shouldReceive(['getSecretKey' => random_bytes(32)]);
$symmtricKey = $merchant->getSecretKey();

$server = new Server($merchant, new ServerRequest(
'POST',
'http://easywechat.com/sample-webhook-handler',
[
'Content-Type' => 'text/xml',
],
Xml::build([
'event_type' => 'TRANSACTION.SUCCESS',
'event_algorithm' => 'AEAD_AES_256_GCM',
'event_nonce' => $nonce = bin2hex(random_bytes(6)),
'event_associated_data' => $aad = '',
'event_ciphertext' => AesGcm::encrypt(Xml::build([
'state' => 'USER_PAID',
'service_id' => '1234352342',
]), $symmtricKey, iv: $nonce, aad: $aad),
])
));

$response = $server->with(function (Message $message): ResponseInterface {
$source = $message->getOriginalContents();
$parsed = $message->toArray();

$this->assertStringContainsString('<xml>', $source);
$this->assertStringContainsString('<event_type>', $source);
$this->assertStringContainsString('<event_algorithm>', $source);
$this->assertStringContainsString('<event_nonce>', $source);
$this->assertStringContainsString('<event_associated_data>', $source);
$this->assertStringContainsString('<event_ciphertext>', $source);
$this->assertStringNotContainsString('<state>', $source);
$this->assertStringNotContainsString('<service_id>', $source);
$this->assertArrayHasKey('event_type', $parsed);
$this->assertArrayHasKey('event_algorithm', $parsed);
$this->assertArrayHasKey('event_nonce', $parsed);
$this->assertArrayHasKey('event_associated_data', $parsed);
$this->assertArrayHasKey('event_ciphertext', $parsed);
$this->assertArrayHasKey('state', $parsed);
$this->assertArrayHasKey('service_id', $parsed);

return new Response(
500,
['Content-Type' => 'text/xml'],
'<xml><code>ERROR_NAME</code><message>ERROR_DESCRIPTION</message></xml>'
);
})->serve();

$this->assertEquals(500, $response->getStatusCode());
$this->assertSame('<xml><code>ERROR_NAME</code><message>ERROR_DESCRIPTION</message></xml>', \strval($response->getBody()));
}
}

0 comments on commit 42cb7ab

Please sign in to comment.