refactor(client): remove useless client session state

This commit is contained in:
Nicolas Meienberger 2023-05-03 22:52:43 +02:00 committed by Nicolas Meienberger
parent 46ccbf2591
commit 2b853596c3
16 changed files with 61 additions and 211 deletions

View file

@ -1,7 +1,5 @@
import React from 'react';
import { render, screen, waitFor } from '../../../../tests/test-utils';
import { getTRPCMock, getTRPCMockError } from '../../mocks/getTrpcMock';
import { server } from '../../mocks/server';
import { render, screen } from '../../../../tests/test-utils';
import { Layout } from './Layout';
const pushFn = jest.fn();
@ -23,28 +21,4 @@ describe('Test: Layout', () => {
expect(screen.getByText('test')).toBeInTheDocument();
});
it('should correctly set token in localStorage when refreshToken is called', async () => {
// Arranger
server.use(getTRPCMock({ path: ['auth', 'refreshToken'], type: 'mutation', response: { token: 'fake-token' } }));
render(<Layout>test</Layout>);
// Act
await waitFor(() => {
expect(localStorage.getItem('token')).toBe('fake-token');
});
});
it('should remove token from local storage and redirect to login page on error', async () => {
// Arranger
server.use(getTRPCMockError({ path: ['auth', 'refreshToken'], type: 'mutation', message: 'fake-error' }));
render(<Layout>test</Layout>);
const removeItemSpy = jest.spyOn(localStorage, 'removeItem');
// Act
await waitFor(() => {
expect(removeItemSpy).toBeCalledWith('token');
});
expect(pushFn).toBeCalledWith('/login');
});
});

View file

@ -1,16 +1,13 @@
import Head from 'next/head';
import Link from 'next/link';
import React, { useEffect } from 'react';
import React from 'react';
import clsx from 'clsx';
import semver from 'semver';
import { useRouter } from 'next/router';
import { Header } from '../ui/Header';
import styles from './Layout.module.scss';
import { useSystemStore } from '../../state/systemStore';
import { trpc } from '../../utils/trpc';
interface IProps {
loading?: boolean;
breadcrumbs?: { name: string; href: string; current?: boolean }[];
children: React.ReactNode;
title?: string;
@ -18,21 +15,6 @@ interface IProps {
}
export const Layout: React.FC<IProps> = ({ children, breadcrumbs, title, actions }) => {
const router = useRouter();
const { mutate } = trpc.auth.refreshToken.useMutation({
onSuccess: (data) => {
if (data?.token) localStorage.setItem('token', data.token);
},
onError: () => {
localStorage.removeItem('token');
router.push('/login');
},
});
useEffect(() => {
mutate();
}, [mutate]);
const { version } = useSystemStore();
const defaultVersion = '0.0.0';
const isLatest = semver.gte(version?.current || defaultVersion, version?.latest || defaultVersion);

View file

@ -1,49 +0,0 @@
import React from 'react';
import { render, screen } from '../../../../../tests/test-utils';
import { getTRPCMock } from '../../../mocks/getTrpcMock';
import { server } from '../../../mocks/server';
import { AuthProvider } from './AuthProvider';
describe('Test: AuthProvider', () => {
it('should render login form if user is not logged in', async () => {
// arrange
render(
<AuthProvider>
<div>Should not render</div>
</AuthProvider>,
);
server.use(getTRPCMock({ path: ['auth', 'me'], type: 'query', response: null }));
// assert
await screen.findByText('Login');
expect(screen.queryByText('Should not render')).not.toBeInTheDocument();
});
it('should render children if user is logged in', async () => {
// arrange
render(
<AuthProvider>
<div>Should render</div>
</AuthProvider>,
);
// assert
await screen.findByText('Should render');
});
it('should render register form if app is not configured', async () => {
// arrange
server.use(getTRPCMock({ path: ['auth', 'me'], type: 'query', response: null }));
server.use(getTRPCMock({ path: ['auth', 'isConfigured'], type: 'query', response: false }));
render(
<AuthProvider>
<div>Should not render</div>
</AuthProvider>,
);
// assert
await screen.findByText('Register your account');
expect(screen.queryByText('Should not render')).not.toBeInTheDocument();
});
});

View file

@ -1,29 +0,0 @@
import React from 'react';
import { LoginContainer } from '../../../modules/Auth/containers/LoginContainer';
import { RegisterContainer } from '../../../modules/Auth/containers/RegisterContainer';
import { trpc } from '../../../utils/trpc';
import { StatusScreen } from '../../StatusScreen';
interface IProps {
children: React.ReactElement;
}
export const AuthProvider: React.FC<IProps> = ({ children }) => {
const me = trpc.auth.me.useQuery();
const isConfigured = trpc.auth.isConfigured.useQuery();
const loading = me.isLoading || isConfigured.isLoading;
if (loading) {
return <StatusScreen title="" subtitle="" />;
}
if (me.data) {
return children;
}
if (!isConfigured?.data) {
return <RegisterContainer />;
}
return <LoginContainer />;
};

View file

@ -1 +0,0 @@
export { AuthProvider } from './AuthProvider';

View file

@ -3,6 +3,19 @@ import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../t
import { useUIStore } from '../../../state/uiStore';
import { Header } from './Header';
const pushFn = jest.fn();
jest.mock('next/router', () => {
const actualRouter = jest.requireActual('next-router-mock');
return {
...actualRouter,
useRouter: () => ({
...actualRouter.useRouter(),
push: pushFn,
}),
};
});
describe('Header', () => {
it('renders without crashing', () => {
render(<Header />);
@ -46,14 +59,13 @@ describe('Header', () => {
expect(result.current.darkMode).toBe(false);
});
it('Should remove the token from local storage on logout', async () => {
localStorage.setItem('token', 'token');
it('Should redirect to /login after successful logout', async () => {
render(<Header />);
const logoutButton = screen.getByTestId('logout-button');
fireEvent.click(logoutButton as Element);
await waitFor(() => {
expect(localStorage.getItem('token')).toBeNull();
expect(pushFn).toHaveBeenCalledWith('/login');
});
});
});

View file

@ -20,7 +20,6 @@ export const Header: React.FC<IProps> = ({ isUpdateAvailable }) => {
const utils = trpc.useContext();
const logout = trpc.auth.logout.useMutation({
onSuccess: () => {
localStorage.removeItem('token');
utils.auth.me.invalidate();
router.push('/login');
},

View file

@ -40,22 +40,17 @@ export const handlers = [
getTRPCMock({
path: ['auth', 'login'],
type: 'mutation',
response: { token: 'token' },
response: {},
}),
getTRPCMock({
path: ['auth', 'logout'],
type: 'mutation',
response: true,
}),
getTRPCMock({
path: ['auth', 'refreshToken'],
type: 'mutation',
response: { token: 'token' },
}),
getTRPCMock({
path: ['auth', 'register'],
type: 'mutation',
response: { token: 'token' },
response: true,
}),
getTRPCMock({
path: ['auth', 'me'],

View file

@ -37,11 +37,3 @@ export const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
</Layout>
);
};
AppDetailsPage.getInitialProps = (ctx) => {
const { query } = ctx;
const appId = String(query.id);
return { appId };
};

View file

@ -5,6 +5,19 @@ import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
import { server } from '../../../../mocks/server';
import { LoginContainer } from './LoginContainer';
const pushFn = jest.fn();
jest.mock('next/router', () => {
const actualRouter = jest.requireActual('next-router-mock');
return {
...actualRouter,
useRouter: () => ({
...actualRouter.useRouter(),
push: pushFn,
}),
};
});
describe('Test: LoginContainer', () => {
it('should render without error', () => {
// Arrange
@ -38,12 +51,11 @@ describe('Test: LoginContainer', () => {
expect(loginButton).toBeEnabled();
});
it('should add token in localStorage on submit', async () => {
it('should redirect to / upon successful login', async () => {
// Arrange
const email = faker.internet.email();
const password = faker.internet.password();
const token = faker.datatype.uuid();
server.use(getTRPCMock({ path: ['auth', 'login'], type: 'mutation', response: { token } }));
server.use(getTRPCMock({ path: ['auth', 'login'], type: 'mutation', response: {} }));
render(<LoginContainer />);
// Act
@ -57,7 +69,7 @@ describe('Test: LoginContainer', () => {
// Assert
await waitFor(() => {
expect(localStorage.getItem('token')).toEqual(token);
expect(pushFn).toHaveBeenCalledWith('/');
});
});
@ -79,8 +91,6 @@ describe('Test: LoginContainer', () => {
await waitFor(() => {
expect(screen.getByText(/my big error/)).toBeInTheDocument();
});
const token = localStorage.getItem('token');
expect(token).toBeNull();
});
it('should show totp form if totpSessionId is returned', async () => {
@ -149,14 +159,13 @@ describe('Test: LoginContainer', () => {
});
});
it('should add token in localStorage if totp code is valid', async () => {
it('should redirect to / if totp is valid', async () => {
// arrange
const email = faker.internet.email();
const password = faker.internet.password();
const totpSessionId = faker.datatype.uuid();
const token = faker.datatype.uuid();
server.use(getTRPCMock({ path: ['auth', 'login'], type: 'mutation', response: { totpSessionId } }));
server.use(getTRPCMock({ path: ['auth', 'verifyTotp'], type: 'mutation', response: { token } }));
server.use(getTRPCMock({ path: ['auth', 'verifyTotp'], type: 'mutation', response: true }));
render(<LoginContainer />);
// act
@ -183,7 +192,7 @@ describe('Test: LoginContainer', () => {
// assert
await waitFor(() => {
expect(localStorage.getItem('token')).toEqual(token);
expect(pushFn).toHaveBeenCalledWith('/');
});
});
});

View file

@ -1,6 +1,6 @@
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useRouter } from 'next/router';
import { trpc } from '../../../../utils/trpc';
import { AuthFormLayout } from '../../components/AuthFormLayout';
import { LoginForm } from '../../components/LoginForm';
@ -14,14 +14,12 @@ export const LoginContainer: React.FC = () => {
const utils = trpc.useContext();
const login = trpc.auth.login.useMutation({
onError: (e) => {
localStorage.removeItem('token');
toast.error(`Login failed: ${e.message}`);
},
onSuccess: (data) => {
if (data.totpSessionId) {
setTotpSessionId(data.totpSessionId);
} else if (data.token) {
localStorage.setItem('token', data.token);
} else {
utils.auth.me.invalidate();
router.push('/');
}
@ -32,8 +30,7 @@ export const LoginContainer: React.FC = () => {
onError: (e) => {
toast.error(`Verification failed: ${e.message}`);
},
onSuccess: (data) => {
localStorage.setItem('token', data.token);
onSuccess: () => {
utils.auth.me.invalidate();
router.push('/');
},

View file

@ -5,6 +5,19 @@ import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
import { server } from '../../../../mocks/server';
import { RegisterContainer } from './RegisterContainer';
const pushFn = jest.fn();
jest.mock('next/router', () => {
const actualRouter = jest.requireActual('next-router-mock');
return {
...actualRouter,
useRouter: () => ({
...actualRouter.useRouter(),
push: pushFn,
}),
};
});
describe('Test: RegisterContainer', () => {
it('should render without error', () => {
render(<RegisterContainer />);
@ -12,13 +25,12 @@ describe('Test: RegisterContainer', () => {
expect(screen.getByText('Register')).toBeInTheDocument();
});
it('should add token in localStorage on submit', async () => {
it.only('should redirect to / upon successful registration', async () => {
// Arrange
const email = faker.internet.email();
const password = faker.internet.password();
const token = faker.datatype.uuid();
server.use(getTRPCMock({ path: ['auth', 'register'], type: 'mutation', response: { token }, delay: 100 }));
server.use(getTRPCMock({ path: ['auth', 'register'], type: 'mutation', response: true, delay: 100 }));
render(<RegisterContainer />);
// Act
@ -33,7 +45,9 @@ describe('Test: RegisterContainer', () => {
fireEvent.click(registerButton);
// Assert
await waitFor(() => expect(localStorage.getItem('token')).toEqual(token));
await waitFor(() => {
expect(pushFn).toHaveBeenCalledWith('/');
});
});
it('should show toast if register mutation fails', async () => {

View file

@ -12,11 +12,9 @@ export const RegisterContainer: React.FC = () => {
const utils = trpc.useContext();
const register = trpc.auth.register.useMutation({
onError: (e) => {
localStorage.removeItem('token');
toast.error(`Registration failed: ${e.message}`);
},
onSuccess: (data) => {
localStorage.setItem('token', data.token);
onSuccess: () => {
utils.auth.me.invalidate();
router.push('/');
},

View file

@ -16,6 +16,7 @@ jest.mock('next/router', () => {
}),
};
});
describe('Test: LoginPage', () => {
it('should render correctly', async () => {
render(<LoginPage />);

View file

@ -32,28 +32,6 @@ describe('Test: GeneralActions', () => {
});
});
it('should log user out if update is successful', async () => {
// arrange
localStorage.setItem('token', '123');
server.use(getTRPCMock({ path: ['system', 'getVersion'], response: { current: '1.0.0', latest: '2.0.0', body: '' } }));
server.use(getTRPCMock({ path: ['system', 'update'], response: true }));
render(<GeneralActions />);
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Update to 2.0.0' })).toBeInTheDocument();
});
const updateButton = screen.getByRole('button', { name: /Update to 2.0.0/i });
fireEvent.click(updateButton);
// act
const updateButtonModal = screen.getByRole('button', { name: /Update/i });
fireEvent.click(updateButtonModal);
// assert
await waitFor(() => {
expect(localStorage.getItem('token')).toBeNull();
});
});
it('should show toast if restart mutation fails', async () => {
// arrange
server.use(getTRPCMockError({ path: ['system', 'restart'], type: 'mutation', status: 500, message: 'Something went wrong' }));
@ -70,24 +48,4 @@ describe('Test: GeneralActions', () => {
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
});
});
it('should log user out if restart is successful', async () => {
// arrange
localStorage.setItem('token', '1234');
server.use(getTRPCMock({ path: ['system', 'restart'], response: true }));
render(<GeneralActions />);
// Find button near the top of the page
const restartButton = screen.getByRole('button', { name: /Restart/i });
// act
fireEvent.click(restartButton);
const restartButtonModal = screen.getByRole('button', { name: /Restart/i });
fireEvent.click(restartButtonModal);
// assert
await waitFor(() => {
expect(localStorage.getItem('token')).toBeNull();
});
});
});

View file

@ -27,7 +27,6 @@ export const GeneralActions = () => {
},
onSuccess: async () => {
setPollStatus(true);
localStorage.removeItem('token');
},
onError: (error) => {
updateDisclosure.close();
@ -42,7 +41,6 @@ export const GeneralActions = () => {
},
onSuccess: async () => {
setPollStatus(true);
localStorage.removeItem('token');
},
onError: (error) => {
restartDisclosure.close();