Public Access
feat(auth): tell the user when the session expired
Every 401 from Auth or Core now goes through useSessionExpired: the login page says the session expired and returns to the last page after login. App no longer removes a token that only the browser clock calls expired, so a backend 401 is the one signal. A 403 on the profile now shows an error instead.
This commit is contained in:
1 parent
03259d0515
commit
d4c0f8d8f3
19 files changed
+351
-35
No files matched your search
@@ -0,0 +1,82 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {MemoryRouter, Route, Routes, useLocation} from 'react-router-dom';
|
||||
import LoginPage from './LoginPage.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
|
||||
const b64 = (o) => btoa(JSON.stringify(o)).replace(/=+$/, '');
|
||||
const TOKEN = `${b64({alg: 'EdDSA'})}.${b64({exp: Math.floor(Date.now() / 1000) + 3600})}.sig`;
|
||||
|
||||
function CurrentPath() {
|
||||
const location = useLocation();
|
||||
return <div data-testid="path">{location.pathname + location.search}</div>;
|
||||
}
|
||||
|
||||
function renderLogin(state) {
|
||||
render(
|
||||
<MemoryRouter initialEntries={[{pathname: '/login', state}]}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage/>}/>
|
||||
<Route path="*" element={<CurrentPath/>}/>
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
function submit() {
|
||||
fireEvent.change(screen.getByLabelText('LOGIN.EMAIL'), {target: {value: 'a@b.de'}});
|
||||
fireEvent.change(screen.getByLabelText('LOGIN.PASSWORD'), {target: {value: 'secret'}});
|
||||
fireEvent.click(screen.getByRole('button', {name: 'LOGIN.SUBMIT'}));
|
||||
}
|
||||
|
||||
describe('LoginPage', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.fetch = vi.fn(async () => ({ok: true, status: 200, json: async () => ({token: TOKEN})}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('says that the session expired', () => {
|
||||
renderLogin({sessionExpired: true, from: '/library'});
|
||||
|
||||
expect(screen.getByText('LOGIN.SESSION_EXPIRED')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows no message on a normal visit', () => {
|
||||
renderLogin(undefined);
|
||||
|
||||
expect(screen.queryByText('LOGIN.SESSION_EXPIRED')).toBeNull();
|
||||
});
|
||||
|
||||
it('goes back to the page of the expired session after login', async () => {
|
||||
renderLogin({sessionExpired: true, from: '/library?status=PLAYING'});
|
||||
|
||||
submit();
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('path').textContent).toBe('/library?status=PLAYING'));
|
||||
expect(localStorage.getItem('tbp_jwt')).toBe(TOKEN);
|
||||
});
|
||||
|
||||
it('ignores a return path outside the app', async () => {
|
||||
renderLogin({sessionExpired: true, from: '//evil.example'});
|
||||
|
||||
submit();
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('path').textContent).toBe('/dashboard'));
|
||||
});
|
||||
|
||||
it('hides the expired message once a login attempt fails', async () => {
|
||||
fetch.mockResolvedValueOnce({ok: false, status: 401, headers: new Headers(), json: async () => ({code: 'INVALID_CREDENTIALS'})});
|
||||
renderLogin({sessionExpired: true, from: '/library'});
|
||||
|
||||
submit();
|
||||
|
||||
await waitFor(() => expect(screen.getByText('LOGIN.ERROR_INVALID_CREDENTIALS')).toBeTruthy());
|
||||
expect(screen.queryByText('LOGIN.SESSION_EXPIRED')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user