import { beforeEach, describe, expect, it } from 'vitest'; import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { installFetch, installGatewaySocket, json, makeReadySnapshot, makeUser, messagesRoutes, renderApp, resetStores, type FetchRoute, } from './helpers'; /** * Доступ к комнате (AGENT.md 6.2, 7.5): приватная комната закрывается запретом * @everyone, права выдаются ролям, а смена прав требует подтверждения личности. */ const user = makeUser({ id: 'user-1' }); const channel = { id: 'c-1', guild_id: 'g-1', name: 'общий', type: 'text' as const, position: 0, can_view: true, permission_overwrites: [], }; const roles = [ { id: 'r-1', name: '@everyone', color: 0, position: 0, permissions: '', is_default: true }, { id: 'r-2', name: 'Модераторы', color: 0, position: 1, permissions: '', is_default: false }, ]; const members = [ { user_id: 'user-2', username: 'bob', display_name: 'Bob', status: 'online', is_instance_admin: false, joined_at: '2026-09-01T00:00:00Z', role_ids: [], }, ]; /** routes собирает мок REST: комнаты, роли и участники сервера. */ function routes(permissions: string[], extra: FetchRoute[] = []): FetchRoute[] { return [ ...extra, { match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [ { id: 'g-1', name: 'Сервер', owner_id: 'user-1', is_main: true, member_count: 2, my_permissions: permissions, my_role_ids: [], }, ], }), }, { match: '/api/v1/users/@me', response: () => json({ user }) }, { match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat-test' } }) }, { match: '/api/v1/guilds/g-1/channels', response: () => json({ channels: [channel] }) }, { match: '/api/v1/guilds/g-1/members', response: () => json({ members }) }, { match: '/api/v1/guilds/g-1/roles', response: () => json({ roles }) }, { match: '/api/v1/guilds/g-1', response: () => json({ guild: { id: 'g-1', name: 'Сервер', owner_id: 'user-1', is_main: true, member_count: 2, my_role_ids: [], my_permissions: permissions, channels: [channel], roles: [], }, }), }, ...messagesRoutes('c-1', []), ]; } beforeEach(() => { resetStores(); }); /** openChannelSettings открывает настройки комнаты из шапки чата. */ async function openChannelSettings(permissions: string[], extra: FetchRoute[] = []) { const fetchMock = installFetch(routes(permissions, extra)); const snapshot = makeReadySnapshot([ { id: 'g-1', name: 'Сервер', is_main: true, owner_id: permissions.includes('MANAGE_ROLES') ? 'user-1' : 'user-9', my_permissions: permissions, channels: [channel], }, ]); const gateway = installGatewaySocket(snapshot); renderApp('/app/g-1/c-1'); await gateway.greet(); await screen.findByTestId('message-list'); await userEvent.click(await screen.findByTestId('channel-settings')); const content = await screen.findByTestId('settings-content'); return { fetchMock, content }; } describe('доступ к комнате', () => { it('приватная комната закрывается запретом @everyone, права ролей меняются', async () => { const calls: { url: string; body: unknown }[] = []; const { content } = await openChannelSettings( ['MANAGE_ROLES', 'MANAGE_GUILD'], [ { match: '/api/v1/guilds/g-1/channels/c-1/overwrites/role/r-1', method: 'PUT', response: (request) => { calls.push({ url: request.url, body: request.body }); return json({ channel }); }, }, { match: '/api/v1/guilds/g-1/channels/c-1/overwrites/role/r-2', method: 'PUT', response: (request) => { calls.push({ url: request.url, body: request.body }); return json({ channel }); }, }, ], ); const editor = await within(content).findByTestId('channel-permissions'); expect(within(editor).getByTestId('channel-permission-row-r-1')).toHaveTextContent('@everyone'); // Включаем приватность: оверрайд @everyone получает запрет просмотра. await userEvent.click(within(content).getByTestId('channel-private-toggle')); await waitFor(() => expect(calls).toHaveLength(1)); expect(calls[0]?.body).toEqual({ allow: '', deny: 'VIEW_CHANNEL' }); // Разрешаем роли просмотр: в запросе появляется allow с именем права. await userEvent.selectOptions( within(editor).getByTestId('channel-permission-r-2-view_channel'), 'allow', ); await waitFor(() => expect(calls).toHaveLength(2)); expect(calls[1]?.url).toContain('/overwrites/role/r-2'); expect(calls[1]?.body).toEqual({ allow: 'VIEW_CHANNEL', deny: '' }); // Запрет переписки для @everyone уходит отдельным правом. await userEvent.selectOptions( within(editor).getByTestId('channel-permission-r-1-send_messages'), 'deny', ); await waitFor(() => expect(calls).toHaveLength(3)); expect(calls[2]?.body).toEqual({ allow: '', deny: 'SEND_MESSAGES' }); }); it('смена прав повторяется после подтверждения личности', async () => { const calls: { body: unknown }[] = []; let requireStepUp = true; const { content } = await openChannelSettings( ['MANAGE_ROLES', 'MANAGE_GUILD'], [ { match: '/api/v1/guilds/g-1/channels/c-1/overwrites/role/r-1', method: 'PUT', response: (request) => { calls.push({ body: request.body }); // Сервер требует свежее подтверждение личности (AGENT.md 9.3). if (requireStepUp) { requireStepUp = false; return json({ error: { code: 'auth.step_up_required', message: 'need' } }, 403); } return json({ channel }); }, }, { match: '/api/v1/auth/step-up', method: 'POST', response: () => json({ ok: true }) }, ], ); await within(content).findByTestId('channel-permissions'); await userEvent.click(within(content).getByTestId('channel-private-toggle')); await waitFor(() => expect(calls).toHaveLength(1)); const stepUp = await within(content).findByTestId('channel-permissions-step-up'); await userEvent.type(within(stepUp).getByLabelText('Ваш пароль'), 'correct-horse-battery'); await userEvent.click(within(stepUp).getByRole('button', { name: 'Подтвердить' })); await waitFor(() => expect(calls).toHaveLength(2)); expect(calls[1]?.body).toEqual({ allow: '', deny: 'VIEW_CHANNEL' }); }); it('без права MANAGE_ROLES редактор доступа скрыт', async () => { // Шестерёнка комнаты доступна по MANAGE_WEBHOOKS, но прав на доступ нет. const { content } = await openChannelSettings(['MANAGE_WEBHOOKS', 'MANAGE_GUILD']); await waitFor(() => expect(within(content).queryByTestId('channel-permissions')).not.toBeInTheDocument(), ); }); });