import { describe, expect, it } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { apiError, findRequest, installFetch, json, makeUser, recordedRequests, renderApp, } from './helpers'; const instanceOpen = { name: 'glchat-test', version: 'v0.1.0-test', registration_enabled: true, allow_guild_creation: true, max_guilds_per_user: 5, max_members_per_guild: 100, max_message_length: 2000, main_guild_id: 'g-main', user_count: 1, guild_count: 1, }; const totpLabel = 'Код 2FA или резервный код'; /** Ответы для входа: логин и оболочка приложения. */ function loginRoutes(extra: Parameters[0] = []) { const user = makeUser(); return installFetch([ { match: '/api/v1/instance', response: () => json({ instance: instanceOpen }) }, { match: '/api/v1/auth/login', method: 'POST', response: () => json({ user }) }, { match: '/api/v1/users/@me', response: () => json({ user }) }, { match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) }, ...extra, ]); } function loginBodies(fetchMock: ReturnType) { return recordedRequests(fetchMock).filter( (request) => request.url.includes('/auth/login') && request.method === 'POST', ); } describe('страница входа', () => { it('поле кода 2FA показано сразу и принимает резервные коды', async () => { installFetch([{ match: '/api/v1/instance', response: () => json({ instance: instanceOpen }) }]); renderApp('/login'); const field = await screen.findByLabelText(totpLabel); expect(field).toBeVisible(); expect(field).toHaveAttribute('autocomplete', 'one-time-code'); expect(field).toHaveAttribute('inputmode', 'text'); // Резервный код вида `a8eh-pshp-t8st` должен влезать целиком. expect(Number(field.getAttribute('maxlength'))).toBeGreaterThanOrEqual(20); expect(field).not.toHaveAttribute('pattern'); expect(screen.getByText(/резервный код вида xxxx-xxxx-xxxx/)).toBeVisible(); }); it('успешный вход переводит в /app и уходит без totp_code при пустом поле', async () => { const fetchMock = loginRoutes([ { match: '/api/v1/guilds/g-main/join', method: 'POST', response: () => json({ ok: true }) }, ]); const { router } = renderApp('/login'); const visitor = userEvent.setup(); await visitor.type(await screen.findByLabelText('Почта'), 'alice@example.com'); await visitor.type(screen.getByLabelText('Пароль'), 'super-secret-1'); await visitor.click(screen.getByRole('button', { name: 'Войти' })); await waitFor(() => { expect(router.state.location.pathname).toBe('/app/empty'); }); expect(await screen.findByRole('heading', { name: 'У вас пока нет серверов' })).toBeVisible(); // Автовступления в «главный сервер» больше нет: вход ведёт на пустой экран. expect(findRequest(fetchMock, { url: '/guilds/g-main/join', method: 'POST' })).toBeUndefined(); expect(findRequest(fetchMock, { url: '/auth/login', method: 'POST' })?.body).toEqual({ email: 'alice@example.com', password: 'super-secret-1', }); }); it('код 2FA уходит вместе с логином в первом же запросе', async () => { const fetchMock = loginRoutes([ { match: '/api/v1/guilds/g-main/join', method: 'POST', response: () => json({ ok: true }) }, ]); renderApp('/login'); const visitor = userEvent.setup(); await visitor.type(await screen.findByLabelText('Почта'), 'alice@example.com'); await visitor.type(screen.getByLabelText('Пароль'), 'super-secret-1'); await visitor.type(screen.getByLabelText(totpLabel), '123456'); await visitor.click(screen.getByRole('button', { name: 'Войти' })); await waitFor(() => { expect(loginBodies(fetchMock)).toHaveLength(1); }); expect(loginBodies(fetchMock)[0]?.body).toEqual({ email: 'alice@example.com', password: 'super-secret-1', totp_code: '123456', }); }); it('резервный код из 14 символов уходит на сервер целиком', async () => { const fetchMock = loginRoutes(); const { router } = renderApp('/login'); const visitor = userEvent.setup(); await visitor.type(await screen.findByLabelText('Почта'), 'alice@example.com'); await visitor.type(screen.getByLabelText('Пароль'), 'super-secret-1'); await visitor.type(screen.getByLabelText(totpLabel), 'a8eh-pshp-t8st'); // Поле не обрезает значение: 14 символов с дефисами видны целиком. expect(screen.getByLabelText(totpLabel)).toHaveValue('a8eh-pshp-t8st'); await visitor.click(screen.getByRole('button', { name: 'Войти' })); await waitFor(() => { expect(loginBodies(fetchMock)[0]?.body).toEqual({ email: 'alice@example.com', password: 'super-secret-1', totp_code: 'a8eh-pshp-t8st', }); }); // Вход не требует предварительной попытки без кода. expect(loginBodies(fetchMock)).toHaveLength(1); await waitFor(() => { expect(router.state.location.pathname).toBe('/app/empty'); }); }); it('при auth.2fa_required подсказывает про код и сохраняет логин с паролем', async () => { const user = makeUser(); let loginAttempts = 0; const fetchMock = installFetch([ { match: '/api/v1/instance', response: () => json({ instance: instanceOpen }) }, { match: '/api/v1/auth/login', method: 'POST', response: () => { loginAttempts += 1; return loginAttempts === 1 ? apiError('auth.2fa_required', 401) : json({ user }); }, }, { match: '/api/v1/users/@me', response: () => json({ user }) }, { match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) }, ]); const { router } = renderApp('/login'); const visitor = userEvent.setup(); await visitor.type(await screen.findByLabelText('Почта'), 'alice@example.com'); await visitor.type(screen.getByLabelText('Пароль'), 'super-secret-1'); await visitor.click(screen.getByRole('button', { name: 'Войти' })); // Подсказка понятная, а введённые логин и пароль остались на месте. expect(await screen.findByText(/Введите код 2FA или резервный код/)).toBeVisible(); expect(screen.getByLabelText('Почта')).toHaveValue('alice@example.com'); expect(screen.getByLabelText('Пароль')).toHaveValue('super-secret-1'); // Код, введённый после подсказки, не теряется при повторной отправке. await visitor.type(screen.getByLabelText(totpLabel), 'a8eh-pshp-t8st'); await visitor.click(screen.getByRole('button', { name: 'Войти' })); await waitFor(() => { expect(router.state.location.pathname).toBe('/app/empty'); }); expect(loginBodies(fetchMock)).toHaveLength(2); expect(loginBodies(fetchMock)[1]?.body).toEqual({ email: 'alice@example.com', password: 'super-secret-1', totp_code: 'a8eh-pshp-t8st', }); }); });