task-25(会话/菜单/权限): 锁定 Cookie/Session 请求配置契约

Cookie 规则回归契约:withCredentials 字面量开启、同源根路径、有界超时、
前端不写 localStorage/sessionStorage/document.cookie 持久化凭证、不注入
Authorization/Bearer;current-user/menus/logout 三类会话调用统一复用共享
http 实例;登出走 X-Requested-With AJAX 标识;401 统一拦截并携带目标路径。
8 用例全过,171 单测 + vue-tsc/vite build 通过。
This commit is contained in:
2026-09-05 14:05:01 +08:00
parent 46622d9f42
commit 7c8374424d
+74
View File
@@ -0,0 +1,74 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import { loginRedirectTarget } from '../src/api/envelope.ts'
const httpSrc = readSource('src/api/http.ts')
const sessionSrc = readSource('src/api/session.ts')
test('test_task_025_cookie_session_config_normal_primary_path', () => {
// 正常主路径:共享传输开启同源 Cookie/SessionAxios withCredentials 必须开启)。
assert.match(httpSrc, /withCredentials:\s*true/, 'Cookie 规则:Axios withCredentials 必须开启')
assert.match(httpSrc, /baseURL:\s*'\/'/, '请求使用同源 Cookie/Session')
})
test('test_task_025_cookie_session_config_normal_variant_input', () => {
// 正常变体:current-user / menus / logout 三类会话调用全部复用同一共享 http 实例。
assert.match(sessionSrc, /import \{ http \} from '\.\/http'/)
const getCalls = (sessionSrc.match(/\bhttp\.get\b/g) || []).length
assert.equal(getCalls, 2, 'current-user 与 menus 两个 GET 均走共享 http')
assert.equal((sessionSrc.match(/\bhttp\.post\b/g) || []).length, 1, 'logout 走共享 http')
assert.match(sessionSrc, /current-user/)
assert.match(sessionSrc, /current-user\/menus/)
})
test('test_task_025_cookie_session_config_normal_repeated_operation_is_idempotent', () => {
// 正常重复:传输模块只创建一次 axios 实例,会话模块不另建客户端、不污染状态。
assert.equal((httpSrc.match(/axios\.create/g) || []).length, 1, '共享实例只创建一次')
assert.equal((sessionSrc.match(/axios\.create/g) || []).length, 0, '会话模块不自行创建新客户端')
assert.equal((sessionSrc.match(/import \{ http \}/g) || []).length, 1)
})
test('test_task_025_cookie_session_config_boundary_empty_input', () => {
// 边界空值:无登录态时请求层不把凭证写入本地/会话存储(前端不保存 JWT/密码)。
for (const rel of ['src/api/http.ts', 'src/api/session.ts']) {
const src = readSource(rel)
assert.equal((src.match(/localStorage/g) || []).length, 0, `${rel} 不得使用 localStorage`)
assert.equal((src.match(/sessionStorage/g) || []).length, 0, `${rel} 不得使用 sessionStorage`)
assert.equal(/document\.cookie\s*=/.test(src), false, `${rel} 不得直接写 document.cookie`)
}
})
test('test_task_025_cookie_session_config_boundary_single_item', () => {
// 边界单元素:登出是单次 POST,以 AJAX 标识发到共享实例,后端据此返回 JSON 而非页面。
assert.match(sessionSrc, /X-Requested-With/, '登出走 AJAX 标识以便后端识别为 JSON 请求')
assert.match(sessionSrc, /http\.post/)
assert.match(sessionSrc, /\/api\/admin\/logout/)
})
test('test_task_025_cookie_session_config_boundary_limit_or_missing_field', () => {
// 边界上限/缺字段:即便会话在身,传输层也不注入 token/Authorization 头——
// 凭证只由同源 Cookie 携带,前端禁止保存并在后续请求回填。
for (const rel of ['src/api/http.ts', 'src/api/session.ts', 'src/api/session-model.ts']) {
const src = readSource(rel)
assert.equal(/\bAuthorization\b/.test(src), false, `${rel} 不应注入 Authorization 头`)
assert.equal(/\bBearer\b/.test(src), false, `${rel} 不应出现 Bearer token`)
}
})
test('test_task_025_cookie_session_config_invalid_input_rejected', () => {
// 异常输入:Cookie 规则要求 withCredentials 必须开启、同源根路径、有界超时。
assert.match(httpSrc, /withCredentials:\s*true/, '传输配置必须字面量开启 withCredentials')
assert.match(httpSrc, /baseURL:\s*'\/'/, '传输配置必须使用同源根路径')
assert.match(httpSrc, /timeout:\s*\d+/, '传输配置必须声明数值型有界超时')
})
test('test_task_025_cookie_session_config_dependency_failure_returns_actionable_message', () => {
// 依赖失败:会话失效/网络异常走同一拦截与解包兜底,且登录跳转携带当前目标路径。
assert.match(httpSrc, /interceptors\.response/, '存在 401/异常统一响应拦截')
assert.match(httpSrc, /unwrap/)
assert.match(httpSrc, /isUnauthorized/)
assert.match(httpSrc, /login\?redirect=/)
const target = loginRedirectTarget({ pathname: '/admin-vue/account/users', search: '?x=1' })
assert.equal(target, encodeURIComponent('/admin-vue/account/users?x=1'), 'redirect 需携带当前目标路径')
})