Files
glchat/web/tests/api.test.ts
T
grendervill 5320618d31 feat(web): статистика, карточка пользователя и фильтры аудита в админ-панели (Фаза 7)
Новые разделы панели инстанса (AGENT.md 3.2, 7.18):

- вкладка «Статистика»: рост пользователей и сообщений по дням, размеры
  данных, топы серверов;
- карточка пользователя из списка: устройства с отзывом, серверы и роли,
  события безопасности, аудит по пользователю; отзыв устройства и сброс
  второго фактора подтверждаются step-up;
- вкладка «Аудит»: фильтры по действию, актору, цели, серверу и датам,
  пагинация и ссылка на выгрузку CSV;
- вкладка «Серверы»: поиск по названию, фильтр по владельцу и главному
  серверу, пагинация;
- ключи i18n для новых разделов (ru/en, паритет), тесты панели.
2026-09-26 16:48:00 +03:00

184 lines
6.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { enableTotp, login, logoutAll, register, setupTotp, stepUp } from '@/api/auth';
import { createChannel, fetchChannels, joinGuild, leaveGuild } from '@/api/guilds';
import { fetchInstance, fetchAudit } from '@/api/instance';
import { fetchMyGuilds, updateProfile } from '@/api/users';
import { ApiError } from '@/api/client';
import { findRequest, installFetch, json, recordedRequests } from './helpers';
function lastCall(fetchMock: ReturnType<typeof installFetch>) {
const request = recordedRequests(fetchMock).at(-1);
if (request === undefined) {
throw new Error('fetch не вызывался');
}
return request;
}
describe('api-модули Фазы 1', () => {
it('register/login отправляют POST с телом и читают поле user', async () => {
const fetchMock = installFetch([
{
match: '/api/v1/auth/register',
method: 'POST',
response: () => json({ user: { id: 'u' } }),
},
]);
await expect(
register({
username: 'alice',
display_name: 'Alice',
email: 'a@b.co',
password: 'password12',
locale: 'ru',
}),
).resolves.toEqual({ user: { id: 'u' } });
expect(findRequest(fetchMock, { url: '/auth/register', method: 'POST' })?.method).toBe('POST');
});
it('login отправляет POST с телом', async () => {
const fetchMock = installFetch([
{ match: '/api/v1/auth/login', method: 'POST', response: () => json({ user: { id: 'u' } }) },
]);
await login({ email: 'a@b.co', password: 'password12' });
expect(findRequest(fetchMock, { url: '/auth/login', method: 'POST' })?.body).toEqual({
email: 'a@b.co',
password: 'password12',
});
});
it('login прокидывает код 2FA как totp_code', async () => {
const fetchMock = installFetch([
{ match: '/api/v1/auth/login', method: 'POST', response: () => json({ user: { id: 'u' } }) },
]);
await login({ email: 'a@b.co', password: 'password12', totp_code: '123456' });
expect(lastCall(fetchMock).body).toEqual({
email: 'a@b.co',
password: 'password12',
totp_code: '123456',
});
});
it('setup/enable 2FA и step-up используют нужные пути', async () => {
const fetchMock = installFetch([
{
match: '/api/v1/auth/2fa/setup',
method: 'POST',
response: () => json({ secret: 'SECRET', otpauth_url: 'otpauth://x' }),
},
{
match: '/api/v1/auth/2fa/enable',
method: 'POST',
response: () => json({ recovery_codes: ['one', 'two'] }),
},
{ match: '/api/v1/auth/step-up', method: 'POST', response: () => json({ ok: true }) },
{ match: '/api/v1/auth/logout-all', method: 'POST', response: () => json({ ok: true }) },
]);
await expect(setupTotp()).resolves.toEqual({ secret: 'SECRET', otpauth_url: 'otpauth://x' });
await expect(enableTotp('123456')).resolves.toEqual(['one', 'two']);
await stepUp('password12', '654321');
expect(lastCall(fetchMock).body).toEqual({
password: 'password12',
totp_code: '654321',
});
await expect(logoutAll()).resolves.toEqual({ ok: true });
});
it('PATCH профиля уходит на /users/@me и возвращает user', async () => {
const fetchMock = installFetch([
{
match: '/api/v1/users/@me',
method: 'PATCH',
response: () => json({ user: { id: 'u', display_name: 'Новое' } }),
},
]);
await expect(updateProfile({ display_name: 'Новое' })).resolves.toMatchObject({
display_name: 'Новое',
});
expect(lastCall(fetchMock).method).toBe('PATCH');
});
it('GET /users/@me/guilds разворачивает конверт {guilds}', async () => {
installFetch([
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [{ id: 'g-1' }] }) },
]);
await expect(fetchMyGuilds()).resolves.toEqual([{ id: 'g-1' }]);
});
it('каналы и участие в сервере используют id в пути', async () => {
const fetchMock = installFetch([
{ match: '/api/v1/guilds/g-1/channels', response: () => json({ channels: [{ id: 'c-1' }] }) },
{ match: '/api/v1/guilds/g-1/join', method: 'POST', response: () => json({ ok: true }) },
{ match: '/api/v1/guilds/g-1/leave', method: 'POST', response: () => json({ ok: true }) },
]);
await expect(fetchChannels('g-1')).resolves.toEqual([{ id: 'c-1' }]);
await expect(joinGuild('g-1')).resolves.toEqual({ ok: true });
await expect(leaveGuild('g-1')).resolves.toEqual({ ok: true });
expect(lastCall(fetchMock).url).toContain('/api/v1/guilds/g-1/leave');
});
it('createChannel отправляет type и parent_id', async () => {
const fetchMock = installFetch([
{
match: '/api/v1/guilds/g-1/channels',
method: 'POST',
response: () => json({ channel: { id: 'c-9' } }),
},
]);
await createChannel('g-1', { name: 'общий', type: 'text', parent_id: 'cat-1' });
expect(lastCall(fetchMock).body).toEqual({
name: 'общий',
type: 'text',
parent_id: 'cat-1',
});
});
it('instance: конверты {instance}, {settings}, {entries} и лимит аудита', async () => {
const fetchMock = installFetch([
{ match: '/api/v1/instance/settings', response: () => json({ settings: { motd: 'x' } }) },
{ match: '/api/v1/instance/audit', response: () => json({ entries: [{ id: 'a-1' }] }) },
{ match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat' } }) },
]);
await expect(fetchInstance()).resolves.toEqual({ name: 'glchat' });
// Журнал отдаётся вместе с общим числом записей под фильтр (AGENT.md 7.18).
await expect(fetchAudit({ limit: 50 })).resolves.toEqual({
entries: [{ id: 'a-1' }],
total: 1,
});
expect(recordedRequests(fetchMock).map((request) => request.url)).toContain(
'/api/v1/instance/audit?limit=50',
);
});
it('пробрасывает коды ошибок контракта (в том числе auth.guild_limit_reached)', async () => {
installFetch([
{
match: '/api/v1/guilds',
method: 'POST',
response: () =>
json({ error: { code: 'auth.guild_limit_reached', message: 'limit' } }, 403),
},
]);
const error = await import('@/api/guilds')
.then(({ createGuild }) => createGuild('Много'))
.catch((reason: unknown) => reason);
expect(error).toBeInstanceOf(ApiError);
expect((error as ApiError).code).toBe('auth.guild_limit_reached');
expect((error as ApiError).status).toBe(403);
});
});