169 lines
9.2 KiB
JavaScript
169 lines
9.2 KiB
JavaScript
/**
|
||
* 夹具 —— 平台侧官方账号授权两跳(`/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: '[email protected]' })
|
||
|
||
/** 起一个只挂本路由的 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() }
|
||
})
|