mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-26 12:24:10 +01:00
🧭 fix: Add Base Path Support for Login/Register and Image Paths (#10116)
* fix: add basePath pattern to support login/register and image paths * Fix linter errors * refactor: Update import statements for getBasePath and isEnabled, and add path utility functions with tests - Refactored imports in addImages.js and StableDiffusion.js to use getBasePath from '@librechat/api'. - Consolidated isEnabled and getBasePath imports in validateImageRequest.js. - Introduced new path utility functions in path.ts and corresponding unit tests in path.spec.ts to validate base path extraction logic. * fix: Update domain server base URL in MarkdownComponents and refactor authentication redirection logic - Changed the domain server base URL in MarkdownComponents.tsx to use the API base URL. - Refactored the useAuthRedirect hook to utilize React Router's navigate for redirection instead of window.location, ensuring a smoother SPA experience. - Added unit tests for the useAuthRedirect hook to verify authentication redirection behavior. * test: Mock isEnabled in validateImages.spec.js for improved test isolation - Updated validateImages.spec.js to mock the isEnabled function from @librechat/api, ensuring that tests can run independently of the actual implementation. - Cleared the DOMAIN_CLIENT environment variable before tests to avoid interference with basePath resolution. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
ef3bf0a932
commit
7aa8d49f3a
21 changed files with 717 additions and 30 deletions
202
client/src/routes/__tests__/useAuthRedirect.spec.tsx
Normal file
202
client/src/routes/__tests__/useAuthRedirect.spec.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/* eslint-disable i18next/no-literal-string */
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
|
||||
import useAuthRedirect from '../useAuthRedirect';
|
||||
import { useAuthContext } from '~/hooks';
|
||||
|
||||
// Polyfill Request for React Router in test environment
|
||||
if (typeof Request === 'undefined') {
|
||||
global.Request = class Request {
|
||||
constructor(
|
||||
public url: string,
|
||||
public init?: RequestInit,
|
||||
) {}
|
||||
} as any;
|
||||
}
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useAuthContext: jest.fn(),
|
||||
}));
|
||||
|
||||
/**
|
||||
* TestComponent that uses the useAuthRedirect hook and exposes its return value
|
||||
*/
|
||||
function TestComponent() {
|
||||
const result = useAuthRedirect();
|
||||
// Expose result for assertions
|
||||
(window as any).__testResult = result;
|
||||
return <div data-testid="test-component">Test Component</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test router with optional basename to verify navigation works correctly
|
||||
* with subdirectory deployments (e.g., /librechat)
|
||||
*/
|
||||
const createTestRouter = (basename = '/') => {
|
||||
// When using basename, initialEntries must include the basename
|
||||
const initialEntry = basename === '/' ? '/' : `${basename}/`;
|
||||
|
||||
return createMemoryRouter(
|
||||
[
|
||||
{
|
||||
path: '/',
|
||||
element: <TestComponent />,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <div data-testid="login-page">Login Page</div>,
|
||||
},
|
||||
],
|
||||
{
|
||||
basename,
|
||||
initialEntries: [initialEntry],
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe('useAuthRedirect', () => {
|
||||
beforeEach(() => {
|
||||
(window as any).__testResult = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(window as any).__testResult = undefined;
|
||||
});
|
||||
|
||||
it('should not redirect when user is authenticated', async () => {
|
||||
(useAuthContext as jest.Mock).mockReturnValue({
|
||||
user: { id: '123', email: 'test@example.com' },
|
||||
isAuthenticated: true,
|
||||
});
|
||||
|
||||
const router = createTestRouter();
|
||||
const { getByTestId } = render(<RouterProvider router={router} />);
|
||||
|
||||
expect(router.state.location.pathname).toBe('/');
|
||||
expect(getByTestId('test-component')).toBeInTheDocument();
|
||||
|
||||
// Wait for the timeout (300ms) plus a buffer
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
|
||||
// Should still be on home page, not redirected
|
||||
expect(router.state.location.pathname).toBe('/');
|
||||
expect(getByTestId('test-component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should redirect to /login when user is not authenticated', async () => {
|
||||
(useAuthContext as jest.Mock).mockReturnValue({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
});
|
||||
|
||||
const router = createTestRouter();
|
||||
const { getByTestId, queryByTestId } = render(<RouterProvider router={router} />);
|
||||
|
||||
expect(router.state.location.pathname).toBe('/');
|
||||
expect(getByTestId('test-component')).toBeInTheDocument();
|
||||
|
||||
// Wait for the redirect to happen (300ms timeout + navigation)
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(router.state.location.pathname).toBe('/login');
|
||||
expect(getByTestId('login-page')).toBeInTheDocument();
|
||||
expect(queryByTestId('test-component')).not.toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
|
||||
// Verify navigation used replace (history has only 1 entry)
|
||||
// This prevents users from hitting back to return to protected pages
|
||||
expect(router.state.historyAction).toBe('REPLACE');
|
||||
});
|
||||
|
||||
it('should respect router basename when redirecting (subdirectory deployment)', async () => {
|
||||
(useAuthContext as jest.Mock).mockReturnValue({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
});
|
||||
|
||||
// Test with basename="/librechat" (simulates subdirectory deployment)
|
||||
const router = createTestRouter('/librechat');
|
||||
const { getByTestId } = render(<RouterProvider router={router} />);
|
||||
|
||||
// Full pathname includes basename
|
||||
expect(router.state.location.pathname).toBe('/librechat/');
|
||||
|
||||
// Wait for the redirect - router handles basename internally
|
||||
await waitFor(
|
||||
() => {
|
||||
// Router state pathname includes the full path with basename
|
||||
expect(router.state.location.pathname).toBe('/librechat/login');
|
||||
expect(getByTestId('login-page')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
|
||||
// The key point: navigate('/login', { replace: true }) works correctly with basename
|
||||
// The router automatically prepends the basename to create the full URL
|
||||
expect(router.state.historyAction).toBe('REPLACE');
|
||||
});
|
||||
|
||||
it('should use React Router navigate (not window.location) for SPA experience', async () => {
|
||||
(useAuthContext as jest.Mock).mockReturnValue({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
});
|
||||
|
||||
const router = createTestRouter('/librechat');
|
||||
const { getByTestId } = render(<RouterProvider router={router} />);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(router.state.location.pathname).toBe('/librechat/login');
|
||||
expect(getByTestId('login-page')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
|
||||
// The fact that navigation worked within the router proves we're using
|
||||
// navigate() and not window.location.href (which would cause a full reload
|
||||
// and break the test entirely). This maintains the SPA experience.
|
||||
expect(router.state.location.pathname).toBe('/librechat/login');
|
||||
});
|
||||
|
||||
it('should clear timeout on unmount', async () => {
|
||||
(useAuthContext as jest.Mock).mockReturnValue({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
});
|
||||
|
||||
const router = createTestRouter();
|
||||
const { unmount } = render(<RouterProvider router={router} />);
|
||||
|
||||
// Unmount immediately before timeout fires
|
||||
unmount();
|
||||
|
||||
// Wait past the timeout period
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
|
||||
// Should still be at home, not redirected (timeout was cleared)
|
||||
expect(router.state.location.pathname).toBe('/');
|
||||
});
|
||||
|
||||
it('should return user and isAuthenticated values', async () => {
|
||||
const mockUser = { id: '123', email: 'test@example.com' };
|
||||
(useAuthContext as jest.Mock).mockReturnValue({
|
||||
user: mockUser,
|
||||
isAuthenticated: true,
|
||||
});
|
||||
|
||||
const router = createTestRouter();
|
||||
render(<RouterProvider router={router} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const testResult = (window as any).__testResult;
|
||||
expect(testResult).toBeDefined();
|
||||
expect(testResult.user).toEqual(mockUser);
|
||||
expect(testResult.isAuthenticated).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue