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:
@@ -56,7 +56,7 @@ describe('настройки: безопасность', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
renderApp('/settings/security');
|
||||
renderApp('/settings/account/security');
|
||||
const visitor = userEvent.setup();
|
||||
|
||||
await visitor.click(await screen.findByRole('button', { name: 'Включить 2FA' }));
|
||||
@@ -85,7 +85,7 @@ describe('настройки: безопасность', () => {
|
||||
]);
|
||||
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
const { router } = renderApp('/settings/security');
|
||||
const { router } = renderApp('/settings/account/security');
|
||||
const visitor = userEvent.setup();
|
||||
|
||||
expect(await screen.findByTestId('sessions-list')).toHaveTextContent('Firefox on Linux');
|
||||
@@ -121,7 +121,7 @@ describe('настройки: безопасность', () => {
|
||||
{ match: '/api/v1/auth/step-up', method: 'POST', response: () => json({ ok: true }) },
|
||||
]);
|
||||
|
||||
renderApp('/settings/security');
|
||||
renderApp('/settings/account/security');
|
||||
const visitor = userEvent.setup();
|
||||
|
||||
await visitor.type(await screen.findByLabelText('Текущий пароль'), 'old-password-1');
|
||||
@@ -152,7 +152,7 @@ describe('настройки: безопасность', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
renderApp('/settings/profile');
|
||||
renderApp('/settings/account/profile');
|
||||
const visitor = userEvent.setup();
|
||||
|
||||
const displayName = await screen.findByLabelText('Отображаемое имя');
|
||||
@@ -168,12 +168,55 @@ describe('настройки: безопасность', () => {
|
||||
it('вкладка «Инстанс» скрыта от не-администратора', async () => {
|
||||
installFetch([{ match: '/api/v1/users/@me', response: () => json({ user }) }]);
|
||||
|
||||
renderApp('/settings/profile');
|
||||
renderApp('/settings/account/profile');
|
||||
|
||||
expect(await screen.findByRole('link', { name: 'Профиль' })).toBeVisible();
|
||||
expect(screen.queryByRole('link', { name: 'Инстанс' })).toBeNull();
|
||||
});
|
||||
|
||||
it('вверху настроек — две соседние кнопки-переключателя', async () => {
|
||||
installFetch([
|
||||
{ match: '/api/v1/users/@me', response: () => json({ user }) },
|
||||
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
|
||||
]);
|
||||
|
||||
const { router } = renderApp('/settings/account/profile');
|
||||
const account = await screen.findByRole('link', { name: 'Настройки сервера' });
|
||||
const servers = screen.getByRole('link', { name: 'Мои серверы' });
|
||||
|
||||
expect(account).toHaveAttribute('aria-current', 'page');
|
||||
expect(servers).not.toHaveAttribute('aria-current');
|
||||
expect(screen.getByTestId('settings-switcher-backdrop')).toHaveClass('translate-x-0');
|
||||
|
||||
await userEvent.click(servers);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(router.state.location.pathname).toBe('/settings/servers');
|
||||
});
|
||||
expect(screen.getByRole('link', { name: 'Мои серверы' })).toHaveAttribute(
|
||||
'aria-current',
|
||||
'page',
|
||||
);
|
||||
// Подложка переключателя уезжает ко второй кнопке без перезагрузки.
|
||||
expect(screen.getByTestId('settings-switcher-backdrop')).toHaveClass('translate-x-full');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['/settings', '/settings/account/profile'],
|
||||
['/settings/profile', '/settings/account/profile'],
|
||||
['/settings/security', '/settings/account/security'],
|
||||
['/settings/appearance', '/settings/account/appearance'],
|
||||
['/settings/instance', '/settings/account/instance'],
|
||||
])('старый путь %s редиректит на %s', async (from, to) => {
|
||||
installFetch([{ match: '/api/v1/users/@me', response: () => json({ user }) }]);
|
||||
|
||||
const { router } = renderApp(from);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(router.state.location.pathname).toBe(to);
|
||||
});
|
||||
});
|
||||
|
||||
it('администратор видит данные инстанса', async () => {
|
||||
const admin = makeUser({ is_instance_admin: true });
|
||||
installFetch([
|
||||
@@ -220,7 +263,7 @@ describe('настройки: безопасность', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
renderApp('/settings/instance');
|
||||
renderApp('/settings/account/instance');
|
||||
|
||||
expect(await screen.findByTestId('instance-guilds')).toHaveTextContent('Main');
|
||||
expect(screen.getByTestId('instance-users')).toHaveTextContent('Alice');
|
||||
|
||||
Reference in New Issue
Block a user