// 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) })