feat(files): загрузка и выдача вложений
- POST /api/v1/channels/{id}/files (multipart, ATTACH_FILES): файл пишется на
диск под идентификатором, в БД хранятся метаданные и sha256; имя файла
очищается от путей и управляющих символов, объём ограничен MAX_UPLOAD_SIZE;
- GET/HEAD /files/{id}: содержимое с проверкой прав на комнату сообщения
(невидимая комната и чужие вложения → 404), ETag, immutable-кэш,
Content-Disposition с RFC 5987 для не-ASCII имён;
- вложения привязываются к сообщению при отправке (attachment_ids), сироты
ищутся через store.ListOrphanFiles для обслуживания;
- GET /api/v1/files/{id} — метаданные файла для клиента;
- chi-ручки теперь умеют отдавать huma-ошибки в общем конверте
(writeHumaAPIError), иначе 404 превращался в 500;
- READY отдаёт реальные read states пользователя.
Тесты: загрузка, скачивание участником, скрытая комната → 404, вложение в
сообщении, проверка подписи ETag.
This commit is contained in:
@@ -115,13 +115,102 @@ describe('основной экран', () => {
|
||||
const panel = screen.getByLabelText('Панель пользователя');
|
||||
expect(within(panel).getByText('Alice')).toBeVisible();
|
||||
expect(within(panel).getByLabelText('Настройки пользователя')).toBeVisible();
|
||||
expect(within(panel).getByLabelText('Выйти')).toBeVisible();
|
||||
// Выход переехал во всплывающее меню (проверяется в tests/userMenu.test.tsx).
|
||||
expect(within(panel).getByLabelText('Открыть меню пользователя')).toBeVisible();
|
||||
|
||||
// Заглушка комнаты вместо чата Фазы 1: шапка комнаты + подсказка.
|
||||
expect(await screen.findByText('Чат появится в Фазе 2')).toBeVisible();
|
||||
expect(screen.getByRole('heading', { name: 'общий' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('анимационные классы не мешают рендеру оболочки', async () => {
|
||||
installFetch([
|
||||
{ match: '/api/v1/users/@me', response: () => json({ user }) },
|
||||
{ match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat-test' } }) },
|
||||
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
|
||||
]);
|
||||
const gateway = installGatewaySocket(snapshot);
|
||||
|
||||
renderApp('/app/g-1/c-general');
|
||||
await screen.findByTestId('guild-rail');
|
||||
await gateway.greet();
|
||||
|
||||
// Каскад списков и анимация появления страницы — обычные CSS-классы.
|
||||
expect(screen.getByTestId('guild-rail')).toHaveClass('gl-stagger');
|
||||
expect(document.querySelector('.gl-page')).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Открыть комнату общий')).toHaveClass('gl-indicator');
|
||||
});
|
||||
});
|
||||
|
||||
it('шестерёнка настроек сервера видна владельцу и ведёт в настройки', async () => {
|
||||
const { default: userEvent } = await import('@testing-library/user-event');
|
||||
const visitor = userEvent.setup();
|
||||
installFetch([
|
||||
{ match: '/api/v1/users/@me', response: () => json({ user }) },
|
||||
{ match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat-test' } }) },
|
||||
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
|
||||
{ match: '/api/v1/guilds/g-1/roles', response: () => json({ roles: [] }) },
|
||||
{ match: '/api/v1/guilds/g-1/members', response: () => json({ members: [] }) },
|
||||
{
|
||||
match: '/api/v1/guilds/g-1',
|
||||
response: () =>
|
||||
json({
|
||||
guild: {
|
||||
id: 'g-1',
|
||||
name: 'Основной сервер',
|
||||
description: 'главный',
|
||||
owner_id: 'user-1',
|
||||
is_main: true,
|
||||
member_count: 1,
|
||||
roles: [],
|
||||
my_role_ids: [],
|
||||
my_permissions: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const gateway = installGatewaySocket(snapshot);
|
||||
|
||||
const { router } = renderApp('/app/g-1/c-general');
|
||||
await screen.findByTestId('guild-rail');
|
||||
await gateway.greet();
|
||||
|
||||
const gear = await screen.findByLabelText('Настройки сервера');
|
||||
await visitor.click(gear);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(router.state.location.pathname).toBe('/settings/servers/g-1');
|
||||
});
|
||||
});
|
||||
|
||||
it('обычный участник не видит шестерёнку настроек сервера', async () => {
|
||||
const memberSnapshot = makeReadySnapshot([
|
||||
{
|
||||
id: 'g-9',
|
||||
name: 'Чужой сервер',
|
||||
owner_id: 'user-2',
|
||||
my_permissions: [],
|
||||
channels: [{ id: 'c-1', name: 'общий', position: 0 }],
|
||||
},
|
||||
]);
|
||||
installFetch([
|
||||
{ match: '/api/v1/users/@me', response: () => json({ user }) },
|
||||
{ match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat-test' } }) },
|
||||
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
|
||||
{ match: '/api/v1/guilds/g-9/channels', response: () => json({ channels: [] }) },
|
||||
]);
|
||||
const gateway = installGatewaySocket(memberSnapshot);
|
||||
|
||||
renderApp('/app/g-9/c-1');
|
||||
await screen.findByTestId('guild-rail');
|
||||
await gateway.greet();
|
||||
|
||||
await screen.findByLabelText('Открыть комнату общий');
|
||||
expect(screen.queryByLabelText('Настройки сервера')).toBeNull();
|
||||
expect(screen.queryByLabelText('Создать комнату')).toBeNull();
|
||||
});
|
||||
|
||||
it('пустой список серверов предлагает главный сервер и создание своего', async () => {
|
||||
installFetch([
|
||||
{ match: '/api/v1/users/@me', response: () => json({ user }) },
|
||||
|
||||
Reference in New Issue
Block a user