Files
glchat/web/tests/guard.test.tsx
T
grendervill 751ecc6d45 fix(web): отзыв сессий уводит на вход и при открытом сервере
Сервер отзывал сессии кадром {"op":4,"reason":"…","resumable":false} и закрывал
соединение, но устройство с открытым сервером (/app/<сервер>/<комната>)
оставалось в приложении: уход на /login зависел только от 401 на запросе
профиля, а перечитывание профиля в этой ветке не срабатывало (пункт 12
матрицы 11.6, проверено перехватом WS на стенде).

Теперь признак invalidated в сторе шлюза читает AuthGuard и сразу уводит на
экран входа — в любом состоянии приложения, не дожидаясь ответа REST.
Признак снимается при успешном входе, иначе форма входа зацикливалась бы.

Тесты: «отзыв сессии на открытом сервере уводит на /login»; живая проверка
build/verify-session-invalidation.mjs дополнена сценарием с открытым сервером.
2026-09-22 21:52:16 +03:00

96 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 { dispatchGatewayEvent } from '@/stores/gateway';
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('отзыв сессии на открытом сервере уводит на /login', async () => {
const user = makeUser();
installFetch([
{ match: '/api/v1/users/@me', response: () => json({ user }) },
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
]);
const { router } = renderApp('/app/g-1/c-1');
await waitFor(() => {
expect(screen.getByTestId('guild-rail')).toBeVisible();
});
// Сервер отозвал сессии (logout-all, смена пароля): приходит INVALID_SESSION,
// и уход на экран входа не зависит от ответа запроса профиля (AGENT.md 11.6).
dispatchGatewayEvent({ op: 0, t: 'SESSION_INVALIDATED', s: 1, d: { reason: 'logout_all' } });
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');
});
});