mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-10 00:04:48 -07:00
harden
This commit is contained in:
parent
d04aa83258
commit
dcd6dc00f4
83 changed files with 1797 additions and 390 deletions
33
webclient/src/components/Guard/AuthGuard.spec.tsx
Normal file
33
webclient/src/components/Guard/AuthGuard.spec.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { screen } from '@testing-library/react';
|
||||
import { renderWithProviders, connectedState, disconnectedState } from '../../__test-utils__';
|
||||
import AuthGuard from './AuthGuard';
|
||||
|
||||
vi.mock('@app/hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@app/hooks')>();
|
||||
return { ...actual, useWebClient: vi.fn(() => ({})) };
|
||||
});
|
||||
|
||||
describe('AuthGuard', () => {
|
||||
it('redirects to LOGIN when disconnected', () => {
|
||||
renderWithProviders(<AuthGuard />, {
|
||||
preloadedState: disconnectedState,
|
||||
route: '/server',
|
||||
});
|
||||
|
||||
// Navigate triggers a route change — AuthGuard itself renders no text.
|
||||
// We verify it doesn't render any meaningful content.
|
||||
expect(screen.queryByRole('button')).toBeNull();
|
||||
expect(screen.queryByRole('heading')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nothing visible when connected', () => {
|
||||
const { container } = renderWithProviders(<AuthGuard />, {
|
||||
preloadedState: connectedState,
|
||||
route: '/server',
|
||||
});
|
||||
|
||||
// AuthGuard renders an empty fragment when connected.
|
||||
// The only DOM is from provider wrappers (e.g. ToastProvider's container div).
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
@ -8,7 +8,7 @@ const AuthGuard = () => {
|
|||
const isConnected = useAppSelector(ServerSelectors.getIsConnected);
|
||||
return !isConnected
|
||||
? <Navigate to={App.RouteEnum.LOGIN} />
|
||||
: <div></div>;
|
||||
: <></>;
|
||||
};
|
||||
|
||||
export default AuthGuard;
|
||||
|
|
|
|||
38
webclient/src/components/Guard/ModGuard.spec.tsx
Normal file
38
webclient/src/components/Guard/ModGuard.spec.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { renderWithProviders, connectedState, makeUser } from '../../__test-utils__';
|
||||
import { Data } from '@app/types';
|
||||
import ModGuard from './ModGuard';
|
||||
|
||||
vi.mock('@app/hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@app/hooks')>();
|
||||
return { ...actual, useWebClient: vi.fn(() => ({})) };
|
||||
});
|
||||
|
||||
describe('ModGuard', () => {
|
||||
it('redirects when user is not a moderator', () => {
|
||||
const { container } = renderWithProviders(<ModGuard />, {
|
||||
preloadedState: connectedState,
|
||||
route: '/logs',
|
||||
});
|
||||
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('renders nothing visible when user is a moderator', () => {
|
||||
const modUser = makeUser({
|
||||
userLevel: Data.ServerInfo_User_UserLevelFlag.IsModerator,
|
||||
});
|
||||
|
||||
const { container } = renderWithProviders(<ModGuard />, {
|
||||
preloadedState: {
|
||||
...connectedState,
|
||||
server: {
|
||||
...(connectedState.server as any),
|
||||
user: modUser,
|
||||
},
|
||||
},
|
||||
route: '/logs',
|
||||
});
|
||||
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
30
webclient/src/components/InputField/InputField.spec.tsx
Normal file
30
webclient/src/components/InputField/InputField.spec.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import InputField from './InputField';
|
||||
|
||||
describe('InputField', () => {
|
||||
const defaultProps = {
|
||||
input: { name: 'test', value: '', onChange: vi.fn(), onBlur: vi.fn(), onFocus: vi.fn() },
|
||||
meta: { touched: false, error: null, warning: null },
|
||||
label: 'Test Field',
|
||||
};
|
||||
|
||||
it('renders a text field with label', () => {
|
||||
render(<InputField {...defaultProps} />);
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when touched and has error', () => {
|
||||
render(<InputField {...defaultProps} meta={{ touched: true, error: 'Required', warning: null }} />);
|
||||
expect(screen.getByText('Required')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows warning when touched and has warning', () => {
|
||||
render(<InputField {...defaultProps} meta={{ touched: true, error: null, warning: 'Weak password' }} />);
|
||||
expect(screen.getByText('Weak password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show validation messages when not touched', () => {
|
||||
render(<InputField {...defaultProps} meta={{ touched: false, error: 'Required', warning: null }} />);
|
||||
expect(screen.queryByText('Required')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -16,7 +16,7 @@ import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined';
|
|||
import { useWebClient } from '@app/hooks';
|
||||
import { KnownHostDialog } from '@app/dialogs';
|
||||
import { useReduxEffect } from '@app/hooks';
|
||||
import { HostDTO } from '@app/services';
|
||||
import { DefaultHosts, HostDTO, getHostPort } from '@app/services';
|
||||
import { ServerTypes } from '@app/store';
|
||||
import { App } from '@app/types';
|
||||
import Toast from '../Toast/Toast';
|
||||
|
|
@ -87,7 +87,7 @@ const KnownHosts = (props) => {
|
|||
|
||||
if (!hosts?.length) {
|
||||
// @TODO: find a better pattern to seeding default data in indexedDB
|
||||
await HostDTO.bulkAdd(App.DefaultHosts);
|
||||
await HostDTO.bulkAdd(DefaultHosts);
|
||||
loadKnownHosts();
|
||||
} else {
|
||||
const selectedHost = hosts.find(({ lastSelected }) => lastSelected) || hosts[0];
|
||||
|
|
@ -197,7 +197,7 @@ const KnownHosts = (props) => {
|
|||
const testConnection = () => {
|
||||
setTestingConnection(TestConnection.TESTING);
|
||||
|
||||
const options = { ...App.getHostPort(hostsState.selectedHost) };
|
||||
const options = { ...getHostPort(hostsState.selectedHost) };
|
||||
webClient.request.authentication.testConnection(options);
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ const KnownHosts = (props) => {
|
|||
|
||||
{
|
||||
hostsState.hosts.map((host, index) => {
|
||||
const hostPort = App.getHostPort(hostsState.hosts[index]);
|
||||
const hostPort = getHostPort(hostsState.hosts[index]);
|
||||
|
||||
return (
|
||||
<MenuItem value={host} key={index}>
|
||||
|
|
|
|||
19
webclient/src/components/Message/Message.spec.tsx
Normal file
19
webclient/src/components/Message/Message.spec.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { screen } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../../__test-utils__';
|
||||
import Message from './Message';
|
||||
|
||||
describe('Message', () => {
|
||||
it('renders a plain message', () => {
|
||||
const message = { message: 'Hello world' };
|
||||
renderWithProviders(<Message message={message} />);
|
||||
|
||||
expect(screen.getByText('Hello world')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the message container', () => {
|
||||
const message = { message: 'Test message' };
|
||||
const { container } = renderWithProviders(<Message message={message} />);
|
||||
|
||||
expect(container.querySelector('.message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -3,8 +3,7 @@ import Grid from '@mui/material/Grid';
|
|||
|
||||
import './ThreePaneLayout.css';
|
||||
|
||||
// @DEPRECATED
|
||||
// This component sucks balls, dont use it. It will be removed sooner than later.
|
||||
/** @deprecated Scheduled for replacement with a more flexible layout component. */
|
||||
function ThreePaneLayout(props: ThreePaneLayoutProps) {
|
||||
return (
|
||||
<div className="three-pane-layout">
|
||||
|
|
|
|||
46
webclient/src/components/UserDisplay/UserDisplay.spec.tsx
Normal file
46
webclient/src/components/UserDisplay/UserDisplay.spec.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { screen } from '@testing-library/react';
|
||||
import { renderWithProviders, connectedState, makeUser, createMockWebClient } from '../../__test-utils__';
|
||||
import UserDisplay from './UserDisplay';
|
||||
|
||||
const mockWebClient = createMockWebClient();
|
||||
|
||||
vi.mock('@app/hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@app/hooks')>();
|
||||
return { ...actual, useWebClient: vi.fn(() => mockWebClient) };
|
||||
});
|
||||
|
||||
vi.mock('@app/images', () => ({
|
||||
Images: { Countries: { us: 'us.png', de: 'de.png' } },
|
||||
}));
|
||||
|
||||
describe('UserDisplay', () => {
|
||||
it('renders user name', () => {
|
||||
const user = makeUser({ name: 'TestPlayer', country: 'us' });
|
||||
renderWithProviders(<UserDisplay user={user} />, {
|
||||
preloadedState: connectedState,
|
||||
});
|
||||
|
||||
expect(screen.getByText('TestPlayer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders country flag image', () => {
|
||||
const user = makeUser({ name: 'TestPlayer', country: 'us' });
|
||||
renderWithProviders(<UserDisplay user={user} />, {
|
||||
preloadedState: connectedState,
|
||||
});
|
||||
|
||||
const img = screen.getByAltText('us');
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img).toHaveAttribute('src', 'us.png');
|
||||
});
|
||||
|
||||
it('renders link to player profile', () => {
|
||||
const user = makeUser({ name: 'TestPlayer', country: 'us' });
|
||||
renderWithProviders(<UserDisplay user={user} />, {
|
||||
preloadedState: connectedState,
|
||||
});
|
||||
|
||||
const link = screen.getByRole('link', { name: /TestPlayer/ });
|
||||
expect(link).toHaveAttribute('href', '/player/TestPlayer');
|
||||
});
|
||||
});
|
||||
21
webclient/src/components/VirtualList/VirtualList.spec.tsx
Normal file
21
webclient/src/components/VirtualList/VirtualList.spec.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import VirtualList from './VirtualList';
|
||||
|
||||
describe('VirtualList', () => {
|
||||
it('renders without crashing with empty items', () => {
|
||||
const { container } = render(<VirtualList items={[]} />);
|
||||
expect(container.querySelector('.virtual-list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts className as a string', () => {
|
||||
const { container } = render(<VirtualList items={[]} className="custom-class" />);
|
||||
expect(container.querySelector('.custom-class')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies empty string as default className (not object)', () => {
|
||||
const { container } = render(<VirtualList items={[]} />);
|
||||
const list = container.querySelector('.virtual-list__list');
|
||||
// className should not contain "[object Object]"
|
||||
expect(list?.className).not.toContain('[object Object]');
|
||||
});
|
||||
});
|
||||
|
|
@ -15,7 +15,7 @@ const Row = ({ index, style, items }: RowComponentProps<RowData>) => (
|
|||
</div>
|
||||
);
|
||||
|
||||
const VirtualList = ({ items, className = {}, size = 30 }) => (
|
||||
const VirtualList = ({ items, className = '', size = 30 }) => (
|
||||
<div className="virtual-list">
|
||||
<List<RowData>
|
||||
className={`virtual-list__list ${className}`}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue