feat(guilds): встроенные шаблоны серверов

- каталог шаблонов «Пустой», «Сообщество», «Игровой» (AGENT.md 7.3): роли,
  категории, текстовые и голосовые комнаты без сообщений
- POST /guilds принимает template_id, неизвестный шаблон отклоняется; аудит
  фиксирует выбранный шаблон
- GET /guild-templates отдаёт каталог с предпросмотром структуры
- клиент: выбор шаблона в окне создания сервера
- тесты: Go (структура каждого шаблона) и Vitest (выбор шаблона)
This commit is contained in:
2026-09-21 18:04:52 +03:00
parent a36ee9527b
commit 91f415e36d
9 changed files with 572 additions and 15 deletions
+61
View File
@@ -322,3 +322,64 @@ describe('оформление «для друзей»', () => {
expect(within(row).getByTestId('nick-name')).toHaveStyle({ color: '#2ecc71' });
});
});
describe('шаблоны серверов', () => {
it('создаёт сервер по выбранному шаблону', async () => {
const templates = [
{
id: 'empty',
name: 'Пустой',
description: 'Одна общая текстовая комната.',
roles: ['Администратор', 'Пользователь'],
text_channels: 1,
voice_channels: 0,
categories: [],
},
{
id: 'gaming',
name: 'Игровой',
description: 'Текстовые комнаты и две голосовые.',
roles: ['Администратор', 'Пользователь', 'Игрок'],
text_channels: 2,
voice_channels: 2,
categories: ['Текстовые', 'Голосовые'],
},
];
const fetchMock = installFetch([
{ match: '/api/v1/guild-templates', response: () => json({ templates }) },
{
match: '/api/v1/guilds',
method: 'POST',
response: () =>
json({
guild: {
id: 'g-new',
name: 'Новый',
owner_id: 'user-1',
is_main: false,
member_count: 1,
my_role_ids: [],
my_permissions: ['ADMINISTRATOR'],
},
}),
},
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
{ match: '/api/v1/users/@me', response: () => json({ user }) },
{ match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat-test' } }) },
]);
renderApp('/app');
await userEvent.click(await screen.findByLabelText('Создать сервер'));
const picker = await screen.findByTestId('guild-templates');
await userEvent.click(within(picker).getByTestId('guild-template-gaming'));
await userEvent.type(screen.getByLabelText('Название сервера'), 'Новый');
await userEvent.click(screen.getByRole('button', { name: 'Создать' }));
await waitFor(() => {
expect(bodyOf(findRequest(fetchMock, { url: '/guilds', method: 'POST' }))).toMatchObject({
name: 'Новый',
template_id: 'gaming',
});
});
});
});
+2 -1
View File
@@ -337,6 +337,7 @@ describe('основной экран', () => {
const createCall = recordedRequests(fetchMock).find(
(request) => request.url.endsWith('/api/v1/guilds') && request.method === 'POST',
);
expect(createCall?.body).toEqual({ name: 'Новый' });
// Шаблон по умолчанию — «Пустой»: структура комнат и ролей (AGENT.md 7.3).
expect(createCall?.body).toEqual({ name: 'Новый', template_id: 'empty' });
});
});