Files
danyang-match/ui-tests/miniapp-auth-contracts.mjs
T
WorkBuddy 63c18d89c2 fix(miniapp): 修复全站 401 —— 认证链字段名不一致 + 首页 onRetry 缺失
根因(读码 + 本机后端实测坐实):
- 后端登录/刷新返回 accessToken,前端 authStore 却读 result.token →
  accessToken 从未落库 → 每个请求都不带 Authorization → 全站 401(首页无法加载)。
- /auth/refresh 少传 userId:后端以 userId 定位 Redis refresh token,
  缺失即 BAD_REQUEST → HTTP 400(实测 {refreshToken} 体 → 400)。
- 首页 index.wxml 绑定 bind:retry="onRetry"(ui-error-state 触发 retry 事件),
  但 index.ts 无该方法 → 控制台报错、错误页「重新加载」失效。
- app.globalData.token 从未赋值,多页直读它拼 Authorization → Bearer undefined。

修复:
- authStore:登录/刷新改用 result.accessToken;刷新传 this.state.userId。
- authService.refresh(userId):发送 { userId, refreshToken }。
- pages/index:新增 onRetry;onLoad 订阅 authStore(expired → 回登录引导;
  刷新成功且停在错误态 → 自动重载);onUnload 退订。
- utils/api:saveToken/clearToken/getToken 同步 app.globalData.token,兼容旧页面。

守卫补强:
- ui-tests/miniapp-auth-contracts.mjs 原仅覆盖 hydrate,对字段名缺陷零覆盖。
  新增行为断言:登录后 storage.accessToken 必须等于后端 accessToken;
  刷新请求体必须含 userId + refreshToken 并持久化新 accessToken。
  已双向负向注入验证判别力(改回 result.token / 去掉 userId 均 exit 1)。

验证:
- tsc --noEmit -p miniapp/tsconfig.json → exit 0,零错误。
- scripts/run-ui-contracts.mjs → 18/18 PASS。
- 本机 8085 /auth/refresh 三态实测:{} → 400;{refreshToken} → 400;
  {userId,refreshToken} → 500(进入服务校验,非 400)。
- 未验证:开发者工具内实际登录→首页链路(本机无法驱动 devtools,待用户复测)。
2026-09-22 19:08:16 +08:00

71 lines
2.6 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.
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
/**
* 小程序鉴权契约守卫
*
* 覆盖三类此前只在真机/开发者工具上才会暴露的缺陷(2026-09-22 首页 401 事故):
* 1. hydrate:没有 access token 时不得恢复登录态。
* 2. 登录/刷新:后端返回字段是 accessToken(不是 token),若字段名写错,
* token 永远写不进 storage → 每个请求都不带 Authorization → 全站 401。
* 3. 刷新:后端 /auth/refresh 需要 userId(用于定位 Redis 中的 refresh token),
* 缺失会直接 400,导致刷新失败、登录态被误清。
*/
const removed = []
const storage = {}
globalThis.wx = {
getStorageSync(key) {
if (key in storage) return storage[key]
if (key === 'isLoggedIn') return { isLoggedIn: true, userId: 'stale-user' }
return ''
},
setStorageSync(key, value) {
storage[key] = value
},
removeStorageSync(key) {
removed.push(key)
delete storage[key]
},
}
const require = createRequire(import.meta.url)
// ---- 1. hydrate:无 token 不得恢复登录态 ----
const authStore = require('../miniapp/stores/authStore.js')
authStore.hydrate()
assert.equal(authStore.state.isLoggedIn, false, '没有 access token 时不得恢复登录态')
assert.ok(removed.includes('isLoggedIn'), '应清理失真的登录快照')
const api = require('../miniapp/utils/api.js')
// ---- 2. 登录:必须持久化 accessToken(后端字段名为 accessToken)----
api.post = async () => ({ accessToken: 'ACC-1', refreshToken: 'REF-1', userId: 'u-1', expiresIn: 7200 })
await authStore.loginWithWechat('wx-code')
assert.equal(
storage.accessToken,
'ACC-1',
'登录后必须把后端返回的 accessToken 写入 storage(字段名写成 token 会落空 → 全站 401)',
)
assert.equal(storage.refreshToken, 'REF-1', '登录后必须写入 refreshToken')
// ---- 3. 刷新:必须带 userId + refreshToken,并持久化新的 accessToken ----
let refreshBody = null
api.post = async (url, body) => {
if (url === '/auth/refresh') {
refreshBody = body
return { accessToken: 'ACC-2', refreshToken: 'REF-2', expiresIn: 7200 }
}
return {}
}
await authStore.refresh()
assert.ok(refreshBody, '刷新必须发起 /auth/refresh 请求')
assert.equal(
refreshBody.userId,
'u-1',
'刷新请求必须携带 userId(后端据此定位 Redis refresh token,缺失会返回 400)',
)
assert.equal(refreshBody.refreshToken, 'REF-1', '刷新请求必须携带 refreshToken')
assert.equal(storage.accessToken, 'ACC-2', '刷新成功后必须持久化新的 accessToken')
console.log('Miniapp auth contracts passed')