Files
glchat/web/tests/register.test.tsx
T
grendervill bf0d130ee8 feat(cli): включение 2FA инстанс-админа командой totp-setup
2FA обязательна для инстанс-администраторов, но до её включения вход закрыт —
получался замкнутый круг. Добавлены команды обслуживания:

- `glchat totp-setup --email <admin>`: создаёт секрет, подтверждает его кодом,
  печатает секрет, otpauth-ссылку и 8 резервных кодов (каждый одноразовый);
- `glchat totp-reset --email <admin>`: удаляет секрет при потере устройства;
- код `auth.2fa_enrollment_required` с подсказкой, какую команду выполнить;
- установщик: флаг `--admin-2fa` для автоматического включения (по умолчанию
  печатает подсказку, чтобы секреты не оседали в логах установки).

Проверено сквозным прогоном локально: bootstrap → 403 на входе без 2FA →
totp-setup → вход с TOTP-кодом → профиль, серверы, комнаты, роли, участники,
аудит, создание сервера админом, 429 на шестой попытке входа, секретов в логах
нет.
2026-09-19 21:53:35 +03:00

97 lines
3.9 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 { installFetch, json, makeUser, 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();
const registerCall = fetchMock.mock.calls.find(([input]) =>
String(input).includes('/auth/register'),
);
const init = registerCall?.[1] as RequestInit;
expect(JSON.parse(String(init.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(
fetchMock.mock.calls.filter(([input]) => String(input).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();
});
});