Files
dsh_shenxian/test/dsh-oauth.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

169 lines
9.2 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.
/**
* 夹具 —— 平台侧官方账号授权两跳(`/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() }
})