build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
362 lines
20 KiB
JavaScript
362 lines
20 KiB
JavaScript
// IM **网关准入面**回归测试(node:test)—— IM 线 · 第 16 棒。
|
||
//
|
||
// 跑的是**构建产物** `lib/`(`npm test` 先 build)+ **真客户端件** `poc/im-conversation-tabs/lib/client.js`。
|
||
// 判据编号写在用例名里 ⇒ 「哪条验收对哪个用例」一眼可查,第三方可复现。
|
||
//
|
||
// ## 覆盖策略
|
||
// ① **能力口径修正判据**:`providesServerHeartbeat` 与实测一致(网关**会**发协议级 ping);
|
||
// 连带复核 `assertCapabilities` —— 含**一条反例**(构造即抛仍可复现)。
|
||
// ② **票据判据**(平台侧纯函数):HS256 结构 / 签验往返 / 过期 / 错密钥 / 无 `sub` / 频道不符;
|
||
// ⛔ 密钥与票据都不进任何读数(返回值里**没有**密钥字段)。
|
||
// ③ **频道名反解判据**:严格(前缀不符 / 非 `imr_` / 含 `:` / 空 ⇒ 一律 `undefined`),
|
||
// 且 `channelFor` ↔ `roomIdFromChannel` 往返恒等。
|
||
// ④ **客户端件判据**(🔴 直接驱动**真客户端件**):空对象即回、回的是**空命令**、
|
||
// 非空对象**不回**;`push.pub.data.frame` 提取后与发出去的原帧**逐字相同**。
|
||
// ⑤ **源码级结构性判据**:fail-closed 分支齐备、密钥从不外泄、两处房间前缀字面一致、
|
||
// 客户端不含 `{op:'pong'}` 这类未认 op。
|
||
//
|
||
// ⚠️ 本文件**不碰网络**、**不碰 DB**(不 import fastify 路由:那需要真 DB)。
|
||
|
||
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import { join } from 'node:path'
|
||
import { GATEWAY_CAPABILITIES } from '../lib/im/backends/gateway.js'
|
||
import {
|
||
GATEWAY_ROOM_ID_PREFIX,
|
||
IM_GATEWAY_SUBSCRIBE_CALLBACK_PATH,
|
||
channelFor,
|
||
resolveChannelPrefix,
|
||
roomIdFromChannel,
|
||
} from '../lib/im/backends/gateway.js'
|
||
import { NATIVE_CAPABILITIES } from '../lib/im/backends/native.js'
|
||
import { assertCapabilities } from '../lib/im/connection-backend.js'
|
||
import {
|
||
IM_GATEWAY_CONNECT_TTL_SEC,
|
||
IM_GATEWAY_SUBSCRIBE_TTL_SEC,
|
||
base64Url,
|
||
buildConnectClaims,
|
||
buildSubscriptionClaims,
|
||
issueConnectToken,
|
||
issueSubscriptionToken,
|
||
signHmacJwt,
|
||
verifyHmacJwt,
|
||
} from '../lib/im/gateway-token.js'
|
||
|
||
const ROOT = process.cwd()
|
||
const readSrc = (rel) => readFileSync(join(ROOT, rel), 'utf8')
|
||
|
||
const SECRET = 'test-secret-not-a-real-key'
|
||
const NOW = 1_700_000_000
|
||
|
||
// ── 判据 ① · 能力口径修正(连带复核 assertCapabilities)──────────────────────
|
||
|
||
test('G-1 · capability 口径:外部连接层**会**发协议级 ping ⇒ providesServerHeartbeat=true(依据 §15.11 三向实测)', () => {
|
||
assert.equal(GATEWAY_CAPABILITIES.providesServerHeartbeat, true)
|
||
// ⛔ 旧的 false 出自对 `wsPingRecv=0` 的误读(那条读数数的是 WS 控制帧)——
|
||
// 本断言把修正后的值钉住,防止有人照旧注释又改回去。
|
||
assert.notEqual(GATEWAY_CAPABILITIES.providesServerHeartbeat, false)
|
||
// 垫片仍需要(平台侧拨不到外部层的连接 ⇒ 保活落点在客户端)。
|
||
assert.equal(GATEWAY_CAPABILITIES.needsKeepaliveShim, true)
|
||
assert.equal(GATEWAY_CAPABILITIES.terminatesClients, false)
|
||
assert.equal(GATEWAY_CAPABILITIES.fanoutOffsite, true)
|
||
assert.equal(GATEWAY_CAPABILITIES.presenceSource, 'backend')
|
||
})
|
||
|
||
test('G-2 · 构造即抛仍可复现(反例)+ 新增的「扇出外移必须声明垫片」判据', () => {
|
||
// 反例 ①:不发心跳、又不声明垫片 ⇒ 抛(E-2「26 s 全断」的约束形态)
|
||
assert.throws(
|
||
() => assertCapabilities('native', { ...NATIVE_CAPABILITIES, providesServerHeartbeat: false, needsKeepaliveShim: false }),
|
||
/backend-without-heartbeat-needs-shim/,
|
||
)
|
||
// 反例 ②(第 16 棒新增):扇出外移却声明"不需要垫片" ⇒ 抛。
|
||
// 🔴 为什么必须新增:`providesServerHeartbeat` 改 true 之后,反例 ① 的网关分支
|
||
// 不再生效(true + 任意 shim 值都过)⇒ 不补这一条,规则就只是摆设。
|
||
assert.throws(
|
||
() => assertCapabilities('gateway', { ...GATEWAY_CAPABILITIES, needsKeepaliveShim: false }),
|
||
/backend-fanout-offsite-needs-shim/,
|
||
)
|
||
// 反例 ③:扇出外移又自称终结客户端 ⇒ 抛
|
||
assert.throws(
|
||
() => assertCapabilities('gateway', { ...GATEWAY_CAPABILITIES, terminatesClients: true }),
|
||
/backend-fanout-offsite-cannot-terminate-clients/,
|
||
)
|
||
// 正向对照:两组合规自述都不抛
|
||
assert.doesNotThrow(() => assertCapabilities('gateway', GATEWAY_CAPABILITIES))
|
||
assert.doesNotThrow(() => assertCapabilities('native', NATIVE_CAPABILITIES))
|
||
})
|
||
|
||
// ── 判据 ② · 票据(纯函数;⛔ 不碰密钥以外的东西)────────────────────────────
|
||
|
||
test('G-3 · 票据结构:三段 base64url + 头部写死 HS256(⛔ alg 不由调用方给)', () => {
|
||
const token = signHmacJwt({ sub: 'u_1', exp: NOW + 60 }, SECRET)
|
||
const parts = token.split('.')
|
||
assert.equal(parts.length, 3)
|
||
for (const p of parts) assert.match(p, /^[A-Za-z0-9_-]+$/)
|
||
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8'))
|
||
assert.deepEqual(header, { alg: 'HS256', typ: 'JWT' })
|
||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'))
|
||
assert.deepEqual(payload, { sub: 'u_1', exp: NOW + 60 })
|
||
// 空密钥 ⇒ 抛(⛔ 不签出"没签名"的票据)
|
||
assert.throws(() => signHmacJwt({ sub: 'u_1' }, ''), /gateway-token-secret-empty/)
|
||
assert.equal(base64Url(Buffer.from([0xfb, 0xff])), '-_8')
|
||
})
|
||
|
||
test('G-4 · 签 → 验往返:正确密钥过,错密钥 / 过期 / 无 sub / 频道不符 一律不过(且原因具名)', () => {
|
||
const tok = issueConnectToken('u_42', SECRET, NOW)
|
||
assert.equal(verifyHmacJwt(tok.token, SECRET, NOW + 1).ok, true)
|
||
assert.deepEqual(verifyHmacJwt(tok.token, 'other-secret', NOW + 1), { ok: false, reason: 'bad-signature' })
|
||
assert.deepEqual(verifyHmacJwt(tok.token, SECRET, tok.exp + 1), { ok: false, reason: 'expired' })
|
||
assert.deepEqual(verifyHmacJwt('not-a-jwt', SECRET, NOW), { ok: false, reason: 'malformed' })
|
||
assert.deepEqual(verifyHmacJwt(signHmacJwt({ exp: NOW + 60 }, SECRET), SECRET, NOW), { ok: false, reason: 'no-sub' })
|
||
// 订阅票据:频道不符 ⇒ 不过("这张票只对这一个频道有效")
|
||
const sub = issueSubscriptionToken('u_42', 'im:imr_a', SECRET, NOW)
|
||
assert.equal(verifyHmacJwt(sub.token, SECRET, NOW + 1, { expectedChannel: 'im:imr_a' }).ok, true)
|
||
assert.equal(verifyHmacJwt(sub.token, SECRET, NOW + 1, { expectedChannel: 'im:imr_b' }).ok, false)
|
||
// ⛔ 空密钥 ⇒ 恒不过(fail-closed,⛔ 不"跳过校验")
|
||
assert.equal(verifyHmacJwt(tok.token, '', NOW + 1).ok, false)
|
||
})
|
||
|
||
test('G-5 · claims 口径:sub 恒为字符串、exp = now + ttl、ttl 有下限;签发结果里⛔ 没有密钥字段', () => {
|
||
const c = buildConnectClaims(42, NOW)
|
||
assert.equal(c.sub, '42') // 数字 id 也必须转成字符串(协议要求,否则连接被拒)
|
||
assert.equal(c.exp, NOW + IM_GATEWAY_CONNECT_TTL_SEC)
|
||
assert.equal(c.iat, NOW)
|
||
const s = buildSubscriptionClaims('u_1', 'im:imr_x', NOW)
|
||
assert.equal(s.channel, 'im:imr_x')
|
||
assert.equal(s.exp, NOW + IM_GATEWAY_SUBSCRIBE_TTL_SEC)
|
||
|
||
const issued = issueSubscriptionToken('u_1', 'im:imr_x', SECRET, NOW)
|
||
assert.equal(issued.scope, 'subscribe')
|
||
assert.equal(issued.channel, 'im:imr_x')
|
||
assert.equal(issued.exp, NOW + IM_GATEWAY_SUBSCRIBE_TTL_SEC)
|
||
assert.equal(issued.ttlSec, IM_GATEWAY_SUBSCRIBE_TTL_SEC)
|
||
assert.equal(issued.exp - NOW, issued.ttlSec)
|
||
// 🔴 响应形状级判据:**没有**任何字段的名字里带 key / secret
|
||
for (const k of Object.keys(issued)) {
|
||
assert.doesNotMatch(k.toLowerCase(), /key|secret|token_?secret/)
|
||
}
|
||
assert.ok(issued.token.length > 0)
|
||
// ttl 下限:0 / 负数 ⇒ 至少 1 s(⛔ 不签出"出生即过期"的票据)
|
||
assert.equal(buildConnectClaims('u', NOW, 0).exp, NOW + 1)
|
||
assert.equal(buildConnectClaims('u', NOW, -5).exp, NOW + 1)
|
||
const conn = issueConnectToken('u_1', SECRET, NOW)
|
||
assert.equal(conn.scope, 'connect')
|
||
assert.equal(conn.channel, undefined)
|
||
})
|
||
|
||
// ── 判据 ③ · 频道名 ↔ 房间 id(严格反解)────────────────────────────────────
|
||
|
||
test('G-6 · 频道名反解:往返恒等;前缀不符 / 非 imr_ / 含 : / 空 ⇒ 一律 undefined', () => {
|
||
const prefix = 'im:'
|
||
const roomId = 'imr_5f1c-9a'
|
||
const channel = channelFor(roomId, prefix)
|
||
assert.equal(channel, 'im:imr_5f1c-9a')
|
||
assert.equal(roomIdFromChannel(channel, prefix), roomId)
|
||
|
||
// 严格拒绝(各给一条反例 ⇒ "猜"这条路被关掉)
|
||
assert.equal(roomIdFromChannel('other:imr_5f1c', prefix), undefined) // 前缀不符
|
||
assert.equal(roomIdFromChannel('im:notaroom', prefix), undefined) // 不是 imr_ 起头
|
||
assert.equal(roomIdFromChannel('im:imr_a:b', prefix), undefined) // 房间 id 里带 `:`(跨命名空间注入)
|
||
assert.equal(roomIdFromChannel('im:imr_', prefix), undefined) // 余部为空
|
||
assert.equal(roomIdFromChannel('im:', prefix), undefined)
|
||
assert.equal(roomIdFromChannel('', prefix), undefined)
|
||
assert.equal(roomIdFromChannel('im:imr_a b', prefix), undefined) // 空格
|
||
// 默认前缀(未配 env 时)也要能反解
|
||
assert.equal(roomIdFromChannel('im-room-imr_x', undefined), 'imr_x')
|
||
assert.equal(GATEWAY_ROOM_ID_PREFIX, 'imr_')
|
||
})
|
||
|
||
test('G-7 · 频道前缀单一来源:缺项 / 空串 ⇒ 默认值(⛔ 不返回空串);规范化只发生在 `channelFor` 里', () => {
|
||
assert.equal(resolveChannelPrefix({}), 'im-room-')
|
||
assert.equal(resolveChannelPrefix({ DSH_IM_GATEWAY_CHANNEL_PREFIX: '' }), 'im-room-')
|
||
assert.equal(resolveChannelPrefix({ DSH_IM_GATEWAY_CHANNEL_PREFIX: 'im:' }), 'im:')
|
||
// 本函数**只做取值**(⛔ 不悄悄改写运维给的串)—— 规范化是 `channelFor` 的职责,且
|
||
// 两处必须同规则,否则"发布进 A 频道、订阅判在 B 频道"(本用例顺手把这条也钉住)。
|
||
assert.equal(resolveChannelPrefix({ DSH_IM_GATEWAY_CHANNEL_PREFIX: 'im $:' }), 'im $:')
|
||
assert.equal(channelFor('imr_a', 'im $:'), 'im__:imr_a')
|
||
assert.equal(roomIdFromChannel('im__:imr_a', 'im $:'), 'imr_a')
|
||
})
|
||
|
||
// ── 判据 ④ · 真客户端件(网关协议规则)──────────────────────────────────────
|
||
|
||
/**
|
||
* 载入**真客户端件**(`poc/im-conversation-tabs/lib/client.js`)。
|
||
*
|
||
* 做法:给它一个假的 `window.__ModuleLoader__`,把 `load()` 的入参截下来,再用桩 `require`
|
||
* 执行 `factory()`。⛔ 不 mock 任何被测函数 —— 拿到的是**生产那份代码的同一份实现**。
|
||
*/
|
||
function loadClientBundle() {
|
||
const src = readSrc('poc/im-conversation-tabs/lib/client.js')
|
||
let captured = null
|
||
const reactStub = new Proxy(
|
||
{},
|
||
{
|
||
get: () => () => undefined,
|
||
},
|
||
)
|
||
const requireStub = (name) => {
|
||
if (name === 'react' || name === 'react/jsx-runtime') return reactStub
|
||
if (name === 'react-dom') return { createPortal: () => null }
|
||
throw new Error('unexpected require in bundle: ' + name)
|
||
}
|
||
const fakeWindow = {
|
||
__ModuleLoader__: { load: (def) => { captured = def } },
|
||
console: { warn: () => undefined, log: () => undefined },
|
||
location: { protocol: 'https:', host: 'example.test' },
|
||
}
|
||
// eslint-disable-next-line no-new-func
|
||
const run = new Function('window', 'document', 'require', src)
|
||
run(fakeWindow, { querySelector: () => null, head: { appendChild: () => undefined } }, requireStub)
|
||
assert.notEqual(captured, null, '未捕获到 __ModuleLoader__.load 的注册对象')
|
||
const mod = captured.factory(requireStub)
|
||
assert.ok(mod.imTransport !== undefined, '客户端件必须导出 imTransport(判据 ③ 的独立断言面)')
|
||
return mod
|
||
}
|
||
|
||
test('G-8 · 客户端件:「空对象即回」——回的是**空命令**;非空对象**不回**(独立断言,⛔ 不靠端到端)', () => {
|
||
const { imTransport: T } = loadClientBundle()
|
||
// ① 只有"零键对象"才算协议级 ping
|
||
assert.equal(T.isGatewayPing({}), true)
|
||
assert.equal(JSON.parse(T.GATEWAY_PONG) !== null && Object.keys(JSON.parse(T.GATEWAY_PONG)).length, 0)
|
||
assert.equal(T.GATEWAY_PONG, '{}')
|
||
// ② 非空对象一律**不是** ping ⇒ 客户端不会误回(乱回会被判 3501 bad request)
|
||
assert.equal(T.isGatewayPing({ id: 0 }), false)
|
||
assert.equal(T.isGatewayPing({ push: {} }), false)
|
||
assert.equal(T.isGatewayPing({ connect: {} }), false)
|
||
// ③ 形态边界:数组 / null / 字符串 / 数字 都不是
|
||
assert.equal(T.isGatewayPing([]), false)
|
||
assert.equal(T.isGatewayPing(null), false)
|
||
assert.equal(T.isGatewayPing('{}'), false)
|
||
assert.equal(T.isGatewayPing(0), false)
|
||
// ④ 原型链上的键不算(`{}` 的 toString 之类不得把它判成"非空")
|
||
assert.equal(T.isGatewayPing(Object.create({ inherited: 1 })), true)
|
||
})
|
||
|
||
test('G-9 · 客户端件:出站命令形状(id 从 1 起、方法名即键)+ 推送取回"原帧"逐字相同', () => {
|
||
const { imTransport: T } = loadClientBundle()
|
||
assert.equal(T.GATEWAY_FIRST_ID, 1)
|
||
assert.deepEqual(T.gatewayCommand(1, 'connect', { token: 'tk' }), { id: 1, connect: { token: 'tk' } })
|
||
assert.deepEqual(T.gatewayCommand(2, 'subscribe', { channel: 'im:imr_x' }), {
|
||
id: 2,
|
||
subscribe: { channel: 'im:imr_x' },
|
||
})
|
||
// 平台发出来的那一串(含 `op` 制信封)原样装在 `pub.data.frame` 里 ⇒ 取出来必须**逐字相同**
|
||
const original = '{"op":"message","roomId":"imr_x","messages":[{"id":"m1","seq":7}]}'
|
||
const push = { push: { channel: 'im:imr_x', pub: { data: { frame: original }, offset: 7 } } }
|
||
assert.equal(T.appFrameOfPush(push), original)
|
||
assert.equal(JSON.parse(T.appFrameOfPush(push)).op, 'message')
|
||
// 不是 pub 推送 ⇒ null(join / leave 交给平台产 presence 帧,客户端不处理)
|
||
assert.equal(T.appFrameOfPush({ push: { channel: 'im:imr_x', join: 'c1' } }), null)
|
||
assert.equal(T.appFrameOfPush({ push: { channel: 'im:imr_x', leave: 'c1' } }), null)
|
||
assert.equal(T.appFrameOfPush({}), null)
|
||
assert.equal(T.appFrameOfPush({ push: { pub: { data: {} } } }), null)
|
||
assert.equal(T.appFrameOfPush(null), null)
|
||
// 断开建议读得出来(用户要看得见原因)
|
||
assert.deepEqual(T.disconnectInfoOf({ disconnect: { code: 3012, reason: 'no pong' } }), {
|
||
code: 3012,
|
||
reason: 'no pong',
|
||
})
|
||
assert.equal(T.disconnectInfoOf({}), null)
|
||
// 两套协议的 op 白名单仍然只在 native 侧生效(gateway 的帧⛔ 不走白名单)
|
||
assert.deepEqual(T.WS_OPS_OUT, ['subscribe', 'resume', 'ping'])
|
||
assert.ok(T.WS_OPS_OUT.indexOf('pong') < 0, '`pong` ⛔ 不是合法出站 op(ws.ts 实现里没有该 case)')
|
||
})
|
||
|
||
// ── 判据 ⑤ · 源码级结构性判据(本机可查、无需真环境)────────────────────────
|
||
|
||
test('G-10 · 平台侧 fail-closed 分支齐备:非 gateway ⇒ 403 / 无密钥 ⇒ 403 / 非成员 ⇒ 403(票据面)', () => {
|
||
const src = readSrc('src/web/routes/im.ts')
|
||
assert.match(src, /app\.post\('\/api\/im\/gateway\/token'/)
|
||
assert.match(src, /reply\.code\(403\)\.send\(\{ error: 'gateway-not-enabled'/)
|
||
assert.match(src, /reply\.code\(403\)\.send\(\{ error: 'gateway-key-missing'/)
|
||
assert.match(src, /reply\.code\(403\)\.send\(\{ error: 'not-a-member'/)
|
||
// 🔴 成员表是**唯一**判据:签发订阅票据之前必须真查一次
|
||
assert.match(src, /await store\.isMember\(roomId, user\.id\)/)
|
||
// 🔴 票据用**共享密钥**签;密钥只从 env 来,⛔ 不出现在响应体
|
||
assert.match(src, /issueSubscriptionToken\(user\.id, channelFor\(roomId, channelPrefix\), gatewayKey, nowSec\)/)
|
||
assert.doesNotMatch(src, /gatewayKey\s*[,)]\s*\}\s*\)\s*$/m)
|
||
})
|
||
|
||
test('G-11 · 订阅回检面:与在线态回调面**同一套密钥与常量时间比对**,⛔ 不另造第二套', () => {
|
||
const src = readSrc('src/web/routes/im.ts')
|
||
const route = new RegExp(
|
||
'app\\.post\\(IM_GATEWAY_SUBSCRIBE_CALLBACK_PATH[\\s\\S]{0,2600}?return \\{ result: \\{\\} \\}',
|
||
)
|
||
const m = route.exec(src)
|
||
assert.notEqual(m, null, '未找到订阅回检路由体')
|
||
const body = m[0]
|
||
assert.match(body, /selection\.kind !== 'gateway' \|\| gatewayKey === ''/)
|
||
assert.match(body, /reply\.code\(404\)\.send\(\{ error: 'not-found' \}\)/) // fail-closed 不暴露
|
||
assert.match(body, /secretEquals\(supplied, gatewayKey\)/)
|
||
assert.match(body, /reply\.code\(401\)\.send\(\{ error: 'bad-gateway-key' \}\)/)
|
||
assert.match(body, /roomIdFromChannel\(channel, channelPrefix\)/)
|
||
assert.match(body, /store\.isMember\(roomId, userId\)/)
|
||
// 拒绝形状遵守回检契约(code 落 400–1999,且显式 terminal)
|
||
assert.match(body, /code: 403, message: reason, temporary: false/)
|
||
// ⛔ 回调面里不得出现任何把密钥回给调用方的写法
|
||
assert.doesNotMatch(body, /gatewayKey\s*\}\s*\)/)
|
||
assert.equal(IM_GATEWAY_SUBSCRIBE_CALLBACK_PATH, '/api/im/gateway/subscribe')
|
||
})
|
||
|
||
test('G-12 · 读数面不含凭据:stats 的准入计数只有整数与具名原因(⛔ 无 token / key 字段)', () => {
|
||
const src = readSrc('src/web/routes/im.ts')
|
||
const block = /const gatewayAccess = \{([\s\S]*?)\n \}/.exec(src)
|
||
assert.notEqual(block, null, '未找到 gatewayAccess 计数器')
|
||
const fields = block[1]
|
||
assert.match(fields, /connectIssued/)
|
||
assert.match(fields, /subscribeIssued/)
|
||
assert.match(fields, /subscribeAllowed/)
|
||
assert.match(fields, /rejected/)
|
||
assert.match(fields, /lastReject/)
|
||
for (const line of fields.split('\n')) {
|
||
const name = /^\s*([A-Za-z]+):/.exec(line)
|
||
if (name === null) continue
|
||
assert.doesNotMatch(name[1].toLowerCase(), /token|key|secret/)
|
||
}
|
||
// 响应体里只 spread 一份拷贝(⛔ 不外泄内部对象)
|
||
assert.match(src, /gatewayAccess: \{ \.\.\.gatewayAccess \}/)
|
||
})
|
||
|
||
test('G-13 · 传输口径面:只回协议与路径 + 保活要求(⛔ 无端点地址、无凭据、无票据)', () => {
|
||
const src = readSrc('src/web/routes/im.ts')
|
||
const m = /app\.get\('\/api\/im\/transport'[\s\S]{0,2000}?\n \}\)/.exec(src)
|
||
assert.notEqual(m, null, '未找到传输口径路由体')
|
||
const body = m[0]
|
||
assert.match(body, /backend: 'native' as const/)
|
||
assert.match(body, /backend: 'gateway' as const/)
|
||
assert.match(body, /pongFrame: '\{\}'/)
|
||
assert.match(body, /tokenPath: '\/api\/im\/gateway\/token'/)
|
||
assert.match(body, /keepalive: planKeepalive\(\)/)
|
||
assert.match(body, /keepaliveAnchor: 'per-connection-auth-complete' as const/)
|
||
// ⛔ 不下发端点地址 / 密钥 / 票据
|
||
assert.doesNotMatch(body, /DSH_IM_GATEWAY_URL|endpoint|gatewayKey/)
|
||
})
|
||
|
||
test('G-14 · 客户端件源码级:网关分支必须"空对象即回"+ ⛔ 不出未认 op', () => {
|
||
const src = readSrc('poc/im-conversation-tabs/lib/client.js')
|
||
// ① 回帧落点存在且**就在** isGatewayPing 分支里
|
||
const m = /if \(isGatewayPing\(msg\)\) \{([\s\S]{0,400}?)\n \}/.exec(src)
|
||
assert.notEqual(m, null, '未找到空对象帧分支')
|
||
assert.match(m[1], /sendRaw\(ws, GATEWAY_PONG\)/)
|
||
// ② 网关模式不发应用层 ping(外部层会发协议级 ping)
|
||
assert.match(src, /if \(gatewayMode\) return/)
|
||
// ③ 模式来自平台下发(⛔ 不硬编码)+ 取不到时**不**默认退回 native
|
||
assert.match(src, /apiFetch\('\/transport'\)/)
|
||
assert.match(src, /const gatewayMode = cfg\.protocol === PROTOCOL_GATEWAY/)
|
||
// ④ 应用层帧两种模式共用同一段分发(换连接层不动协议)
|
||
assert.match(src, /onAppFrame\(msg\)/)
|
||
assert.match(src, /onAppFrame\(app\)/)
|
||
// ⑤ ⛔ 出站白名单里不得出现 `pong`(那是服务端回帧;发出去 = bad-op + destroy)
|
||
const ops = /const WS_OPS_OUT = \[([^\]]*)\]/.exec(src)
|
||
assert.notEqual(ops, null)
|
||
assert.doesNotMatch(ops[1], /pong/)
|
||
})
|
||
|
||
test('G-15 · 两处房间 id 前缀字面必须一致(签发 / 发布 / 反解三面共用同一命名空间)', () => {
|
||
const routes = readSrc('src/web/routes/im.ts')
|
||
const m = /const ROOM_ID_PREFIX = '([^']+)'/.exec(routes)
|
||
assert.notEqual(m, null, '未找到 routes 的 ROOM_ID_PREFIX')
|
||
assert.equal(m[1], GATEWAY_ROOM_ID_PREFIX)
|
||
})
|