mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-03 23:00:18 +01:00
103 lines
2.9 KiB
TypeScript
103 lines
2.9 KiB
TypeScript
|
|
/**
|
||
|
|
* @jest-environment jsdom
|
||
|
|
*/
|
||
|
|
import axios from 'axios';
|
||
|
|
import { setTokenHeader } from '../src/headers-helpers';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The response interceptor in request.ts registers at import time when
|
||
|
|
* `typeof window !== 'undefined'` (jsdom provides window).
|
||
|
|
*
|
||
|
|
* We use axios's built-in request adapter mock to avoid real HTTP calls,
|
||
|
|
* and verify the interceptor's behavior by observing whether a 401 triggers
|
||
|
|
* a refresh POST or is immediately rejected.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** Mock the axios adapter to simulate responses without HTTP */
|
||
|
|
const mockAdapter = jest.fn();
|
||
|
|
let originalAdapter: typeof axios.defaults.adapter;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
originalAdapter = axios.defaults.adapter;
|
||
|
|
axios.defaults.adapter = mockAdapter;
|
||
|
|
|
||
|
|
/** Import triggers interceptor registration */
|
||
|
|
await import('../src/request');
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
mockAdapter.mockReset();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(() => {
|
||
|
|
axios.defaults.adapter = originalAdapter;
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
delete axios.defaults.headers.common['Authorization'];
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('axios 401 interceptor — Authorization header guard', () => {
|
||
|
|
it('skips refresh and rejects when Authorization header is cleared', async () => {
|
||
|
|
/** Simulate a cleared header (as done by setTokenHeader(undefined) during logout) */
|
||
|
|
setTokenHeader(undefined);
|
||
|
|
|
||
|
|
/** Set up adapter: first call returns 401, second would be the refresh */
|
||
|
|
mockAdapter.mockRejectedValueOnce({
|
||
|
|
response: { status: 401 },
|
||
|
|
config: { url: '/api/messages', headers: {} },
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
await axios.get('/api/messages');
|
||
|
|
} catch {
|
||
|
|
// expected rejection
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* If the interceptor skipped refresh, only 1 call was made (the original).
|
||
|
|
* If it attempted refresh, there would be 2+ calls (original + refresh POST).
|
||
|
|
*/
|
||
|
|
expect(mockAdapter).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('attempts refresh when Authorization header is present', async () => {
|
||
|
|
setTokenHeader('valid-token');
|
||
|
|
|
||
|
|
/** First call: 401 on the original request */
|
||
|
|
mockAdapter.mockRejectedValueOnce({
|
||
|
|
response: { status: 401 },
|
||
|
|
config: { url: '/api/messages', headers: {}, _retry: false },
|
||
|
|
});
|
||
|
|
|
||
|
|
/** Second call: the refresh endpoint succeeds */
|
||
|
|
mockAdapter.mockResolvedValueOnce({
|
||
|
|
data: { token: 'new-token' },
|
||
|
|
status: 200,
|
||
|
|
headers: {},
|
||
|
|
config: {},
|
||
|
|
});
|
||
|
|
|
||
|
|
/** Third call: retried original request succeeds */
|
||
|
|
mockAdapter.mockResolvedValueOnce({
|
||
|
|
data: { messages: [] },
|
||
|
|
status: 200,
|
||
|
|
headers: {},
|
||
|
|
config: {},
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
await axios.get('/api/messages');
|
||
|
|
} catch {
|
||
|
|
// may reject depending on exact flow
|
||
|
|
}
|
||
|
|
|
||
|
|
/** More than 1 call means the interceptor attempted refresh */
|
||
|
|
expect(mockAdapter.mock.calls.length).toBeGreaterThan(1);
|
||
|
|
|
||
|
|
/** Verify the second call targeted the refresh endpoint */
|
||
|
|
const refreshCall = mockAdapter.mock.calls[1];
|
||
|
|
expect(refreshCall[0].url).toContain('api/auth/refresh');
|
||
|
|
});
|
||
|
|
});
|