/** * Серверная идемпотентность сообщений (вариант C, миграция 0086): * повторная отправка с тем же clientMessageId НЕ создаёт дубль — * sendTaskMessage возвращает уже существующее сообщение без side-эффектов. * * Два сценария: * 1. Запись уже есть (быстрый путь: select до insert). * 2. Гонка: параллельный insert упал на unique index (23505) — fallback на select. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mockStorage = vi.hoisted(() => ({ getTaskMessageIdByClientMessageId: vi.fn(), getTaskMessage: vi.fn(), createTaskMessage: vi.fn(), })); vi.mock('../server/storage', () => ({ storage: mockStorage })); import { sendTaskMessage } from '../server/services/task-message.service'; const task = { id: 1988, formId: 14 } as never; const user = { id: 1, firstName: 'Ильяс', lastName: 'Султанов' } as never; const existingMessage = { id: 994, taskId: 1988, message: 'тест', clientMessageId: 'uuid-1' }; const baseParams = { task, user, organizationId: 1, message: 'тест', clientMessageId: 'uuid-1', }; describe('sendTaskMessage — идемпотентность по clientMessageId', () => { beforeEach(() => { vi.clearAllMocks(); }); it('повтор с тем же ключом: возвращает существующее сообщение, insert НЕ вызывается', async () => { mockStorage.getTaskMessageIdByClientMessageId.mockResolvedValue(994); mockStorage.getTaskMessage.mockResolvedValue(existingMessage); const result = await sendTaskMessage(baseParams); expect(result).toBe(existingMessage); expect(mockStorage.createTaskMessage).not.toHaveBeenCalled(); expect(mockStorage.getTaskMessageIdByClientMessageId).toHaveBeenCalledWith(1988, 'uuid-1'); }); it('гонка: unique violation (23505) на insert → fallback возвращает существующее', async () => { mockStorage.getTaskMessageIdByClientMessageId .mockResolvedValueOnce(null) // select до insert — ещё пусто .mockResolvedValueOnce(994); // select после unique-конфликта — уже вставлено параллельным запросом const pgError = Object.assign(new Error('duplicate key value violates unique constraint'), { code: '23505' }); mockStorage.createTaskMessage.mockRejectedValue(pgError); mockStorage.getTaskMessage.mockResolvedValue(existingMessage); const result = await sendTaskMessage(baseParams); expect(result).toBe(existingMessage); expect(mockStorage.createTaskMessage).toHaveBeenCalledTimes(1); }); it('другие ошибки insert пробрасываются наружу', async () => { mockStorage.getTaskMessageIdByClientMessageId.mockResolvedValue(null); mockStorage.createTaskMessage.mockRejectedValue(new Error('connection refused')); await expect(sendTaskMessage(baseParams)).rejects.toThrow('connection refused'); }); });