94 lines
6.1 KiB
PHP
94 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use app\common\service\qywx\QywxPromotionContactApiException;
|
|
use app\common\service\qywx\QywxPromotionContactApiService;
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Exception\ConnectException;
|
|
use GuzzleHttp\Handler\MockHandler;
|
|
use GuzzleHttp\HandlerStack;
|
|
use GuzzleHttp\Middleware;
|
|
use GuzzleHttp\Psr7\Request;
|
|
use GuzzleHttp\Psr7\Response;
|
|
use think\facade\Config;
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
|
|
// 不initialize,不加载环境数据库;所有HTTP必须经过MockHandler。
|
|
new think\App();
|
|
Config::set(['corp_id' => 'ww_fake', 'secret' => 'acquisition_application_secret'], 'qywx_customer_acquisition');
|
|
Config::set(['wechat_work' => ['external_pay_secret' => 'never_use_payment_secret']], 'pay');
|
|
function apiCheck(bool $ok, string $message): void { if (!$ok) { throw new RuntimeException($message); } }
|
|
$json = static fn (array $value): Response => new Response(200, [], json_encode($value));
|
|
$history = [];
|
|
$stack = HandlerStack::create(new MockHandler([
|
|
$json(['errcode' => 0, 'tag_group' => [
|
|
['group_id' => 'group', 'group_name' => '来源', 'tag' => [['id' => 'tag', 'name' => '推广'], ['id' => 'deleted', 'deleted' => true]]],
|
|
['group_id' => 'removed_group', 'deleted' => true],
|
|
]]),
|
|
$json(['errcode' => 0, 'external_contact' => ['name' => '小李'], 'next_cursor' => 'next']),
|
|
$json(['errcode' => 0, 'name' => '张医助']),
|
|
$json(['errcode' => 0]), $json(['errcode' => 0]), $json(['errcode' => 0]),
|
|
$json(['media_id' => 'media_1', 'created_at' => time(), 'type' => 'image']),
|
|
$json(['errcode' => 40014, 'errmsg' => 'sensitive mock token']),
|
|
$json(['errcode' => 0]),
|
|
]));
|
|
$stack->push(Middleware::history($history));
|
|
$calls = 0;
|
|
$api = new QywxPromotionContactApiService(new Client(['base_uri' => 'https://qyapi.weixin.qq.com/', 'handler' => $stack]),
|
|
static function () use (&$calls): string { return 'mock_token_' . ++$calls; });
|
|
apiCheck($api->credentialFingerprint() === hash('sha256', 'ww_fake|acquisition_application_secret'), 'must use acquisition app, not payment secret');
|
|
apiCheck($api->tagOptions() === ['tag_groups' => [['group_id' => 'group', 'group_name' => '来源', 'tag' => [['id' => 'tag', 'name' => '推广']]]]], 'tag option mapping/deleted filter');
|
|
apiCheck($api->getExternalContact('external', 'cursor')['next_cursor'] === 'next', 'customer detail');
|
|
apiCheck($api->getUser('staff')['name'] === '张医助', 'staff name');
|
|
$api->markTags('staff', 'external', ['tag', 'tag']);
|
|
$api->remark('staff', 'external', ['remark' => '备注', 'remark_mobiles' => ['do-not-send']]);
|
|
$api->sendWelcome('one_time_code', '您好', [['msgtype' => 'image', 'image' => ['media_id' => 'media']]]);
|
|
$stream = fopen('php://temp', 'w+'); fwrite($stream, 'mock_image_bytes'); rewind($stream);
|
|
apiCheck($api->uploadMedia($stream, 'image', 'cover.png')['media_id'] === 'media_1', 'upload media response');
|
|
$api->sendWelcome('other_code', 'hello', []);
|
|
$paths = array_map(static fn (array $h): string => $h['request']->getUri()->getPath(), $history);
|
|
apiCheck($paths === [
|
|
'/cgi-bin/externalcontact/get_corp_tag_list', '/cgi-bin/externalcontact/get', '/cgi-bin/user/get',
|
|
'/cgi-bin/externalcontact/mark_tag', '/cgi-bin/externalcontact/remark', '/cgi-bin/externalcontact/send_welcome_msg',
|
|
'/cgi-bin/media/upload', '/cgi-bin/externalcontact/send_welcome_msg', '/cgi-bin/externalcontact/send_welcome_msg',
|
|
], 'official endpoints');
|
|
apiCheck((string) $history[0]['request']->getBody() === '{}', 'empty tag request must be JSON object');
|
|
parse_str($history[1]['request']->getUri()->getQuery(), $query);
|
|
apiCheck($query['external_userid'] === 'external' && $query['cursor'] === 'cursor', 'GET query fields');
|
|
$body = json_decode((string) $history[3]['request']->getBody(), true);
|
|
apiCheck($body['add_tag'] === ['tag'] && !isset($body['remove_tag']), 'only add configured tags');
|
|
$body = json_decode((string) $history[4]['request']->getBody(), true);
|
|
apiCheck($body === ['userid' => 'staff', 'external_userid' => 'external', 'remark' => '备注'], 'only enabled remark fields');
|
|
$body = (string) $history[6]['request']->getBody();
|
|
apiCheck(str_contains($body, 'name="media"') && str_contains($body, 'filename="cover.png"'), 'multipart file field');
|
|
fclose($stream);
|
|
apiCheck($calls === 9, 'explicit token invalid response refreshes once');
|
|
|
|
$uploadRetry = new QywxPromotionContactApiService(new Client([
|
|
'base_uri' => 'https://qyapi.weixin.qq.com/', 'handler' => HandlerStack::create(new MockHandler([
|
|
$json(['errcode' => 40014]), $json(['media_id' => 'retry_media', 'created_at' => time()]),
|
|
])),
|
|
]), static fn (): string => 'mock');
|
|
$retryStream = fopen('php://temp', 'w+'); fwrite($retryStream, 'mock_image_bytes'); rewind($retryStream);
|
|
apiCheck($uploadRetry->uploadMedia($retryStream, 'image', 'retry.png')['media_id'] === 'retry_media', 'multipart stream survives explicit token refresh');
|
|
if (is_resource($retryStream)) { fclose($retryStream); }
|
|
|
|
foreach ([
|
|
[new ConnectException('secret=should_never_escape', new Request('POST', 'https://mock/?access_token=secret')), true, 0],
|
|
[new Response(502, [], 'secret'), true, 0],
|
|
[$json(['errcode' => 41051, 'errmsg' => 'welcome_code=secret']), false, 41051],
|
|
[$json(['errcode' => 41096, 'errmsg' => 'secret']), false, 41096],
|
|
] as [$response, $uncertain, $code]) {
|
|
$mock = new MockHandler([$response]);
|
|
$service = new QywxPromotionContactApiService(new Client(['base_uri' => 'https://qyapi.weixin.qq.com/', 'handler' => HandlerStack::create($mock), 'http_errors' => false]), static fn (): string => 'mock');
|
|
try { $service->sendWelcome('code', 'hello', []); throw new RuntimeException('expected failure'); }
|
|
catch (QywxPromotionContactApiException $e) {
|
|
apiCheck($e->uncertain === $uncertain && $e->getCode() === $code, 'uncertain result classification');
|
|
apiCheck(!str_contains($e->getMessage(), 'secret') && $e->getPrevious() === null, 'do not leak secrets in exception chain');
|
|
apiCheck(count($mock) === 0, 'network error is not retried');
|
|
}
|
|
}
|
|
echo "QYWX_PROMOTION_CONTACT_API_OK\n";
|