/** * 夹具 —— 平台侧官方账号授权两跳(`/api/dsh-oauth/{begin,complete}`) * ============================================================================ * 判据分三类: * ① **闸门**:公网回调必拒、`state` 必填、`state` 单次有效(不许重放); * ② **通路**:回环回调能起官方授权(回 `authorizeUrl`)、换码能拿到身份(只回非凭据展示名); * ③ **口径**:官方信封按 `{code:0,data:{biz_code:0,biz_data:…}}` 拆;`code`/`biz_code` 非 0 一律失败, * 且**不得**把官方原始 body 或任何 token 回给调用方。 * ⛔ 不联网:打桩 `globalThis.fetch`。跑法:`node --test test/dsh-oauth.test.mjs`(需先 `npm run build`)。 */ import assert from 'node:assert/strict' import test from 'node:test' import Fastify from 'fastify' import { dshOauthRoutes } from '../lib/web/routes/dsh-oauth.js' const LOOPBACK = 'http://127.0.0.1:20090/oauth/callback' /** 桩 `approve` 端点的返回码(T7/T8 切换用)。 */ let approveStatus = 200 /** 官方成功信封(判据 = 官方 `protocol.ts:76/219-224`)。 */ const okEnvelope = (biz) => JSON.stringify({ code: 0, msg: '', data: { biz_code: 0, biz_msg: '', biz_data: biz } }) const AUTH_INIT_OK = okEnvelope({ authorize_url: 'https://platform.deepseek.com/dsh/authorize?authorize_id=TESTID', authorize_id: 'TESTID', expires_in: 600, }) const AUTH_EXCHANGE_OK = okEnvelope({ id: 'user-123', email: 'tester@example.com' }) /** 起一个只挂本路由的 app(打桩 fetch)。`reply` 记下最后一次官方响应。 */ async function makeApp(responder, calls = []) { const original = globalThis.fetch globalThis.fetch = async (url, init) => { calls.push({ url: String(url), body: JSON.parse(init?.body ?? '{}') }) return { status: 200, text: async () => responder(String(url)) } } const app = Fastify() // 平台侧依赖(建号与配置)用最小桩:夹具只验本路由的口径,不碰真库。 app.decorate('db', { findUserByUsername: async (username) => ({ id: 'uid-1', username, role: 'active' }), createUser: async (row) => ({ ...row }), createSession: async () => {}, audit: async () => {}, }) app.decorate('config', { dataRoot: '/tmp/dsh-oauth-test', sessionTtlSeconds: 3600, secureCookies: false, cookieDomain: undefined }) // 桩:既有配对批准端点(真实现是 `overlay-pair.ts` 的 `requireAuth` 路由;这里只验调用与回调口径)。 app.post('/api/overlay/pair/approve', async (_req, reply) => reply.code(approveStatus).send(approveStatus === 200 ? { ok: true, status: 'approved' } : { error: 'pair-already-used' })) await app.register(dshOauthRoutes) return { app, restore: () => { globalThis.fetch = original }, calls } } test('T1 公网回调被拒(闸门)', async () => { const { app, restore } = await makeApp(() => AUTH_INIT_OK) try { const res = await app.inject({ method: 'POST', url: '/api/dsh-oauth/begin', payload: { redirectUri: 'https://ai1net.com/oauth/callback', state: 'S'.repeat(16) } }) assert.equal(res.statusCode, 400) assert.deepEqual(res.json(), { error: 'redirect-uri-not-loopback' }) } finally { restore(); await app.close() } }) test('T2 缺 state 被拒(闸门)', async () => { const { app, restore } = await makeApp(() => AUTH_INIT_OK) try { const res = await app.inject({ method: 'POST', url: '/api/dsh-oauth/begin', payload: { redirectUri: LOOPBACK } }) assert.equal(res.statusCode, 400) assert.deepEqual(res.json(), { error: 'state-required' }) } finally { restore(); await app.close() } }) test('T3 回环回调能起官方授权,且 verifier 不出现在响应里(通路 + 凭据纪律)', async () => { const calls = [] const { app, restore } = await makeApp(() => AUTH_INIT_OK, calls) try { const state = 'STATE_STATE_STATE_1' const res = await app.inject({ method: 'POST', url: '/api/dsh-oauth/begin', payload: { redirectUri: LOOPBACK, state } }) assert.equal(res.statusCode, 200) assert.equal(res.json().authorizeUrl, 'https://platform.deepseek.com/dsh/authorize?authorize_id=TESTID') // 发出的官方请求:challenge 有、verifier 无;redirect_uri 是回环。 const init = calls.find((c) => c.url.endsWith('/auth_init')) assert.ok(init, '应调用官方 auth_init') assert.equal(init.body.redirect_uri, LOOPBACK) assert.equal(init.body.code_challenge_method, 'S256') assert.ok(typeof init.body.code_challenge === 'string' && init.body.code_challenge.length > 20) assert.equal(init.body.code_verifier, undefined, '⛔ challenge 阶段不许发 verifier') assert.ok(!JSON.stringify(res.json()).includes('code_challenge'), '⛔ 不回显 PKCE 私料') } finally { restore(); await app.close() } }) test('T4 陌生 state 被拒(闸门)', async () => { const { app, restore } = await makeApp(() => AUTH_EXCHANGE_OK) try { const res = await app.inject({ method: 'POST', url: '/api/dsh-oauth/complete', payload: { code: 'C', state: 'NOT_OURS_STATE', redirectUri: LOOPBACK } }) assert.equal(res.statusCode, 400) assert.deepEqual(res.json(), { error: 'state-unknown-or-used' }) } finally { restore(); await app.close() } }) test('T5 换码拿到身份,只回展示名;state 单次有效(通路 + 不许重放)', async () => { const calls = [] const { app, restore } = await makeApp((u) => (u.endsWith('/auth_init') ? AUTH_INIT_OK : AUTH_EXCHANGE_OK), calls) try { const state = 'STATE_STATE_STATE_2' const begin = await app.inject({ method: 'POST', url: '/api/dsh-oauth/begin', payload: { redirectUri: LOOPBACK, state } }) assert.equal(begin.statusCode, 200) const first = await app.inject({ method: 'POST', url: '/api/dsh-oauth/complete', payload: { code: 'CODE1', state, redirectUri: LOOPBACK } }) assert.equal(first.statusCode, 200) const bound = first.json() assert.equal(bound.ok, true) // 回的是**平台用户名**(派生自官方身份),不是原始身份、也不是任何凭据。 assert.match(String(bound.account), /^dsh_[0-9a-f]{16}$/) // 换码用的是**服务端**留的 verifier,且与 challenge 不同源(verifier 才是真私料)。 const ex = calls.find((c) => c.url.endsWith('/auth_exchange')) assert.equal(ex.body.code_verifier !== undefined, true, '换码必须带服务端 verifier') assert.equal(ex.body.code, 'CODE1') const replay = await app.inject({ method: 'POST', url: '/api/dsh-oauth/complete', payload: { code: 'CODE1', state, redirectUri: LOOPBACK } }) assert.equal(replay.statusCode, 400, '同一 state 不许重放') assert.deepEqual(replay.json(), { error: 'state-unknown-or-used' }) } finally { restore(); await app.close() } }) test('T6 官方 biz_code≠0 ⇒ 失败且不回官方原文(口径)', async () => { const bad = JSON.stringify({ code: 0, msg: '', data: { biz_code: 40001, biz_msg: 'UPSTREAM_REJECTED', biz_data: null } }) const { app, restore } = await makeApp(() => bad) try { const res = await app.inject({ method: 'POST', url: '/api/dsh-oauth/begin', payload: { redirectUri: LOOPBACK, state: 'STATE_STATE_STATE_3' } }) assert.equal(res.statusCode, 502) assert.deepEqual(res.json(), { error: 'official-begin-failed' }) assert.ok(!JSON.stringify(res.json()).includes('UPSTREAM_REJECTED'), '⛔ 不回官方原文') } finally { restore(); await app.close() } }) test('T7 带 pairCode 且既有批准端点放行 ⇒ 报 pairBound(绑定这一步复用既有端点)', async () => { approveStatus = 200 const calls = [] const { app, restore } = await makeApp((u) => (u.endsWith('/auth_init') ? AUTH_INIT_OK : AUTH_EXCHANGE_OK), calls) try { const state = 'STATE_STATE_STATE_7' assert.equal((await app.inject({ method: 'POST', url: '/api/dsh-oauth/begin', payload: { redirectUri: LOOPBACK, state } })).statusCode, 200) const res = await app.inject({ method: 'POST', url: '/api/dsh-oauth/complete', payload: { code: 'CODE7', state, redirectUri: LOOPBACK, pairCode: 'PAIRCODE7' } }) assert.equal(res.statusCode, 200) const body = res.json() assert.equal(body.ok, true) assert.equal(body.pairBound, true) assert.match(String(body.account), /^dsh_[0-9a-f]{16}$/) } finally { restore(); await app.close() } }) test('T8 带 pairCode 但既有批准端点拒绝 ⇒ 409 pair-not-approved,且不改动账号/不发凭据', async () => { approveStatus = 409 const calls = [] const { app, restore } = await makeApp((u) => (u.endsWith('/auth_init') ? AUTH_INIT_OK : AUTH_EXCHANGE_OK), calls) try { const state = 'STATE_STATE_STATE_8' assert.equal((await app.inject({ method: 'POST', url: '/api/dsh-oauth/begin', payload: { redirectUri: LOOPBACK, state } })).statusCode, 200) const res = await app.inject({ method: 'POST', url: '/api/dsh-oauth/complete', payload: { code: 'CODE8', state, redirectUri: LOOPBACK, pairCode: 'PAIRCODE8' } }) assert.equal(res.statusCode, 409) assert.deepEqual(res.json(), { error: 'pair-not-approved', status: 409 }) // ⛔ 失败回执里不得出现官方原文、凭据或任何 token 痕迹。 const text = JSON.stringify(res.json()) assert.ok(!/token|cookie|verifier|authorize_url/i.test(text), '⛔ 失败回执不得夹带凭据类字段') } finally { approveStatus = 200; restore(); await app.close() } })