785af0dc7b
Сервер:
- store: оверрайды всех комнат сервера одним запросом (без N+1) и снятие
оверрайда с признаком «был ли он»;
- API: PUT/DELETE /guilds/{id}/channels/{cid}/overwrites/{role|user}/{tid}
(права именами через `|`, как у ролей), step-up на изменение, проверка что
роль принадлежит серверу, а участник состоит в нём;
- список комнат отдаёт permission_overwrites; после правки сбрасывается кэш
прав комнаты, пишется аудит (channel.overwrite_set/delete) и уходит событие
GUILD_CHANNELS_SYNC — видимость комнаты меняется у всех участников.
Клиент:
- редактор «Доступ к комнате»: приватность одним переключателем (запрет
VIEW_CHANNEL для @everyone), права просмотра/переписки/входа для ролей и
участников, подтверждение личности по требованию сервера;
- в настройках комнаты теперь и голосовые комнаты (фон и права), вебхуки —
только у текстовых; событие GUILD_CHANNELS_SYNC перечитывает список комнат.
Тесты: 2 Go-теста (скрытие и открытие комнаты ролями, проверка цели и прав) и
3 web-теста редактора (маски, step-up, скрытие без MANAGE_ROLES).
217 lines
7.7 KiB
TypeScript
217 lines
7.7 KiB
TypeScript
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(),
|
|
);
|
|
});
|
|
});
|