Files
dsh_shenxian/test/overlay-zones.test.mjs
T
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

221 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 覆盖网络 · **区域名录** `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'])
})