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

162 lines
8.6 KiB
PHP

<?php
declare(strict_types=1);
use app\common\service\ImRoamMessagePager;
use app\common\service\TencentImService;
use think\facade\Config;
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
// 不 initialize,不读取环境数据库;只有虚构配置,HTTP 始终由替身拦截。
$testApp = new think\App();
$testApp->instance('log', new Psr\Log\NullLogger());
Config::set(['trtc' => ['sdkAppId' => 123, 'secretKey' => 'im-pager-test-key']], 'project');
class RoamHttpFixture extends TencentImService
{
public array $responses = [];
public array $requests = [];
protected function httpPost(string $url, string $data, int $timeout = 10)
{
$this->requests[] = ['data' => json_decode($data, true), 'timeout' => $timeout];
if ($this->responses === []) {
throw new RuntimeException('Unexpected extra 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;
}
}
function roamExpect(bool $ok, string $message): void
{
if (!$ok) {
throw new RuntimeException($message);
}
}
function roamFails(callable $action, string $message, int $code = 0): void
{
try {
$action();
} catch (RuntimeException $exception) {
roamExpect(str_contains($exception->getMessage(), $message), 'Expected error: ' . $message . '; got: ' . $exception->getMessage());
roamExpect($exception->getCode() === $code, 'Cloud error code must survive pager failure');
return;
}
throw new RuntimeException('Expected page failure: ' . $message);
}
$message = static fn (string $key, int $time, bool $reverse = false): array => [
'From_Account' => $reverse ? 'patient_2' : 'doctor_1',
'To_Account' => $reverse ? 'doctor_1' : 'patient_2',
'MsgKey' => $key,
'MsgTimeStamp' => $time,
'MsgBody' => [['MsgType' => 'TIMTextElem', 'MsgContent' => ['Text' => $key]]],
];
$page = static fn (int $complete, ?int $time, ?string $key, array $messages): array => [
'ActionStatus' => 'OK', 'ErrorCode' => 0, 'Complete' => $complete,
'LastMsgTime' => $time, 'LastMsgKey' => $key, 'MsgCnt' => count($messages), 'MsgList' => $messages,
];
$service = new RoamHttpFixture();
$service->responses = [
$page(0, 200, 'a', [$message('a', 200)]),
$page(0, 200, 'b', [$message('b', 200, true)]),
$page(1, 199, 'c', [$message('c', 199)]),
];
$first = ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2');
roamExpect(!$first['completed'] && count($service->requests) === 1, 'One step pulls exactly one page');
$second = ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2', $first['cursor']);
roamExpect(!$second['completed'] && $second['cursor']['seen_keys'] === ['a', 'b'], 'Different keys allow multiple pages in the same second');
$third = ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2', $second['cursor']);
roamExpect($third['completed'] && $third['cursor']['seen_keys'] === ['c'], 'History of keys resets only when time moves into an earlier second');
roamExpect(array_column(array_merge($first['msgList'], $second['msgList'], $third['msgList']), 'MsgKey') === ['a', 'b', 'c'], 'All pages preserve raw messages in both directions');
foreach ($service->requests as $request) {
roamExpect($request['timeout'] === 15, 'Each page uses a bounded 15 second timeout');
roamExpect($request['data']['MinTime'] === 0, 'Initial full scan does not use the latest archived message as its lower bound');
roamExpect(!array_key_exists('LastMsgTime', $request['data']), 'LastMsgTime is never a request field');
}
roamExpect($service->requests[0]['data']['MaxTime'] === 4294967295 && !isset($service->requests[0]['data']['LastMsgKey']), 'Initial request has the full range and no last key');
roamExpect($service->requests[1]['data']['MaxTime'] === 200 && $service->requests[1]['data']['LastMsgKey'] === 'a', 'Second request uses response LastMsgTime as MaxTime');
roamExpect($service->requests[2]['data']['MaxTime'] === 200 && $service->requests[2]['data']['LastMsgKey'] === 'b', 'Same-second continuation advances by key');
$service->responses = [$page(1, null, null, [])];
$legacy = $service->adminGetRoamMsg('doctor_1', 'patient_2', 100, 0, 4294967295, 'old-key', 123);
$legacyRequest = $service->requests[count($service->requests) - 1]['data'];
roamExpect($legacy['success'] && $legacyRequest['MaxTime'] === 123 && !isset($legacyRequest['LastMsgTime']), 'Legacy seven argument calls also use the correct continuation field');
foreach ([
['ActionStatus' => 'FAIL', 'ErrorCode' => 91000, 'ErrorInfo' => 'Cloud retry later'],
['ActionStatus' => 'OK', 'ErrorCode' => 90009, 'ErrorInfo' => 'Cloud permission denied'],
] as $error) {
$service->responses = [$error];
$before = count($service->requests);
roamFails(static fn () => ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2'), $error['ErrorInfo'], $error['ErrorCode']);
roamExpect(count($service->requests) === $before + 1, 'Cloud errors are reported without an inline retry');
}
$service->responses = [['ActionStatus' => 'FAIL', 'ErrorCode' => 91000, 'ErrorInfo' => 'Cloud retry later']];
$failedPage = $service->adminGetRoamMsg('doctor_1', 'patient_2');
roamExpect(!$failedPage['success'] && $failedPage['complete'] === 0
&& $failedPage['error'] === 'Cloud retry later' && $failedPage['rawErrorCode'] === 91000,
'Legacy service errors retain the error and code without marking the conversation complete');
foreach ([
['not-json', 'IM响应解析失败'],
[false, 'IM接口无响应'],
[new RuntimeException('transport timeout'), 'transport timeout'],
[['ActionStatus' => 'OK', 'ErrorCode' => 0, 'MsgList' => []], 'Complete'],
[array_replace($page(1, null, null, []), ['MsgList' => 'invalid']), 'MsgList'],
[array_replace($page(1, null, null, []), ['MsgCnt' => 1]), 'MsgCnt'],
[array_replace($page(1, null, null, []), ['LastMsgTime' => '200']), 'LastMsgTime'],
[array_replace($page(1, null, null, []), ['LastMsgKey' => ['bad']]), 'LastMsgKey'],
[$page(0, null, null, []), 'LastMsgTime/LastMsgKey'],
[$page(0, 200, null, []), 'LastMsgTime/LastMsgKey'],
[$page(0, null, 'a', []), 'LastMsgTime/LastMsgKey'],
] as [$invalid, $error]) {
$service->responses = [$invalid];
roamFails(static fn () => ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2'), $error);
}
foreach ([0, 1] as $complete) {
$service->responses = [$page($complete, 200, 'a', [$message('a', 200)])];
roamFails(static fn () => ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2', $first['cursor']), '游标重复');
$service->responses = [$page($complete, 200, 'a', [$message('a', 200)])];
roamFails(static fn () => ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2', $second['cursor']), '游标重复');
}
$service->responses = [$page(0, 201, 'future', [])];
roamFails(static fn () => ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2', $first['cursor']), '时间超出');
$otherPatientMessage = $message('other-patient', 200);
$otherPatientMessage['To_Account'] = 'patient_999';
$service->responses = [$page(1, 200, 'other-patient', [$otherPatientMessage])];
roamFails(static fn () => ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2'), '不属于当前会话');
foreach ([
'invalid-message',
array_replace($message('bad', 200), ['MsgKey' => '']),
array_replace($message('bad', 200), ['MsgTimeStamp' => '200']),
array_replace($message('bad', 200), ['MsgBody' => null]),
] as $invalidMessage) {
$service->responses = [$page(1, 200, 'bad', [$invalidMessage])];
roamFails(static fn () => ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2'), '非法');
}
$before = count($service->requests);
foreach ([['max_time' => '200'], ['max_time' => -1], ['min_time' => 201, 'max_time' => 200], ['last_key' => []], ['seen_keys' => [null]]] as $invalidCursor) {
roamFails(static fn () => ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2', $invalidCursor), '游标无效');
}
roamFails(static fn () => ImRoamMessagePager::nextPage($service, '', 'patient_2'), '账号无效');
roamExpect(count($service->requests) === $before, 'Invalid requests are rejected before HTTP');
$service->responses = [$page(1, null, null, [])];
$empty = ImRoamMessagePager::nextPage($service, 'doctor_1', 'patient_2');
roamExpect($empty['completed'] && $empty['msgList'] === [], 'Explicit successful empty response can complete a conversation');
echo "IM_ROAM_MESSAGE_PAGER_TEST_OK\n";