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

110 lines
7.2 KiB
PHP

<?php
declare(strict_types=1);
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;
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
// 不initialize,不加载业务数据库;每个HTTP请求都由MockHandler处理。
new think\App();
function createTagCheck(bool $ok, string $message): void { if (!$ok) { throw new RuntimeException($message); } }
$json = static fn (array $data): Response => new Response(200, [], json_encode($data));
$group = static fn (array $tags = [], string $id = 'promotion_group', string $name = '推广渠道'): array => [
'group_id' => $id, 'group_name' => $name, 'tag' => $tags,
];
$listing = static fn (array $groups): Response => $json(['errcode' => 0, 'tag_group' => $groups]);
$created = static fn (string $id, string $name, string $groupId = 'promotion_group'): Response => $json([
'errcode' => 0, 'tag_group' => $group([['id' => $id, 'name' => $name]], $groupId),
]);
$fixture = static function (array $responses, array &$history): QywxPromotionContactApiService {
$history = [];
$stack = HandlerStack::create(new MockHandler($responses));
$stack->push(Middleware::history($history));
return new QywxPromotionContactApiService(new Client([
'base_uri' => 'https://qyapi.weixin.qq.com/', 'handler' => $stack, 'http_errors' => false,
]), static fn (): string => 'mock_token');
};
$addCount = static fn (array $history): int => count(array_filter($history,
static fn (array $entry): bool => $entry['request']->getUri()->getPath() === '/cgi-bin/externalcontact/add_corp_tag'));
$history = [];
$name30 = str_repeat('渠', 30);
$api = $fixture([$listing([]), $created('new_tag', $name30)], $history);
$result = $api->createTag(' ' . $name30 . ' ');
createTagCheck($result === ['tag' => ['id' => 'new_tag', 'name' => $name30], 'group_id' => 'promotion_group', 'group_name' => '推广渠道', 'reused' => false], 'new tag response and 30 Unicode characters');
$body = json_decode((string) $history[1]['request']->getBody(), true);
createTagCheck($body === ['tag' => [['name' => $name30]], 'group_name' => '推广渠道'], 'new group uses group_name plus nonempty tag array');
createTagCheck($history[1]['request']->getMethod() === 'POST' && $addCount($history) === 1, 'official add endpoint called once');
$api = $fixture([$listing([$group([['id' => 'old', 'name' => '旧标签']])]), $created('tag2', '直播')], $history);
$result = $api->createTag('直播');
$body = json_decode((string) $history[1]['request']->getBody(), true);
createTagCheck($body === ['tag' => [['name' => '直播']], 'group_id' => 'promotion_group'], 'existing group uses ID and does not rename/reorder it');
createTagCheck($result['reused'] === false && $result['tag']['id'] === 'tag2', 'existing-group creation returns actual ID');
$api = $fixture([$listing([
$group([['id' => 'other_group_tag', 'name' => '直播']], 'other_group', '其他分组'),
$group([['id' => 'existing_tag', 'name' => '直播']]),
])], $history);
$result = $api->createTag('直播');
createTagCheck($result['reused'] && $result['tag']['id'] === 'existing_tag' && count($history) === 1 && $addCount($history) === 0, 'reuse only exact name in fixed group without write');
$api = $fixture([$listing([$group([['id' => 'other', 'name' => '直播']], 'other_group', '其他分组')]), $created('fixed_group_tag', '直播')], $history);
createTagCheck($api->createTag('直播')['tag']['id'] === 'fixed_group_tag' && $addCount($history) === 1, 'same name in different group is not reused');
$api = $fixture([
$listing([$group()]),
$json(['errcode' => 40058, 'errmsg' => 'mock concurrent creation conflict']),
$listing([$group([['id' => 'concurrent_tag', 'name' => '直播']])]),
], $history);
$result = $api->createTag('直播');
createTagCheck($result['reused'] && $result['tag']['id'] === 'concurrent_tag' && $addCount($history) === 1 && count($history) === 3, 'concurrent conflict resolved by one read-back');
$networkError = static fn (): ConnectException => new ConnectException('secret=do_not_expose', new Request('POST', 'https://example.invalid/?access_token=secret'));
$api = $fixture([$listing([]), $networkError(), $listing([$group([['id' => 'committed_tag', 'name' => '直播']])])], $history);
$result = $api->createTag('直播');
createTagCheck($result['reused'] && $result['tag']['id'] === 'committed_tag' && $addCount($history) === 1, 'uncertain transport uses read-back, never duplicate creation');
$api = $fixture([$listing([]), $networkError(), $listing([])], $history);
$error = null;
try { $api->createTag('直播'); } catch (RuntimeException $e) { $error = $e; }
createTagCheck($error !== null && str_contains($error->getMessage(), '无法确认') && str_contains($error->getMessage(), '勿重复提交'), 'unknown result must not invent an ID or claim success');
createTagCheck($addCount($history) === 1 && count($history) === 3 && !str_contains($error->getMessage(), 'secret') && $error->getPrevious() === null, 'uncertain result no resend/no secret chain');
$api = $fixture([$listing([]), $json(['errcode' => 48002, 'errmsg' => 'secret']), $listing([])], $history);
$error = null;
try { $api->createTag('直播'); } catch (RuntimeException $e) { $error = $e; }
createTagCheck($error !== null && $error->getCode() === 48002 && $addCount($history) === 1, 'explicit failure retained after read-back absent');
$api = $fixture([$listing([]), $json(['errcode' => 0, 'tag_group' => $group([['name' => '直播']])]), $listing([$group([['id' => 'verified_tag', 'name' => '直播']])])], $history);
createTagCheck($api->createTag('直播')['tag']['id'] === 'verified_tag' && $addCount($history) === 1, 'success without ID still requires authoritative read-back');
$api = $fixture([$listing([$group([['id' => 'deleted_tag', 'name' => '直播', 'deleted' => true]])]), $created('recreated', '直播')], $history);
createTagCheck($api->createTag('直播')['tag']['id'] === 'recreated' && $addCount($history) === 1, 'deleted tag is not reused');
$api = $fixture([$listing([]), $networkError(), new Response(503, [], '')], $history);
$error = null;
try { $api->createTag('直播'); } catch (RuntimeException $e) { $error = $e; }
createTagCheck($error !== null && str_contains($error->getMessage(), '无法确认') && $addCount($history) === 1, 'failed read-back retains uncertainty without write retry');
$api = $fixture([$json(['errcode' => 48002])], $history);
$error = null;
try { $api->createTag('直播'); } catch (RuntimeException $e) { $error = $e; }
createTagCheck($error !== null && count($history) === 1 && $addCount($history) === 0, 'initial read failure cannot proceed to creation');
foreach (['', ' ', "\u{3000}", str_repeat('字', 31), str_repeat('😀', 31), "标签\n", "标\t签", "\0标签", "\u{200B}标签", "\xFF"] as $invalid) {
$api = $fixture([], $history);
$failed = false;
try { $api->createTag($invalid); } catch (RuntimeException) { $failed = true; }
createTagCheck($failed && $history === [], 'invalid name rejected before HTTP');
}
echo "QYWX_PROMOTION_CREATE_TAG_OK\n";