|
|
@@ -216,6 +216,67 @@ describe('auth API', () => {
|
|
|
expect(global.uni.showToast).not.toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
+ // 回归:首页冷启动会并发打出多个 401(family/member/list、membership/my、onboarding/progress…)。
|
|
|
+ // 修复前 _handle401 在 _reloginLock 为真时直接调 _clearAuthAndRedirect():
|
|
|
+ // 第 1 个 401 刚启动 silentRelogin()(uni.login 异步),第 2、3 个 401 立刻把 token 从
|
|
|
+ // storage 抹掉并预约 1500ms 后 reLaunch 登录页 → 在途续期被打断,
|
|
|
+ // 线上表现为「控制台只有 401、没有任何续期日志、用户被弹回登录页」。
|
|
|
+ test('concurrent 401s queue behind one renewal and all get replayed', async () => {
|
|
|
+ global.uni._storage.openid = 'openid_abc'
|
|
|
+ global.uni._storage.token = 'expired-token'
|
|
|
+ mockUniLoginCode('fresh-wxcode')
|
|
|
+ // 3 个业务请求同时 401 → 续期 1 次成功 → 3 个重放全部 200
|
|
|
+ mockSequence([
|
|
|
+ { data: { code: 401, message: 'Token无效或已过期', data: null } },
|
|
|
+ { data: { code: 401, message: 'Token无效或已过期', data: null } },
|
|
|
+ { data: { code: 401, message: 'Token无效或已过期', data: null } },
|
|
|
+ { data: { code: 200, data: { token: 'new-jwt', userId: 42, role: 'parent', familyId: 1, openid: 'openid_abc' } } },
|
|
|
+ { data: { code: 200, data: { ok: 'verify' } } },
|
|
|
+ { data: { code: 200, data: { ok: 'members' } } },
|
|
|
+ { data: { code: 200, data: { ok: 'membership' } } }
|
|
|
+ ])
|
|
|
+
|
|
|
+ const [verifyRes, memberRes, membershipRes] = await Promise.all([
|
|
|
+ api.verifyToken(),
|
|
|
+ api.getFamilyMemberList({}),
|
|
|
+ api.getMyMembership()
|
|
|
+ ])
|
|
|
+
|
|
|
+ // 三个请求都必须拿到成功结果(修复前后两个会被 reject)
|
|
|
+ expect(verifyRes.data.ok).toBe('verify')
|
|
|
+ expect(memberRes.data.ok).toBe('members')
|
|
|
+ expect(membershipRes.data.ok).toBe('membership')
|
|
|
+
|
|
|
+ const calls = global.uni.request.mock.calls.map((c) => c[0])
|
|
|
+ // 续期只发生一次,绝不能因并发 401 引发续期风暴
|
|
|
+ expect(calls.filter((c) => /\/api\/auth\/silent-login/.test(c.url)).length).toBe(1)
|
|
|
+ // token 必须是被续期后的新值,而不是被并发 401 抹掉
|
|
|
+ expect(global.uni._storage.token).toBe('new-jwt')
|
|
|
+ // 关键回归点:并发 401 不得触发「登录已过期」提示,也不得跳登录页
|
|
|
+ expect(global.uni.showToast).not.toHaveBeenCalled()
|
|
|
+ expect(global.uni.reLaunch).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 回归:重放请求若仍 401(刚签发的 token 依旧不可用),必须直接失败而不是重新入队,
|
|
|
+ // 否则「续期 → 重放 → 401 → 续期 → 重放」会 ping-pong 死循环。
|
|
|
+ test('replayed request that still 401 fails without triggering another renewal', async () => {
|
|
|
+ global.uni._storage.openid = 'openid_abc'
|
|
|
+ global.uni._storage.token = 'expired-token'
|
|
|
+ mockUniLoginCode('fresh-wxcode')
|
|
|
+ mockSequence([
|
|
|
+ { data: { code: 401, message: 'Token无效或已过期', data: null } },
|
|
|
+ { data: { code: 200, data: { token: 'new-jwt', userId: 42, role: 'parent', familyId: 1, openid: 'openid_abc' } } },
|
|
|
+ { data: { code: 401, message: 'Token无效或已过期', data: null } }
|
|
|
+ ])
|
|
|
+
|
|
|
+ await expect(api.verifyToken()).rejects.toEqual({ code: 401, message: 'Token无效或已过期', data: null })
|
|
|
+
|
|
|
+ const calls = global.uni.request.mock.calls.map((c) => c[0])
|
|
|
+ // 只续期过一次,重放 401 不再触发第二次续期
|
|
|
+ expect(calls.filter((c) => /\/api\/auth\/silent-login/.test(c.url)).length).toBe(1)
|
|
|
+ expect(global.uni._storage.token).toBe('new-jwt')
|
|
|
+ })
|
|
|
+
|
|
|
test('401 retry does not duplicate query params in the replayed request URL', async () => {
|
|
|
global.uni._storage.openid = 'openid_abc'
|
|
|
global.uni._storage.token = 'expired-token'
|