/** * 覆盖网络 · **区域名录** `GET /api/overlay/zones` 单测(F6 第一条最小竖切)。 * * 运行:`node --test test/overlay-zones.test.mjs`(测 `lib/` 产物 ⇒ 先 `npm run build`)。 * ⚠️ 本文件**不连库**:区数据源与凭据判据都是路由的**注入点**(见 `OverlayZonesOptions`), * 真验签那条路(T7/T8)用的是**一次性测试签名者**,⛔ 与生产密钥无关。 * * ## 这个文件要回答的四个问题(= 本棒的验收判据) * | # | 判据 | 落点 | * |---|---|---| * | ① | **无凭据 ⇒ 401 fail-closed** | T1 / T2 | * | ② | **无有效邀请码 ⇒ 空列表**(⛔ 不报错、⛔ 不回全量) | T3 / T4 | * | ③ | **有成员关系 ⇒ 可见;无成员关系 ⇒ 不可见**(逐用户独立) | T5 / T6 | * | ④ | **邀请码真的走验签**(受信签名者 / 有效期 / 载荷形状) | T7 / T8 | */ import assert from 'node:assert/strict' import { randomBytes } from 'node:crypto' import { test } from 'node:test' import Fastify from 'fastify' import { generateAuthorityKey, signPayloadWith } from '../lib/net/relay/identity.js' import { networkInvitePayload, newInviteNonce } from '../lib/net/relay/registry.js' import { INVITE_SIGNERS_ENV, OVERLAY_INVITE_HEADER, decodeInviteCode, defaultInviteCheck, overlayZoneRoutes, visibleZones, } from '../lib/web/routes/overlay-zones.js' // ── 夹具(⛔ 不连库、⛔ 不起真进程)─────────────────────────────────────────── /** 一个可用用户行(`toPublicUser` 只读这五个字段)。 */ function userRow(id) { return { id, username: `u-${id}`, role: 'user', created_at: 1, shared_model_granted: 0 } } /** * 造一个 Fastify 应用:假 `db`(只够 `requireAuthOrInstanceToken` 解析 cookie)+ * 注入的区数据源 / 凭据判据。 * * @param regionsFor - 名录里那些区**属于谁**(缺省 = 当前这个用户)。⛔ 与 `userId` 分开设, * 才能构造「**别人的**成员关系」那种情形(T6 的判据就靠它)。 */ async function buildApp({ userId = 'u1', regions = [], regionsFor, verifyInvite } = {}) { const owner = regionsFor ?? userId const app = Fastify() app.decorate('db', { findSessionWithUser: async () => ({ expiresAt: Date.now() + 60_000, user: userRow(userId) }), findUserById: async () => undefined, }) await app.register(overlayZoneRoutes, { source: { regionsOfMember: async (memberId) => (memberId === owner ? regions : []) }, ...(verifyInvite === undefined ? {} : { verifyInvite }), }) return app } /** 一次带 cookie 的取名录请求。 */ async function fetchZones(app, headers = {}) { const res = await app.inject({ method: 'GET', url: '/api/overlay/zones', headers: { cookie: 'sid=test-session', ...headers }, }) return { status: res.statusCode, body: res.json() } } /** 一枚**真签发**的邀请码(`base64url(JSON.stringify({doc, sig}))`)。 */ function signedInvite(privPem, { network = 'ops', expiresInMs = 600_000 } = {}) { const doc = { version: 1, network, nonce: newInviteNonce(randomBytes), issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + expiresInMs).toISOString(), } const sig = signPayloadWith(privPem, networkInvitePayload(doc)) return Buffer.from(JSON.stringify({ doc, sig }), 'utf8').toString('base64url') } const OK_INVITE = () => ({ ok: true, network: 'ops' }) // ── ① 无凭据 ⇒ 401 ───────────────────────────────────────────────────────── test('T1 无任何凭据 ⇒ 401 fail-closed(⛔ 不回空列表冒充成功)', async () => { const app = await buildApp({ regions: ['r1'], verifyInvite: OK_INVITE }) const res = await app.inject({ method: 'GET', url: '/api/overlay/zones' }) assert.equal(res.statusCode, 401) assert.deepEqual(res.json(), { error: 'unauthorized' }) await app.close() }) test('T2 实例码头缺席(只有 region 头别的东西)⇒ 仍 401', async () => { const app = await buildApp({ regions: ['r1'], verifyInvite: OK_INVITE }) const res = await app.inject({ method: 'GET', url: '/api/overlay/zones', headers: { 'x-dsh-im-region': 'r1' }, }) assert.equal(res.statusCode, 401) await app.close() }) // ── ② 邀请码闸 ───────────────────────────────────────────────────────────── test('T3 有凭据但**没带邀请码** ⇒ 200 且空列表(⛔ 不是 401、⛔ 不是全量)', async () => { const app = await buildApp({ regions: ['r1', 'r2'], verifyInvite: undefined }) // 未注入判据 ⇒ 走缺省真验签;环境变量未配受信签名者 ⇒ 一律不认。 const before = process.env[INVITE_SIGNERS_ENV] delete process.env[INVITE_SIGNERS_ENV] try { const { status, body } = await fetchZones(app) assert.equal(status, 200) assert.deepEqual(body, { zones: [], count: 0 }) } finally { if (before !== undefined) process.env[INVITE_SIGNERS_ENV] = before await app.close() } }) test('T4 邀请码**验不通** ⇒ 空列表(即使确有成员关系)', async () => { for (const reason of ['missing', 'bad-code', 'signature-mismatch', 'expired', 'no-trusted-signer']) { const app = await buildApp({ regions: ['r1'], verifyInvite: () => ({ ok: false, reason }) }) const { status, body } = await fetchZones(app, { [OVERLAY_INVITE_HEADER]: 'whatever' }) assert.equal(status, 200, reason) assert.deepEqual(body, { zones: [], count: 0 }, reason) await app.close() } }) // ── ③ 可见性 = 用户 × 区成员关系 ──────────────────────────────────────────── test('T5 有成员关系 ⇒ 可见,且去重 / 升序 / 空区不算一个区', async () => { const app = await buildApp({ userId: 'u1', regions: ['r2', 'r1', 'r2', ''], verifyInvite: OK_INVITE }) const { status, body } = await fetchZones(app, { [OVERLAY_INVITE_HEADER]: 'x' }) assert.equal(status, 200) assert.deepEqual(body, { zones: [{ region: 'r1' }, { region: 'r2' }], count: 2 }) await app.close() }) test('T6 无成员关系 ⇒ 不返回(逐用户独立,⛔ 不退化成"全体可见")', async () => { // mine:r1 是**我**的成员关系 ⇒ 回 r1。 const mine = await buildApp({ userId: 'u1', regions: ['r1'], verifyInvite: OK_INVITE }) // other:名录里 r9 是 **u1** 的成员关系,而当前请求者是 **u2** ⇒ 一个都不回 // (判据 = 路由必须按 `request.user.id` 取,⛔ 不是"谁问都给同一份")。 const other = await buildApp({ userId: 'u2', regions: ['r9'], regionsFor: 'u1', verifyInvite: OK_INVITE }) const a = await fetchZones(mine, { [OVERLAY_INVITE_HEADER]: 'x' }) const b = await fetchZones(other, { [OVERLAY_INVITE_HEADER]: 'x' }) assert.deepEqual(a.body.zones, [{ region: 'r1' }]) assert.deepEqual(b.body, { zones: [], count: 0 }, 'u2 不在任何区 ⇒ 一个都不回') await mine.close() await other.close() }) // ── ④ 真验签路径(缺省判据)──────────────────────────────────────────────── test('T7 真签发的邀请码 ⇒ 名录可出;同一枚码在**换过受信签名者**后 ⇒ 空列表', async () => { const signer = generateAuthorityKey() const code = signedInvite(signer.privateKeyPem) const app = await buildApp({ regions: ['r1', 'r3'] }) // 不注入判据 ⇒ 走缺省真验签 const before = process.env[INVITE_SIGNERS_ENV] try { process.env[INVITE_SIGNERS_ENV] = signer.publicKey const good = await fetchZones(app, { [OVERLAY_INVITE_HEADER]: code }) assert.deepEqual(good.body.zones, [{ region: 'r1' }, { region: 'r3' }]) // 换一把不相关的签名者 ⇒ 同一枚码**不认** ⇒ 空列表(⛔ 不降级放行) process.env[INVITE_SIGNERS_ENV] = generateAuthorityKey().publicKey const bad = await fetchZones(app, { [OVERLAY_INVITE_HEADER]: code }) assert.equal(bad.status, 200) assert.deepEqual(bad.body, { zones: [], count: 0 }) } finally { if (before === undefined) delete process.env[INVITE_SIGNERS_ENV] else process.env[INVITE_SIGNERS_ENV] = before await app.close() } }) test('T8 过期 / 未标定网 / 裸 JSON 三种形态的判据', async () => { const signer = generateAuthorityKey() // ① 过期:`expired` const expired = signedInvite(signer.privateKeyPem, { expiresInMs: -1000 }) assert.equal( defaultInviteCheck(expired, [signer.publicKey]).ok, false, ) // ② 网标定非法(`parseNetworkInvite` 严格)⇒ 不认 const badNet = Buffer.from( JSON.stringify({ doc: { version: 1, network: 'not-a-network', nonce: 'f'.repeat(32), issuedAt: new Date().toISOString(), expiresAt: '' }, sig: 'zz' }), 'utf8', ).toString('base64url') assert.equal(defaultInviteCheck(badNet, [signer.publicKey]).ok, false) // ③ 裸 JSON 形态也认(排障用) const doc = { version: 1, network: 'ops', nonce: newInviteNonce(randomBytes), issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), } const raw = JSON.stringify({ doc, sig: signPayloadWith(signer.privateKeyPem, networkInvitePayload(doc)) }) assert.deepEqual(decodeInviteCode(raw)?.doc, doc) assert.equal(defaultInviteCheck(raw, [signer.publicKey]).ok, true) // ④ 空 / 乱码 ⇒ 不认(⛔ 不抛) for (const bad of [undefined, '', ' ', 'not-base64-json', 'e30=' /* {} */]) { assert.equal(defaultInviteCheck(bad, [signer.publicKey]).ok, false, String(bad)) } }) // ── 纯函数(判据本体)────────────────────────────────────────────────────── test('T9 visibleZones:邀请码没验通 ⇒ 恒空(⛔ 不"降级成只回自己的")', () => { assert.deepEqual(visibleZones(['r1', 'r2'], false), []) assert.deepEqual(visibleZones([], true), []) assert.deepEqual(visibleZones(['', ' ', 'r1'], true), ['r1']) assert.deepEqual(visibleZones(['b', 'a', 'b'], true), ['a', 'b']) })