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