feat(webhooks): вебхуки комнат — бэкенд, клиент и настройки комнаты
- миграция 00012: таблица webhooks, снимок имени и аватара в messages (AGENT.md 7.11)
- API: список/создание/правка/удаление и пересоздание токена (MANAGE_WEBHOOKS,
step-up при создании, аудит), загрузка аватара отдельной multipart-ручкой
- исполнение POST /webhooks/{id}/{token} без сессии: content, username,
avatar_url, файлы создателя вебхука, лимит 30/мин на вебхук
- клиент: пункт настроек «Комната» со списком вебхуков, копированием ссылки,
пересозданием токена и удалением
- сообщения вебхуков: имя, аватар и значок в ленте вместо «неизвестного автора»
- fix: неполный ответ REST больше не затирает данные READY-снапшота
This commit is contained in:
@@ -21,10 +21,25 @@ const authors: AuthorDirectory = {
|
||||
isInstanceAdmin: false,
|
||||
isMember: true,
|
||||
}),
|
||||
resolveWebhook: () => ({
|
||||
id: '',
|
||||
name: 'вебхук',
|
||||
avatarFileId: undefined,
|
||||
colorHex: null,
|
||||
isInstanceAdmin: false,
|
||||
isMember: false,
|
||||
}),
|
||||
resolveMention: (userId) => ({ name: userId === 'user-1' ? 'Аля' : 'Bob', isMe: false }),
|
||||
loaded: true,
|
||||
};
|
||||
|
||||
/** Сообщение без автора-пользователя: так приходят сообщения вебхуков. */
|
||||
function authorlessMessage(overrides: Partial<Message> & { id: string }): Message {
|
||||
const base = message(overrides);
|
||||
delete base.author_id;
|
||||
return base;
|
||||
}
|
||||
|
||||
function message(overrides: Partial<Message> & { id: string }): Message {
|
||||
return {
|
||||
channel_id: 'c-1',
|
||||
@@ -196,3 +211,59 @@ describe('виды сообщений: действия', () => {
|
||||
expect(within(groups[1] as HTMLElement).getByText('Bob')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('сообщения вебхуков', () => {
|
||||
it('показывает имя и аватар вебхука, а не «неизвестного автора»', () => {
|
||||
const webhookAuthors: AuthorDirectory = {
|
||||
...authors,
|
||||
resolveWebhook: (message) => ({
|
||||
id: message.webhook_id ?? '',
|
||||
name: message.webhook_name ?? 'вебхук',
|
||||
avatarFileId: undefined,
|
||||
avatarUrl: message.webhook_avatar,
|
||||
colorHex: null,
|
||||
isInstanceAdmin: false,
|
||||
isMember: false,
|
||||
}),
|
||||
};
|
||||
render(
|
||||
<MessageList
|
||||
channelId="c-1"
|
||||
channelName="общий"
|
||||
messages={[
|
||||
authorlessMessage({
|
||||
id: 'w-1',
|
||||
type: 'webhook',
|
||||
content: 'сборка прошла',
|
||||
webhook_id: 'hook-1',
|
||||
webhook_name: 'Деплой',
|
||||
webhook_avatar: '/files/777',
|
||||
}),
|
||||
]}
|
||||
status="ready"
|
||||
error={null}
|
||||
hasMore={false}
|
||||
loadingMore={false}
|
||||
unreadAnchorId={null}
|
||||
highlightId={null}
|
||||
authors={webhookAuthors}
|
||||
currentUserId="user-1"
|
||||
canManageMessages
|
||||
canSend
|
||||
onReload={() => undefined}
|
||||
onLoadMore={() => undefined}
|
||||
onReply={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const body = article('w-1');
|
||||
expect(body).toHaveAttribute('data-message-type', 'webhook');
|
||||
// Имя, значок и аватар рисует шапка группы (само сообщение — только текст).
|
||||
const group = screen.getByTestId('message-group');
|
||||
expect(group).toHaveAttribute('data-message-type', 'webhook');
|
||||
expect(within(group).getByText('Деплой')).toBeVisible();
|
||||
expect(within(group).getByTestId('webhook-badge')).toHaveTextContent('вебхук');
|
||||
expect(group.querySelector('img')?.getAttribute('src')).toBe('/files/777');
|
||||
expect(within(group).queryByText('неизвестный')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user