This commit is contained in:
2026-05-09 19:56:03 +08:00
parent 052dbc248b
commit 7c8aa1e124
12 changed files with 483 additions and 59 deletions

View File

@@ -8,6 +8,7 @@ import {
fetchWithApiAuth,
getStoredAccessToken,
isTimeoutError,
refreshStoredAccessToken,
requestJson,
setStoredAccessToken,
} from './apiClient';
@@ -312,6 +313,27 @@ describe('apiClient', () => {
expect(getStoredAccessToken()).toBe('still-valid-token');
});
it('keeps local token when explicit refresh opts out of clearing on failure', async () => {
setStoredAccessToken('usable-local-token', { emit: false });
fetchMock.mockResolvedValueOnce(createResponseMock({ status: 401 }));
await expect(
refreshStoredAccessToken({ clearOnFailure: false }),
).rejects.toMatchObject({
status: 401,
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/auth/refresh',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
}),
);
expect(dispatchEventMock).not.toHaveBeenCalled();
expect(getStoredAccessToken()).toBe('usable-local-token');
});
it('keeps the refreshed token when the retried protected request is still unauthorized', async () => {
setStoredAccessToken('expired-token', { emit: false });
fetchMock

View File

@@ -548,11 +548,17 @@ export async function ensureStoredAccessToken() {
return refreshAccessToken();
}
export async function refreshStoredAccessToken() {
export async function refreshStoredAccessToken(
options: {
clearOnFailure?: boolean;
} = {},
) {
try {
return await refreshAccessToken();
} catch (error) {
clearStoredAccessToken({ emit: false });
if (options.clearOnFailure !== false) {
clearStoredAccessToken({ emit: false });
}
throw error;
}
}