This commit is contained in:
commit
5ec99ce227
668 files changed
+164448
No files matched your search
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 覆盖网络 · **区域名录** `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'])
|
||||
})
|
||||
Reference in new issue
Block a user