Files
zyt/server/tests/TencentImAccountCheckTest.php
2026-09-09 15:47:48 +08:00

152 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
use app\common\service\TencentImService;
use think\facade\Config;
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
// Do not initialize the app or load environment/DB settings; HTTP is always replaced.
$testApp = new think\App();
$testApp->instance('log', new Psr\Log\NullLogger());
Config::set(['trtc' => ['sdkAppId' => 123, 'secretKey' => 'account-check-test-key']], 'project');
final class AccountCheckHttpFixture extends TencentImService
{
public array $responses = [];
public array $requests = [];
protected function httpPost(string $url, string $data, int $timeout = 10)
{
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
$this->requests[] = [
'path' => parse_url($url, PHP_URL_PATH), 'query' => $query,
'data' => json_decode($data, true, 512, JSON_THROW_ON_ERROR), 'timeout' => $timeout,
];
if ($this->responses === []) throw new RuntimeException('Unexpected additional HTTP request');
$response = array_shift($this->responses);
if ($response instanceof Throwable) throw $response;
return is_array($response) ? json_encode($response, JSON_THROW_ON_ERROR) : $response;
}
public function importAccount(string $userId, string $nick = '', string $faceUrl = '')
{
throw new RuntimeException('Read-only account checks must not import accounts');
}
public function batchImportAccounts(array $accounts): array
{
throw new RuntimeException('Read-only account checks must not import accounts');
}
}
function accountCheckExpect(bool $ok, string $message): void
{
if (!$ok) throw new RuntimeException($message);
}
function accountCheckFails(callable $action, string $expected, int $code = 0): void
{
try { $action(); }
catch (RuntimeException $exception) {
accountCheckExpect(str_contains($exception->getMessage(), $expected), 'Expected ' . $expected . '; got ' . $exception->getMessage());
accountCheckExpect($exception->getCode() === $code, 'Account-check errors must retain the original code');
return;
}
throw new RuntimeException('Expected account check to fail: ' . $expected);
}
$item = static fn (string $account, string $status): array => [
'UserID' => $account, 'ResultCode' => 0, 'ResultInfo' => '', 'AccountStatus' => $status,
];
$success = static fn (array $items): array => [
'ActionStatus' => 'OK', 'ErrorCode' => 0, 'ErrorInfo' => '', 'ResultItem' => $items,
];
$service = new AccountCheckHttpFixture();
accountCheckExpect($service->checkAccounts([]) === ['existing' => [], 'missing' => []] && $service->requests === [], 'Empty input makes no network request');
$service->responses = [$success([
$item('doctor_3', 'Imported'), $item('patient_501', 'NotImported'), $item('doctor_1', 'Imported'),
])];
$result = $service->checkAccounts(['doctor_1', 'patient_501', 'doctor_3', 'doctor_1']);
accountCheckExpect($result === ['existing' => ['doctor_1', 'doctor_3'], 'missing' => ['patient_501']], 'Only explicit NotImported is missing; results preserve requested order and deduplicate input');
accountCheckExpect(count($service->requests) === 1 && $service->requests[0]['timeout'] === 15, 'A check performs exactly one bounded request');
accountCheckExpect($service->requests[0]['data'] === ['CheckItem' => [
['UserID' => 'doctor_1'], ['UserID' => 'patient_501'], ['UserID' => 'doctor_3'],
]], 'Use the official CheckItem/UserID request shape');
accountCheckExpect($service->requests[0]['query']['sdkappid'] === '123'
&& $service->requests[0]['query']['identifier'] === 'administrator'
&& $service->requests[0]['query']['contenttype'] === 'json'
&& $service->requests[0]['query']['usersig'] !== '', 'Use configured IM app and administrator signature');
$hundred = array_map(static fn (int $id): string => 'doctor_' . $id, range(1, 100));
$service->responses = [$success(array_map(static fn (string $account): array => $item($account, 'Imported'), $hundred))];
$before = count($service->requests);
accountCheckExpect($service->checkAccounts($hundred) === ['existing' => $hundred, 'missing' => []]
&& count($service->requests) === $before + 1 && count($service->requests[$before]['data']['CheckItem']) === 100, 'One hundred accounts fit one official batch');
accountCheckFails(static fn () => $service->checkAccounts(array_merge($hundred, ['doctor_101'])), '最多支持100');
accountCheckExpect(count($service->requests) === $before + 1, 'Oversized batch is rejected rather than silently making several requests');
foreach ([[''], [' '], [null], [1], [false], [[]]] as $invalidAccounts) {
$before = count($service->requests);
accountCheckFails(static fn () => $service->checkAccounts($invalidAccounts), '非空账号字符串');
accountCheckExpect(count($service->requests) === $before, 'Invalid account input is rejected before HTTP');
}
foreach ([
['ActionStatus' => 'FAIL', 'ErrorCode' => 70403, 'ErrorInfo' => 'Administrator permission required'],
['ActionStatus' => 'OK', 'ErrorCode' => 70500, 'ErrorInfo' => 'Server internal error'],
] as $failure) {
$service->responses = [$failure];
$before = count($service->requests);
accountCheckFails(static fn () => $service->checkAccounts(['doctor_1']), $failure['ErrorInfo'], $failure['ErrorCode']);
accountCheckExpect(count($service->requests) === $before + 1, 'API failure is visible without inline retries');
}
$service->responses = [$success([
$item('doctor_1', 'Imported'),
['UserID' => 'doctor_2', 'ResultCode' => 70169, 'ResultInfo' => 'Per-account timeout', 'AccountStatus' => 'NotImported'],
])];
accountCheckFails(static fn () => $service->checkAccounts(['doctor_1', 'doctor_2']), 'Per-account timeout', 70169);
$service->responses = [$success([
['UserID' => 'doctor_1', 'ResultCode' => 70202, 'AccountStatus' => 'NotImported'],
])];
accountCheckFails(static fn () => $service->checkAccounts(['doctor_1']), 'ResultCode 70202', 70202);
foreach ([
[false, '无响应', 0],
['', '无响应', 0],
[new RuntimeException('transport timed out', 28), 'transport timed out', 28],
[new LogicException('transport refused', 7), 'transport refused', 7],
['not-json', '响应格式非法', 0],
['null', '响应格式非法', 0],
[['ActionStatus' => 'OK', 'ResultItem' => []], 'ErrorCode', 0],
[['ActionStatus' => 'OK', 'ErrorCode' => '0', 'ResultItem' => []], 'ErrorCode', 0],
[['ErrorCode' => 0, 'ResultItem' => []], '查询失败', 0],
[['ActionStatus' => 'OK', 'ErrorCode' => 0], 'ResultItem', 0],
[$success(['named' => $item('doctor_1', 'Imported')]), 'ResultItem', 0],
[$success([]), '不完整', 0],
[$success([null]), 'UserID', 0],
[$success([['ResultCode' => 0, 'AccountStatus' => 'Imported']]), 'UserID', 0],
[$success([$item('unrequested', 'Imported')]), '未请求或重复', 0],
[$success([$item('doctor_1', 'Imported'), $item('doctor_1', 'NotImported')]), '未请求或重复', 0],
[$success([['UserID' => 'doctor_1', 'CheckResult' => 0, 'AccountStatus' => 'Imported']]), 'ResultCode', 0],
[$success([['UserID' => 'doctor_1', 'ResultCode' => '0', 'AccountStatus' => 'Imported']]), 'ResultCode', 0],
[$success([['UserID' => 'doctor_1', 'ResultCode' => 0]]), 'AccountStatus', 0],
[$success([$item('doctor_1', 'Unknown')]), 'AccountStatus', 0],
] as [$failure, $error, $code]) {
$service->responses = [$failure];
accountCheckFails(static fn () => $service->checkAccounts(['doctor_1']), $error, $code);
}
$service->responses = [$success([$item('doctor_1', 'Imported')])];
accountCheckFails(static fn () => $service->checkAccounts(['doctor_1', 'doctor_2']), '不完整');
foreach ($service->requests as $request) {
accountCheckExpect($request['path'] === '/v4/im_open_login_svc/account_check', 'All requests use the read-only account_check endpoint, never account_import');
accountCheckExpect($request['timeout'] === 15, 'Every account check has a 15 second timeout');
}
echo "TENCENT_IM_ACCOUNT_CHECK_TEST_OK\n";