Files
glchat/web/tests/guard.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

74 lines
2.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 { apiError, installFetch, installFailingFetch, json, makeUser, renderApp } from './helpers';
describe('защита маршрутов', () => {
it('неавторизованного на /app отправляет на /login', async () => {
installFetch([
{ match: '/api/v1/users/@me', response: () => apiError('auth.unauthorized', 401) },
]);
const { router } = renderApp('/app');
await waitFor(() => {
expect(router.state.location.pathname).toBe('/login');
});
expect(await screen.findByLabelText('Почта')).toBeVisible();
});
it('пользователя с onboarding_completed=false отправляет на /onboarding', async () => {
const user = makeUser({ onboarding_completed: false });
installFetch([{ match: '/api/v1/users/@me', response: () => json({ user }) }]);
const { router } = renderApp('/app');
await waitFor(() => {
expect(router.state.location.pathname).toBe('/onboarding');
});
expect(await screen.findByText('Привет, Alice!')).toBeVisible();
});
it('онбординг доступен при незавершённой настройке (без цикла редиректов)', async () => {
const user = makeUser({ onboarding_completed: false });
installFetch([
{ match: '/api/v1/users/@me', response: () => json({ user }) },
{
match: '/api/v1/users/@me/onboarding/complete',
method: 'POST',
response: () => json({ user }),
},
]);
const { router } = renderApp('/onboarding');
expect(await screen.findByLabelText('Как вас показывать?')).toBeVisible();
expect(router.state.location.pathname).toBe('/onboarding');
});
it('/ без сессии ведёт на /login, а /status доступен без авторизации', async () => {
installFetch([
{ match: '/api/v1/users/@me', response: () => apiError('auth.unauthorized', 401) },
{ match: '/api/v1/meta', response: () => apiError('internal.error', 500) },
{ match: '/api/v1/readyz', response: () => apiError('internal.error', 500) },
]);
const { router } = renderApp('/');
await waitFor(() => {
expect(router.state.location.pathname).toBe('/login');
});
const status = renderApp('/status');
expect(await status.findByTestId('connection-state')).toHaveTextContent('Сервер недоступен');
});
it('сетевую ошибку показывает как сообщение, а не редирект', async () => {
installFailingFetch();
const { router } = renderApp('/app');
expect(await screen.findByRole('alert')).toHaveTextContent('Сервер не отвечает');
expect(router.state.location.pathname).toBe('/app');
});
});