Files
glchat/web/tests/register.test.tsx
T
grendervill 9c001fedfe feat(web): базовый клиент Фазы 1 — вход, серверы, комнаты, настройки
Клиент React 19 + TanStack Query + Zustand:

- вход (поле TOTP появляется на `auth.2fa_required`), регистрация с проверками
  и учётом `registration_enabled`, онбординг первого входа с возможностью
  пропустить, редирект `/` и страница `/status`;
- защита маршрутов: неавторизованных — на `/login`, без онбординга — на
  `/onboarding`;
- оболочка `/app`: рейка серверов, сайдбар категорий и комнат, шапка сервера,
  панель пользователя, модалки создания сервера и комнаты, экран «нет серверов»
  со вступлением в главный сервер, заглушка комнаты до Фазы 2;
- настройки: профиль, безопасность (step-up, сессии, logout-all, 2FA с
  локальным QR и кодами восстановления), внешний вид, админ-раздел инстанса;
- Gateway-клиент: HELLO/IDENTIFY/RESUME, heartbeat с ожиданием ACK,
  экспоненциальное переподключение, разбор `INVALID_SESSION {reason,resumable}`;
- диспетчер событий: READY/RESUMED/USER_UPDATE/GUILD_UPDATE/GUILD_DELETE/
  CHANNEL_* применяются к стору, GUILD_CREATE догружается по REST,
  MEMBER_*/ROLE_* инвалидируют запросы участников, ролей и карточки сервера;
- i18n ru/en, 66 тестов Vitest, `check`/`lint`/`test`/`build` зелёные.

Серверные правки под клиент:

- `totp_enabled` в профиле (`GET /users/@me`, вход, регистрация) — клиенту
  нужно знать, требовать ли код при step-up;
- `POST /auth/2fa/setup` отдаёт `otpauth_url` и `qr_png` (data-URI): QR-код
  рисуется локально, внешние сервисы генерации QR не используются;
- права в ответах — имена (`VIEW_CHANNEL|SEND_MESSAGES`), клиент сверяет имена.
2026-09-19 22:03:46 +03:00

93 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { findRequest, installFetch, json, makeUser, recordedRequests, renderApp } from './helpers';
const instanceBase = {
name: 'glchat-test',
version: 'v0.1.0-test',
allow_guild_creation: true,
max_guilds_per_user: 5,
max_members_per_guild: 100,
max_message_length: 2000,
user_count: 1,
guild_count: 0,
};
describe('страница регистрации', () => {
it('успешная регистрация ведёт на /onboarding', async () => {
const user = makeUser({ onboarding_completed: false });
const fetchMock = installFetch([
{
match: '/api/v1/instance',
response: () => json({ instance: { ...instanceBase, registration_enabled: true } }),
},
{ match: '/api/v1/auth/register', method: 'POST', response: () => json({ user }) },
{ match: '/api/v1/users/@me', response: () => json({ user }) },
]);
const { router } = renderApp('/register');
const visitor = userEvent.setup();
await visitor.type(await screen.findByLabelText('Имя пользователя'), 'alice');
await visitor.type(screen.getByLabelText('Отображаемое имя'), 'Алиса');
await visitor.type(screen.getByLabelText('Почта'), '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('/onboarding');
});
expect(await screen.findByText('Привет, Alice!')).toBeVisible();
expect(findRequest(fetchMock, { url: '/auth/register', method: 'POST' })?.body).toEqual({
username: 'alice',
display_name: 'Алиса',
email: 'alice@example.com',
password: 'super-secret-1',
locale: 'ru',
});
});
it('не отправляет форму с некорректными данными', async () => {
const fetchMock = installFetch([
{
match: '/api/v1/instance',
response: () => json({ instance: { ...instanceBase, registration_enabled: true } }),
},
]);
renderApp('/register');
const visitor = userEvent.setup();
await visitor.type(await screen.findByLabelText('Имя пользователя'), 'al');
await visitor.type(screen.getByLabelText('Отображаемое имя'), 'Алиса');
await visitor.type(screen.getByLabelText('Почта'), 'not-an-email');
await visitor.type(screen.getByLabelText('Пароль'), 'short');
await visitor.click(screen.getByRole('button', { name: 'Создать аккаунт' }));
expect(await screen.findByText(/Имя пользователя: 3–32 символа/u)).toBeVisible();
expect(screen.getByText('Укажите корректный адрес почты.')).toBeVisible();
expect(screen.getByText('Пароль должен быть не короче 8 символов.')).toBeVisible();
expect(
recordedRequests(fetchMock).filter((request) => request.url.includes('/auth/register')),
).toHaveLength(0);
});
it('при registration_enabled=false показывает сообщение вместо формы', async () => {
installFetch([
{
match: '/api/v1/instance',
response: () => json({ instance: { ...instanceBase, registration_enabled: false } }),
},
]);
renderApp('/register');
expect(await screen.findByText('Регистрация закрыта')).toBeVisible();
expect(screen.queryByLabelText('Имя пользователя')).toBeNull();
expect(screen.queryByRole('button', { name: 'Создать аккаунт' })).toBeNull();
});
});