mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-06 16:20:19 +01:00
📦 chore: npm audit bump (#12074)
* chore: npm audit - Bumped versions for several packages: `@hono/node-server` to 1.19.10, `@tootallnate/once` to 3.0.1, `hono` to 4.12.5, `serialize-javascript` to 7.0.4, and `svgo` to 2.8.2. - Removed deprecated `@trysound/sax` package from package-lock.json. - Updated integrity hashes and resolved URLs in package-lock.json to reflect the new versions. * chore: update dependencies and package versions - Bumped `jest-environment-jsdom` to version 30.2.0 in both package.json and client/package.json. - Updated related Jest packages to version 30.2.0 in package-lock.json, ensuring compatibility with the latest features and fixes. - Added `svgo` package with version 2.8.2 to package.json for improved SVG optimization. * chore: add @happy-dom/jest-environment and update test files - Added `@happy-dom/jest-environment` version 20.8.3 to `package.json` and `package-lock.json` for improved testing environment. - Updated test files to utilize the new Jest environment, replacing mock implementations of `window.location` with `window.history.replaceState` for better clarity and maintainability. - Refactored tests in `SourcesErrorBoundary`, `useFocusChatEffect`, `AuthContext`, and `StartupLayout` to enhance reliability and reduce complexity.
This commit is contained in:
parent
956f8fb6f0
commit
0ef369af9b
7 changed files with 566 additions and 1208 deletions
|
|
@ -122,6 +122,7 @@
|
|||
"@babel/preset-env": "^7.22.15",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.22.15",
|
||||
"@happy-dom/jest-environment": "^20.8.3",
|
||||
"@tanstack/react-query-devtools": "^4.29.0",
|
||||
"@testing-library/dom": "^9.3.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
|
|
@ -144,7 +145,7 @@
|
|||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"jest-file-loader": "^1.0.3",
|
||||
"jest-junit": "^16.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* @jest-environment @happy-dom/jest-environment
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
|
@ -11,15 +14,6 @@ const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => {
|
|||
return <div data-testid="normal-component">{'Normal component'}</div>;
|
||||
};
|
||||
|
||||
// Mock window.location.reload
|
||||
const mockReload = jest.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
reload: mockReload,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('SourcesErrorBoundary - NEW COMPONENT test', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -53,6 +47,8 @@ describe('SourcesErrorBoundary - NEW COMPONENT test', () => {
|
|||
});
|
||||
|
||||
it('should reload page when refresh button is clicked', () => {
|
||||
const reloadSpy = jest.spyOn(window.location, 'reload').mockImplementation(() => {});
|
||||
|
||||
render(
|
||||
<SourcesErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
|
|
@ -62,6 +58,6 @@ describe('SourcesErrorBoundary - NEW COMPONENT test', () => {
|
|||
const refreshButton = screen.getByRole('button', { name: 'Reload the page' });
|
||||
fireEvent.click(refreshButton);
|
||||
|
||||
expect(mockReload).toHaveBeenCalled();
|
||||
expect(reloadSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,14 +39,12 @@ describe('useFocusChatEffect', () => {
|
|||
state: { focusChat: true },
|
||||
});
|
||||
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: '',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
// Set default window.location
|
||||
window.history.replaceState({}, '', '/c/new');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
describe('Basic functionality', () => {
|
||||
|
|
@ -115,14 +113,7 @@ describe('useFocusChatEffect', () => {
|
|||
testDescription: string;
|
||||
}) => {
|
||||
test(`${testDescription}`, () => {
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: windowLocationSearch,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', `/c/new${windowLocationSearch}`);
|
||||
|
||||
// Mock React Router's location
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
|
|
@ -144,13 +135,7 @@ describe('useFocusChatEffect', () => {
|
|||
};
|
||||
|
||||
test('should use window.location.search instead of location.search', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: '?agent_id=test_agent_id',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new?agent_id=test_agent_id');
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/c/new',
|
||||
|
|
@ -223,13 +208,7 @@ describe('useFocusChatEffect', () => {
|
|||
});
|
||||
|
||||
test('should handle navigation immediately after URL parameter changes', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: '?endpoint=openAI&model=gpt-4',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new?endpoint=openAI&model=gpt-4');
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/c/new',
|
||||
|
|
@ -249,13 +228,7 @@ describe('useFocusChatEffect', () => {
|
|||
|
||||
jest.clearAllMocks();
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: '?agent_id=agent123',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/c/new_changed',
|
||||
|
|
@ -275,13 +248,7 @@ describe('useFocusChatEffect', () => {
|
|||
});
|
||||
|
||||
test('should handle undefined or null search params gracefully', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: undefined,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new');
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/c/new',
|
||||
|
|
@ -301,14 +268,6 @@ describe('useFocusChatEffect', () => {
|
|||
|
||||
jest.clearAllMocks();
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: null,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/c/new',
|
||||
search: null,
|
||||
|
|
@ -327,13 +286,7 @@ describe('useFocusChatEffect', () => {
|
|||
});
|
||||
|
||||
test('should handle navigation when location.state is null', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: '?agent_id=agent123',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/c/new',
|
||||
|
|
@ -348,13 +301,7 @@ describe('useFocusChatEffect', () => {
|
|||
});
|
||||
|
||||
test('should handle navigation when location.state.focusChat is undefined', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: '?agent_id=agent123',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/c/new',
|
||||
|
|
@ -369,13 +316,7 @@ describe('useFocusChatEffect', () => {
|
|||
});
|
||||
|
||||
test('should handle navigation when both search params are empty', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
pathname: '/c/new',
|
||||
search: '',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new');
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/c/new',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* @jest-environment @happy-dom/jest-environment
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, act } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
|
@ -105,31 +108,17 @@ function renderProviderLive() {
|
|||
}
|
||||
|
||||
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,
|
||||
});
|
||||
window.history.replaceState({}, '', '/login');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
window.history.replaceState({}, '', '/login?redirect_to=%2Fc%2Fabc123');
|
||||
|
||||
renderProvider();
|
||||
|
||||
|
|
@ -143,11 +132,7 @@ describe('AuthContextProvider — login onError redirect handling', () => {
|
|||
});
|
||||
|
||||
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,
|
||||
});
|
||||
window.history.replaceState({}, '', '/login?redirect_to=https%3A%2F%2Fevil.com');
|
||||
|
||||
renderProvider();
|
||||
|
||||
|
|
@ -159,11 +144,7 @@ describe('AuthContextProvider — login onError redirect handling', () => {
|
|||
});
|
||||
|
||||
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,
|
||||
});
|
||||
window.history.replaceState({}, '', '/login?redirect_to=%2Flogin');
|
||||
|
||||
renderProvider();
|
||||
|
||||
|
|
@ -186,15 +167,7 @@ describe('AuthContextProvider — login onError redirect handling', () => {
|
|||
|
||||
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,
|
||||
});
|
||||
window.history.replaceState({}, '', `/login?redirect_to=${encodeURIComponent(target)}`);
|
||||
|
||||
renderProvider();
|
||||
|
||||
|
|
@ -209,33 +182,20 @@ describe('AuthContextProvider — login onError redirect handling', () => {
|
|||
});
|
||||
|
||||
describe('AuthContextProvider — logout onSuccess/onError handling', () => {
|
||||
const originalLocation = window.location;
|
||||
const mockSetTokenHeader = jest.requireMock('librechat-data-provider').setTokenHeader;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...originalLocation,
|
||||
pathname: '/c/some-chat',
|
||||
search: '',
|
||||
hash: '',
|
||||
replace: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/some-chat');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
it('calls window.location.replace and setTokenHeader(undefined) when redirect is present', () => {
|
||||
const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {});
|
||||
|
||||
renderProvider();
|
||||
|
||||
act(() => {
|
||||
|
|
@ -245,23 +205,25 @@ describe('AuthContextProvider — logout onSuccess/onError handling', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
'https://idp.example.com/logout?id_token_hint=abc',
|
||||
);
|
||||
expect(replaceSpy).toHaveBeenCalledWith('https://idp.example.com/logout?id_token_hint=abc');
|
||||
expect(mockSetTokenHeader).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('does not call window.location.replace when redirect is absent', async () => {
|
||||
const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {});
|
||||
|
||||
renderProvider();
|
||||
|
||||
act(() => {
|
||||
mockCapturedLogoutOptions.onSuccess({ message: 'Logout successful' });
|
||||
});
|
||||
|
||||
expect(window.location.replace).not.toHaveBeenCalled();
|
||||
expect(replaceSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not trigger silentRefresh after OIDC redirect', () => {
|
||||
const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {});
|
||||
|
||||
renderProviderLive();
|
||||
mockRefreshMutate.mockClear();
|
||||
|
||||
|
|
@ -272,7 +234,7 @@ describe('AuthContextProvider — logout onSuccess/onError handling', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(window.location.replace).toHaveBeenCalled();
|
||||
expect(replaceSpy).toHaveBeenCalled();
|
||||
expect(mockRefreshMutate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -285,6 +247,7 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
|
|||
|
||||
afterEach(() => {
|
||||
sessionStorage.clear();
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
it('navigates to stored sessionStorage redirect after successful token refresh', () => {
|
||||
|
|
@ -315,10 +278,7 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
|
|||
|
||||
it('navigates to current URL when no stored redirect exists', () => {
|
||||
jest.useFakeTimers();
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...window.location, pathname: '/c/new', search: '' },
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new');
|
||||
|
||||
renderProviderLive();
|
||||
|
||||
|
|
@ -367,10 +327,7 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
|
|||
|
||||
it('falls back to current URL for unsafe stored redirect', () => {
|
||||
jest.useFakeTimers();
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...window.location, pathname: '/c/new', search: '' },
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/new');
|
||||
sessionStorage.setItem(SESSION_KEY, 'https://evil.com/steal');
|
||||
|
||||
renderProviderLive();
|
||||
|
|
@ -396,33 +353,18 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
|
|||
});
|
||||
|
||||
describe('AuthContextProvider — logout error handling', () => {
|
||||
const originalLocation = window.location;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...originalLocation,
|
||||
pathname: '/c/some-chat',
|
||||
search: '',
|
||||
hash: '',
|
||||
replace: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/c/some-chat');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
it('clears auth state on logout error without external redirect', () => {
|
||||
jest.useFakeTimers();
|
||||
const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {});
|
||||
const { getByTestId } = renderProvider();
|
||||
|
||||
act(() => {
|
||||
|
|
@ -432,7 +374,7 @@ describe('AuthContextProvider — logout error handling', () => {
|
|||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
|
||||
expect(window.location.replace).not.toHaveBeenCalled();
|
||||
expect(replaceSpy).not.toHaveBeenCalled();
|
||||
expect(getByTestId('consumer').getAttribute('data-authenticated')).toBe('false');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,22 +58,17 @@ const createTestRouter = (initialEntry: string, isAuthenticated: boolean) =>
|
|||
);
|
||||
|
||||
describe('StartupLayout — redirect race condition', () => {
|
||||
const originalLocation = window.location;
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'location', { value: originalLocation, writable: true });
|
||||
window.history.replaceState({}, '', '/');
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('navigates to /c/new when authenticated with no pending redirect', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, search: '' },
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/login');
|
||||
|
||||
const router = createTestRouter('/login', true);
|
||||
render(<RouterProvider router={router} />);
|
||||
|
|
@ -84,10 +79,7 @@ describe('StartupLayout — redirect race condition', () => {
|
|||
});
|
||||
|
||||
it('does NOT navigate to /c/new when redirect_to URL param is present', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, search: '?redirect_to=%2Fc%2Fabc123' },
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/login?redirect_to=%2Fc%2Fabc123');
|
||||
|
||||
const router = createTestRouter('/login?redirect_to=%2Fc%2Fabc123', true);
|
||||
render(<RouterProvider router={router} />);
|
||||
|
|
@ -98,10 +90,7 @@ describe('StartupLayout — redirect race condition', () => {
|
|||
});
|
||||
|
||||
it('does NOT navigate to /c/new when sessionStorage redirect is present', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, search: '' },
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/login');
|
||||
sessionStorage.setItem(SESSION_KEY, '/c/abc123');
|
||||
|
||||
const router = createTestRouter('/login', true);
|
||||
|
|
@ -113,10 +102,7 @@ describe('StartupLayout — redirect race condition', () => {
|
|||
});
|
||||
|
||||
it('does NOT navigate when not authenticated', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, search: '' },
|
||||
writable: true,
|
||||
});
|
||||
window.history.replaceState({}, '', '/login');
|
||||
|
||||
const router = createTestRouter('/login', false);
|
||||
render(<RouterProvider router={router} />);
|
||||
|
|
|
|||
1526
package-lock.json
generated
1526
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -169,7 +169,11 @@
|
|||
"eslint": {
|
||||
"ajv": "6.14.0"
|
||||
},
|
||||
"underscore": "1.13.8"
|
||||
"underscore": "1.13.8",
|
||||
"hono": "^4.12.4",
|
||||
"@hono/node-server": "^1.19.10",
|
||||
"serialize-javascript": "^7.0.3",
|
||||
"svgo": "^2.8.2"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue