import React from 'react';
import { render, act } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import type { TAuthConfig } from '~/common';
import { AuthContextProvider, useAuthContext } from '../AuthContext';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
jest.mock('librechat-data-provider', () => ({
...jest.requireActual('librechat-data-provider'),
setTokenHeader: jest.fn(),
}));
let mockCapturedLoginOptions: {
onSuccess: (...args: unknown[]) => void;
onError: (...args: unknown[]) => void;
};
jest.mock('~/data-provider', () => ({
useLoginUserMutation: jest.fn(
(options: {
onSuccess: (...args: unknown[]) => void;
onError: (...args: unknown[]) => void;
}) => {
mockCapturedLoginOptions = options;
return { mutate: jest.fn() };
},
),
useLogoutUserMutation: jest.fn(() => ({ mutate: jest.fn() })),
useRefreshTokenMutation: jest.fn(() => ({ mutate: jest.fn() })),
useGetUserQuery: jest.fn(() => ({
data: undefined,
isError: false,
error: null,
})),
useGetRole: jest.fn(() => ({ data: null })),
}));
const authConfig: TAuthConfig = { loginRedirect: '/login', test: true };
function TestConsumer() {
const ctx = useAuthContext();
return
;
}
function renderProvider() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return render(
,
);
}
describe('AuthContextProvider — login onError redirect handling', () => {
const originalLocation = window.location;
beforeEach(() => {
jest.clearAllMocks();
Object.defineProperty(window, 'location', {
value: { ...originalLocation, pathname: '/login', search: '', hash: '' },
writable: true,
configurable: true,
});
});
afterEach(() => {
Object.defineProperty(window, 'location', {
value: originalLocation,
writable: true,
configurable: true,
});
});
it('preserves a valid redirect_to param across login failure', () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/login', search: '?redirect_to=%2Fc%2Fabc123', hash: '' },
writable: true,
configurable: true,
});
renderProvider();
act(() => {
mockCapturedLoginOptions.onError({ message: 'Invalid credentials' });
});
expect(mockNavigate).toHaveBeenCalledWith('/login?redirect_to=%2Fc%2Fabc123', {
replace: true,
});
});
it('drops redirect_to when it contains an absolute URL (open-redirect prevention)', () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/login', search: '?redirect_to=https%3A%2F%2Fevil.com', hash: '' },
writable: true,
configurable: true,
});
renderProvider();
act(() => {
mockCapturedLoginOptions.onError({ message: 'Invalid credentials' });
});
expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true });
});
it('drops redirect_to when it points to /login (recursive redirect prevention)', () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/login', search: '?redirect_to=%2Flogin', hash: '' },
writable: true,
configurable: true,
});
renderProvider();
act(() => {
mockCapturedLoginOptions.onError({ message: 'Invalid credentials' });
});
expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true });
});
it('navigates to plain /login when no redirect_to param exists', () => {
renderProvider();
act(() => {
mockCapturedLoginOptions.onError({ message: 'Server error' });
});
expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true });
});
it('preserves redirect_to with query params and hash', () => {
const target = '/c/abc123?model=gpt-4#section';
Object.defineProperty(window, 'location', {
value: {
pathname: '/login',
search: `?redirect_to=${encodeURIComponent(target)}`,
hash: '',
},
writable: true,
configurable: true,
});
renderProvider();
act(() => {
mockCapturedLoginOptions.onError({ message: 'Invalid credentials' });
});
const navigatedUrl = mockNavigate.mock.calls[0][0] as string;
const params = new URLSearchParams(navigatedUrl.split('?')[1]);
expect(decodeURIComponent(params.get('redirect_to')!)).toBe(target);
});
});