Files
zyt/server/tests/QywxPromotionContactApiServiceTest.php
2026-08-31 15:17:34 +08:00

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";