Skip to content

Commit 0a386f9

Browse files
committed
test(e2e): add token refresh mechanism test cases (Refs: 需求 3)
- Add TC0234a: single 401 request auto refresh and retry - Add TC0234b: concurrent 401 requests only trigger one refresh (deduplication) - Add TC0234c: refresh token failure redirects to login page
1 parent e8777dc commit 0a386f9

1 file changed

Lines changed: 272 additions & 0 deletions

File tree

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import { test, expect } from '../../fixtures/auth';
2+
3+
test.describe('TC0234 Token 自动刷新机制', () => {
4+
test('TC0234a: 单请求 401 自动刷新成功重放', async ({
5+
page,
6+
}) => {
7+
let refreshRequestCount = 0;
8+
let apiRequestCount = 0;
9+
let apiRetryAuthorization = '';
10+
11+
// Mock refresh token 接口 - 第一次调用返回新的 token
12+
await page.route('**/api/v1/auth/refresh', async (route) => {
13+
refreshRequestCount += 1;
14+
await route.fulfill({
15+
contentType: 'application/json',
16+
status: 200,
17+
body: JSON.stringify({
18+
code: 0,
19+
data: {
20+
accessToken: 'new-access-token-after-refresh',
21+
refreshToken: 'new-refresh-token',
22+
},
23+
message: 'ok',
24+
}),
25+
});
26+
});
27+
28+
// Mock 业务 API - 第一次返回 401,刷新后第二次返回成功
29+
await page.route('**/api/v1/user/info', async (route) => {
30+
apiRequestCount += 1;
31+
if (apiRequestCount === 1) {
32+
await route.fulfill({
33+
contentType: 'application/json',
34+
status: 401,
35+
body: JSON.stringify({
36+
code: 401,
37+
message: 'Unauthorized',
38+
}),
39+
});
40+
return;
41+
}
42+
// 记录重试请求使用的 Authorization header
43+
apiRetryAuthorization =
44+
route.request().headers().authorization ?? '';
45+
await route.fulfill({
46+
contentType: 'application/json',
47+
status: 200,
48+
body: JSON.stringify({
49+
code: 0,
50+
data: {
51+
avatar: '',
52+
homePath: '/dashboard/analytics',
53+
permissions: ['*'],
54+
realName: 'Admin',
55+
roles: ['admin'],
56+
userId: 1,
57+
username: 'admin',
58+
},
59+
message: 'ok',
60+
}),
61+
});
62+
});
63+
64+
// 设置过期的 token
65+
await page.addInitScript(() => {
66+
localStorage.clear();
67+
const expiredAccessState = JSON.stringify({
68+
accessCodes: ['*'],
69+
accessToken: 'expired-access-token',
70+
isLockScreen: false,
71+
refreshToken: 'valid-refresh-token',
72+
});
73+
for (const envName of ['dev', 'prod']) {
74+
localStorage.setItem(
75+
`lina-web-antd-5.6.0-${envName}-core-access`,
76+
expiredAccessState,
77+
);
78+
}
79+
});
80+
81+
await page.goto('/dashboard/analytics');
82+
83+
// 验证 refresh 接口只被调用了一次
84+
await expect
85+
.poll(() => refreshRequestCount, { timeout: 15_000 })
86+
.toBe(1);
87+
88+
// 验证 API 被调用了至少两次(第一次 401,第二次重试成功)
89+
await expect
90+
.poll(() => apiRequestCount, { timeout: 15_000 })
91+
.toBeGreaterThanOrEqual(2);
92+
93+
// 验证重试请求使用了新的 token
94+
expect(apiRetryAuthorization).toBe('Bearer new-access-token-after-refresh');
95+
96+
// 验证页面正常加载(没有跳转到登录页)
97+
await expect(page).not.toHaveURL(/auth\/login/);
98+
});
99+
100+
test('TC0234b: 多请求并发 401 仅触发一次刷新', async ({
101+
page,
102+
}) => {
103+
let refreshRequestCount = 0;
104+
let api1RequestCount = 0;
105+
let api2RequestCount = 0;
106+
107+
// Mock refresh token 接口
108+
await page.route('**/api/v1/auth/refresh', async (route) => {
109+
refreshRequestCount += 1;
110+
// 模拟网络延迟,让多个请求有机会同时到达
111+
await new Promise((resolve) => setTimeout(resolve, 500));
112+
await route.fulfill({
113+
contentType: 'application/json',
114+
status: 200,
115+
body: JSON.stringify({
116+
code: 0,
117+
data: {
118+
accessToken: 'new-access-token-concurrent',
119+
refreshToken: 'new-refresh-token',
120+
},
121+
message: 'ok',
122+
}),
123+
});
124+
});
125+
126+
// Mock 两个并发的业务的 API
127+
await page.route('**/api/v1/user/info', async (route) => {
128+
api1RequestCount += 1;
129+
if (api1RequestCount === 1) {
130+
await route.fulfill({
131+
contentType: 'application/json',
132+
status: 401,
133+
body: JSON.stringify({ code: 401, message: 'Unauthorized' }),
134+
});
135+
return;
136+
}
137+
await route.fulfill({
138+
contentType: 'application/json',
139+
status: 200,
140+
body: JSON.stringify({
141+
code: 0,
142+
data: {
143+
avatar: '',
144+
homePath: '/dashboard/analytics',
145+
permissions: ['*'],
146+
realName: 'Admin',
147+
roles: ['admin'],
148+
userId: 1,
149+
username: 'admin',
150+
},
151+
}),
152+
});
153+
});
154+
155+
await page.route('**/api/v1/menus/all', async (route) => {
156+
api2RequestCount += 1;
157+
if (api2RequestCount === 1) {
158+
await route.fulfill({
159+
contentType: 'application/json',
160+
status: 401,
161+
body: JSON.stringify({ code: 401, message: 'Unauthorized' }),
162+
});
163+
return;
164+
}
165+
await route.fulfill({
166+
contentType: 'application/json',
167+
status: 200,
168+
body: JSON.stringify({
169+
code: 0,
170+
data: { list: [] },
171+
}),
172+
});
173+
});
174+
175+
// 设置过期的 token
176+
await page.addInitScript(() => {
177+
localStorage.clear();
178+
const expiredAccessState = JSON.stringify({
179+
accessCodes: ['*'],
180+
accessToken: 'expired-access-token',
181+
isLockScreen: false,
182+
refreshToken: 'valid-refresh-token',
183+
});
184+
for (const envName of ['dev', 'prod']) {
185+
localStorage.setItem(
186+
`lina-web-antd-5.6.0-${envName}-core-access`,
187+
expiredAccessState,
188+
);
189+
}
190+
});
191+
192+
await page.goto('/dashboard/analytics');
193+
194+
// 等待所有请求完成
195+
await page.waitForTimeout(3000);
196+
197+
// 关键验证:refresh 接口只被调用了一次(并发去重)
198+
expect(refreshRequestCount).toBe(1);
199+
200+
// 验证两个 API 都被调用了至少两次(第一次 401,第二次重试)
201+
expect(api1RequestCount).toBeGreaterThanOrEqual(2);
202+
expect(api2RequestCount).toBeGreaterThanOrEqual(2);
203+
});
204+
205+
test('TC0234c: Refresh Token 失效跳转登录页', async ({
206+
page,
207+
}) => {
208+
let refreshRequestCount = 0;
209+
let logoutDetected = false;
210+
211+
// Mock refresh token 接口 - 返回失败(refresh token 失效)
212+
await page.route('**/api/v1/auth/refresh', async (route) => {
213+
refreshRequestCount += 1;
214+
await route.fulfill({
215+
contentType: 'application/json',
216+
status: 200,
217+
body: JSON.stringify({
218+
code: 401,
219+
message: 'Refresh token invalid or expired',
220+
messageKey: 'error.auth.token.refreshFailed',
221+
}),
222+
});
223+
});
224+
225+
// Mock 业务 API - 返回 401
226+
await page.route('**/api/v1/user/info', async (route) => {
227+
await route.fulfill({
228+
contentType: 'application/json',
229+
status: 401,
230+
body: JSON.stringify({ code: 401, message: 'Unauthorized' }),
231+
});
232+
});
233+
234+
// 监听是否跳转到登录页
235+
page.on('framenavigated', (frame) => {
236+
if (frame.url().includes('/auth/login')) {
237+
logoutDetected = true;
238+
}
239+
});
240+
241+
// 设置过期的 access token 和无效的 refresh token
242+
await page.addInitScript(() => {
243+
localStorage.clear();
244+
const expiredAccessState = JSON.stringify({
245+
accessCodes: ['*'],
246+
accessToken: 'expired-access-token',
247+
isLockScreen: false,
248+
refreshToken: 'invalid-refresh-token',
249+
});
250+
for (const envName of ['dev', 'prod']) {
251+
localStorage.setItem(
252+
`lina-web-antd-5.6.0-${envName}-core-access`,
253+
expiredAccessState,
254+
);
255+
}
256+
});
257+
258+
await page.goto('/dashboard/analytics');
259+
260+
// 等待 refresh 请求完成
261+
await expect
262+
.poll(() => refreshRequestCount, { timeout: 15_000 })
263+
.toBe(1);
264+
265+
// 等待页面跳转到登录页
266+
await page.waitForURL(/auth\/login/, { timeout: 15_000 });
267+
268+
// 验证确实跳转到了登录页
269+
expect(page.url()).toContain('/auth/login');
270+
expect(logoutDetected).toBe(true);
271+
});
272+
});

0 commit comments

Comments
 (0)