From 1901a6e35b6e09d1b9167effd2698db12760bb17 Mon Sep 17 00:00:00 2001 From: oceans Date: Sun, 20 Sep 2026 23:14:36 +0800 Subject: [PATCH] [#13356] fix(web-v2): Read the refreshed OAuth token from the response body `loginApi` resolves to the response body, because the axios wrapper runs with `isTransformResponse: false`, so `refreshToken` reading `res.data` got `undefined` and the destructuring threw `TypeError: Cannot read properties of undefined (reading 'access_token')`. The scheduled refresh therefore never stored a new token, and the next request after expiry returned 401 and sent the user back to the login page. Read the token from `res`, as `loginAction` already does, and cover the refresh flow with a regression test. Generated-by: ZCode agent (deepseek-v4.1-flash model) --- web-v2/web/src/lib/store/auth/index.js | 2 +- web-v2/web/src/lib/store/auth/index.test.js | 44 ++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/web-v2/web/src/lib/store/auth/index.js b/web-v2/web/src/lib/store/auth/index.js index ef5a1fd57bd..7090dce7a08 100644 --- a/web-v2/web/src/lib/store/auth/index.js +++ b/web-v2/web/src/lib/store/auth/index.js @@ -78,7 +78,7 @@ export const refreshToken = createAsyncThunk('auth/refreshToken', async (data, { throw new Error(err) } - const { access_token, expires_in } = res.data + const { access_token, expires_in } = res // `expires_in ` is in seconds, default is 499 return { token: access_token, expiredIn: expires_in } }) diff --git a/web-v2/web/src/lib/store/auth/index.test.js b/web-v2/web/src/lib/store/auth/index.test.js index eef5ca58d02..585f9357dbb 100644 --- a/web-v2/web/src/lib/store/auth/index.test.js +++ b/web-v2/web/src/lib/store/auth/index.test.js @@ -17,8 +17,25 @@ * under the License. */ -import { beforeEach, describe, expect, it } from 'vitest' -import authReducer, { getAuthMe, setAuthUser } from '@/lib/store/auth' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { configureStore } from '@reduxjs/toolkit' + +import { loginApi } from '@/lib/api/auth' +import authReducer, { getAuthMe, refreshToken, setAuthUser } from '@/lib/store/auth' + +// The store reaches `@/lib/utils/axios`, which imports the JSX provider in +// `@/lib/provider/session.js`. Mock the request layer so these tests stay +// independent of it. +vi.mock('@/lib/api/auth', () => ({ + getAuthConfigsApi: vi.fn(), + getAuthMeApi: vi.fn(), + loginApi: vi.fn(), + basicLoginApi: vi.fn() +})) + +vi.mock('@/lib/store/sys', () => ({ + initialVersion: vi.fn(() => ({ type: 'sys/initialVersion/mock' })) +})) describe('auth store', () => { beforeEach(() => { @@ -51,3 +68,26 @@ describe('auth store', () => { expect(state.isServiceAdmin).toBe(false) }) }) + +describe('refreshToken', () => { + beforeEach(() => { + localStorage.clear() + sessionStorage.clear() + vi.clearAllMocks() + }) + + it('reads the token from the response body, as the login flow does', async () => { + // `loginApi` resolves to the response body, not to the Axios envelope. + loginApi.mockResolvedValue({ access_token: 'refreshed-token', expires_in: 499 }) + + const store = configureStore({ reducer: { auth: authReducer } }) + + const action = await store.dispatch(refreshToken()) + + expect(action.type).toBe(refreshToken.fulfilled.type) + expect(action.payload).toEqual({ token: 'refreshed-token', expiredIn: 499 }) + expect(store.getState().auth.authToken).toBe('refreshed-token') + expect(localStorage.getItem('accessToken')).toBe('refreshed-token') + expect(localStorage.getItem('expiredIn')).toBe('499') + }) +})