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); }); });