import { beforeEach, describe, expect, it, vi } from 'vitest' import { MessagesApi } from './MessagesApi' import type { ApiClient } from '../../core/ApiClient' import type { Message, ForwardMessagesPayload, ForwardMessagesResult, SendVcardsPayload, SendReactionPayload, MagicTextPayload, MagicTextResult, CheckSummaryPayload, CheckSummaryResult, BulkDestroyPayload, OutOfRangeResult, } from './types' import type { Paginated } from '../../core/types' // ── Mock client factory ────────────────────────────────────────────── function createMockClient(): ApiClient { return { setAccessToken: vi.fn().mockReturnThis(), request: vi.fn(), get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn(), } } // ── Fixtures ───────────────────────────────────────────────────────── const message: Message = { id: 'msg-1', isFromMe: true, sent: true, type: 'chat', timestamp: '2024-01-01T00:00:00Z', data: {}, text: 'Hello', visible: true, origin: null, obfuscated: false, reactionParentMessageId: null, hsmId: null, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', deletedAt: null, } const paginated: Paginated = { data: [message], total: 1, limit: 10, skip: 0, currentPage: 1, lastPage: 1, from: 1, to: 1, } // ── Tests ──────────────────────────────────────────────────────────── describe('MessagesApi', () => { let client: ReturnType let api: MessagesApi beforeEach(() => { client = createMockClient() api = new MessagesApi(client) }) // ── getMany ────────────────────────────────────────────────────── describe('getMany', () => { it('calls client.get with /messages when no query is provided', async () => { vi.mocked(client.get).mockResolvedValue(paginated) const result = await api.getMany() expect(client.get).toHaveBeenCalledWith('/messages', undefined) expect(result).toEqual(paginated) }) it('serializes query parameters', async () => { vi.mocked(client.get).mockResolvedValue(paginated) await api.getMany({ page: 2, perPage: 5 }) const url = vi.mocked(client.get).mock.calls[0][0] as string expect(url).toContain('/messages?') expect(url).toContain('page=2') expect(url).toContain('perPage=5') }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue(paginated) const headers = { 'X-Custom': 'value' } await api.getMany(undefined, headers) expect(client.get).toHaveBeenCalledWith('/messages', headers) }) }) // ── getById ────────────────────────────────────────────────────── describe('getById', () => { it('calls client.get with /messages/:id', async () => { vi.mocked(client.get).mockResolvedValue(message) const result = await api.getById('msg-1') expect(client.get).toHaveBeenCalledWith('/messages/msg-1', undefined) expect(result).toEqual(message) }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue(message) const headers = { 'X-Tenant': 'abc' } await api.getById('msg-1', undefined, headers) expect(client.get).toHaveBeenCalledWith('/messages/msg-1', headers) }) }) // ── create ─────────────────────────────────────────────────────── describe('create', () => { it('calls client.post with /messages and body', async () => { vi.mocked(client.post).mockResolvedValue(message) const result = await api.create({ text: 'Hello', contactId: 'c-1' }) expect(client.post).toHaveBeenCalledWith( '/messages', { text: 'Hello', contactId: 'c-1' }, undefined, ) expect(result).toEqual(message) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(message) const headers = { 'X-Req': 'r-1' } await api.create({ text: 'Hi' }, headers) expect(client.post).toHaveBeenCalledWith('/messages', { text: 'Hi' }, headers) }) }) // ── updateById ─────────────────────────────────────────────────── describe('updateById', () => { it('calls client.put with /messages/:id and body', async () => { vi.mocked(client.put).mockResolvedValue(message) const result = await api.updateById('msg-1', { text: 'Updated' }) expect(client.put).toHaveBeenCalledWith('/messages/msg-1', { text: 'Updated' }, undefined) expect(result).toEqual(message) }) it('forwards custom headers', async () => { vi.mocked(client.put).mockResolvedValue(message) const headers = { 'If-Match': 'etag' } await api.updateById('msg-1', { text: 'x' }, headers) expect(client.put).toHaveBeenCalledWith('/messages/msg-1', { text: 'x' }, headers) }) }) // ── deleteById ─────────────────────────────────────────────────── describe('deleteById', () => { it('calls client.delete with /messages/:id', async () => { vi.mocked(client.delete).mockResolvedValue(message) const result = await api.deleteById('msg-1') expect(client.delete).toHaveBeenCalledWith('/messages/msg-1', undefined) expect(result).toEqual(message) }) it('forwards custom headers', async () => { vi.mocked(client.delete).mockResolvedValue(message) const headers = { 'X-Reason': 'cleanup' } await api.deleteById('msg-1', headers) expect(client.delete).toHaveBeenCalledWith('/messages/msg-1', headers) }) }) // ── forward ────────────────────────────────────────────────────── describe('forward', () => { it('calls client.post with /messages/forward and body', async () => { const result: ForwardMessagesResult = [{ id: 'r-1', messagesIds: ['msg-1'] }] vi.mocked(client.post).mockResolvedValue(result) const body: ForwardMessagesPayload = { contactsIds: ['c-1'], messagesIds: ['msg-1'] } const res = await api.forward(body) expect(client.post).toHaveBeenCalledWith('/messages/forward', body, undefined) expect(res).toEqual(result) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue([]) const headers = { 'X-Custom': 'h' } await api.forward({ contactsIds: [], messagesIds: [] }, headers) expect(client.post).toHaveBeenCalledWith( '/messages/forward', { contactsIds: [], messagesIds: [] }, headers, ) }) }) // ── revoke ─────────────────────────────────────────────────────── describe('revoke', () => { it('calls client.post with /messages/:id/revoke', async () => { vi.mocked(client.post).mockResolvedValue(message) const result = await api.revoke('msg-1') expect(client.post).toHaveBeenCalledWith('/messages/msg-1/revoke', undefined, undefined) expect(result).toEqual(message) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(message) const headers = { 'X-Custom': 'h' } await api.revoke('msg-1', headers) expect(client.post).toHaveBeenCalledWith('/messages/msg-1/revoke', undefined, headers) }) }) // ── revokeReaction ─────────────────────────────────────────────── describe('revokeReaction', () => { it('calls client.put with /messages/:id/revoke-reaction', async () => { vi.mocked(client.put).mockResolvedValue(true) const result = await api.revokeReaction('msg-1') expect(client.put).toHaveBeenCalledWith( '/messages/msg-1/revoke-reaction', undefined, undefined, ) expect(result).toBe(true) }) it('forwards custom headers', async () => { vi.mocked(client.put).mockResolvedValue(true) const headers = { 'X-Custom': 'h' } await api.revokeReaction('msg-1', headers) expect(client.put).toHaveBeenCalledWith('/messages/msg-1/revoke-reaction', undefined, headers) }) }) // ── sendVcards ─────────────────────────────────────────────────── describe('sendVcards', () => { it('calls client.post with /messages/send-vcards and body', async () => { vi.mocked(client.post).mockResolvedValue(message) const body: SendVcardsPayload = { serviceId: 's-1', contactId: 'c-1', contactsIds: ['c-2'] } const result = await api.sendVcards(body) expect(client.post).toHaveBeenCalledWith('/messages/send-vcards', body, undefined) expect(result).toEqual(message) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(message) const headers = { 'X-Custom': 'h' } const body: SendVcardsPayload = { serviceId: 's-1', contactId: 'c-1', contactsIds: [] } await api.sendVcards(body, headers) expect(client.post).toHaveBeenCalledWith('/messages/send-vcards', body, headers) }) }) // ── sendReaction ───────────────────────────────────────────────── describe('sendReaction', () => { it('calls client.post with /messages/:id/send-reaction and body', async () => { vi.mocked(client.post).mockResolvedValue(true) const body: SendReactionPayload = { reactionEmojiRendered: '👍' } const result = await api.sendReaction('msg-1', body) expect(client.post).toHaveBeenCalledWith('/messages/msg-1/send-reaction', body, undefined) expect(result).toBe(true) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(true) const headers = { 'X-Custom': 'h' } await api.sendReaction('msg-1', { reactionEmojiRendered: '❤️' }, headers) expect(client.post).toHaveBeenCalledWith( '/messages/msg-1/send-reaction', { reactionEmojiRendered: '❤️' }, headers, ) }) }) // ── magicText ──────────────────────────────────────────────────── describe('magicText', () => { it('calls client.post with /messages/magic-text and body', async () => { const magicResult: MagicTextResult = { answer: 'Improved text' } vi.mocked(client.post).mockResolvedValue(magicResult) const body: MagicTextPayload = { message: 'hello', tone: 'professional' } const result = await api.magicText(body) expect(client.post).toHaveBeenCalledWith('/messages/magic-text', body, undefined) expect(result).toEqual(magicResult) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue({ answer: null }) const headers = { 'X-Custom': 'h' } const body: MagicTextPayload = { message: 'test', tone: 'casual' } await api.magicText(body, headers) expect(client.post).toHaveBeenCalledWith('/messages/magic-text', body, headers) }) }) // ── checkSummary ───────────────────────────────────────────────── describe('checkSummary', () => { it('calls client.post with /messages/check-summary and body', async () => { const checkResult: CheckSummaryResult = { hasSummary: true } vi.mocked(client.post).mockResolvedValue(checkResult) const body: CheckSummaryPayload = { ticketId: 't-1' } const result = await api.checkSummary(body) expect(client.post).toHaveBeenCalledWith('/messages/check-summary', body, undefined) expect(result).toEqual(checkResult) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue({ hasSummary: false }) const headers = { 'X-Custom': 'h' } await api.checkSummary({ ticketId: 't-1' }, headers) expect(client.post).toHaveBeenCalledWith( '/messages/check-summary', { ticketId: 't-1' }, headers, ) }) }) // ── bulkDestroy ────────────────────────────────────────────────── describe('bulkDestroy', () => { it('calls client.request DELETE with /messages/many and body', async () => { vi.mocked(client.request).mockResolvedValue(true) const body: BulkDestroyPayload = { where: { ticketId: 't-1' } } const result = await api.bulkDestroy(body) expect(client.request).toHaveBeenCalledWith('DELETE', '/messages/many', body, undefined) expect(result).toBe(true) }) it('forwards custom headers', async () => { vi.mocked(client.request).mockResolvedValue(true) const headers = { 'X-Custom': 'h' } await api.bulkDestroy({ where: {} }, headers) expect(client.request).toHaveBeenCalledWith( 'DELETE', '/messages/many', { where: {} }, headers, ) }) }) // ── summarize ──────────────────────────────────────────────────── describe('summarize', () => { it('calls client.post with /messages/:id/summarize', async () => { vi.mocked(client.post).mockResolvedValue(undefined) await api.summarize('msg-1') expect(client.post).toHaveBeenCalledWith('/messages/msg-1/summarize', undefined, undefined) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(undefined) const headers = { 'X-Custom': 'h' } await api.summarize('msg-1', headers) expect(client.post).toHaveBeenCalledWith('/messages/msg-1/summarize', undefined, headers) }) }) // ── outOfRange ─────────────────────────────────────────────────── describe('outOfRange', () => { it('calls client.get with /messages/:id/out-of-range without limit', async () => { const result: OutOfRangeResult = { outOfRange: false } vi.mocked(client.get).mockResolvedValue(result) const res = await api.outOfRange('msg-1') expect(client.get).toHaveBeenCalledWith('/messages/msg-1/out-of-range', undefined) expect(res).toEqual(result) }) it('appends limit query parameter when provided', async () => { vi.mocked(client.get).mockResolvedValue({ outOfRange: true }) await api.outOfRange('msg-1', 50) const url = vi.mocked(client.get).mock.calls[0][0] as string expect(url).toContain('/messages/msg-1/out-of-range?') expect(url).toContain('limit=50') }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue({ outOfRange: false }) const headers = { 'X-Custom': 'h' } await api.outOfRange('msg-1', undefined, headers) expect(client.get).toHaveBeenCalledWith('/messages/msg-1/out-of-range', headers) }) }) // ── syncFile ───────────────────────────────────────────────────── describe('syncFile', () => { it('calls client.post with /messages/:id/sync-file', async () => { vi.mocked(client.post).mockResolvedValue({ ok: true }) const result = await api.syncFile('msg-1') expect(client.post).toHaveBeenCalledWith('/messages/msg-1/sync-file', undefined, undefined) expect(result).toEqual({ ok: true }) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(null) const headers = { 'X-Custom': 'h' } await api.syncFile('msg-1', headers) expect(client.post).toHaveBeenCalledWith('/messages/msg-1/sync-file', undefined, headers) }) }) // ── around ─────────────────────────────────────────────────────── describe('around', () => { it('calls client.get with /messages/:id/around without query', async () => { vi.mocked(client.get).mockResolvedValue([message]) const result = await api.around('msg-1') expect(client.get).toHaveBeenCalledWith('/messages/msg-1/around', undefined) expect(result).toEqual([message]) }) it('appends limit query parameter when provided', async () => { vi.mocked(client.get).mockResolvedValue([]) await api.around('msg-1', { limit: 10 }) const url = vi.mocked(client.get).mock.calls[0][0] as string expect(url).toContain('/messages/msg-1/around?') expect(url).toContain('limit=10') }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue([]) const headers = { 'X-Custom': 'h' } await api.around('msg-1', undefined, headers) expect(client.get).toHaveBeenCalledWith('/messages/msg-1/around', headers) }) }) // ── Error propagation ──────────────────────────────────────────── describe('error propagation', () => { it('propagates errors from getMany', async () => { vi.mocked(client.get).mockRejectedValue(new Error('Network error')) await expect(api.getMany()).rejects.toThrow('Network error') }) it('propagates errors from forward', async () => { vi.mocked(client.post).mockRejectedValue(new Error('Bad request')) await expect(api.forward({ contactsIds: [], messagesIds: [] })).rejects.toThrow('Bad request') }) it('propagates errors from bulkDestroy', async () => { vi.mocked(client.request).mockRejectedValue(new Error('Forbidden')) await expect(api.bulkDestroy({ where: {} })).rejects.toThrow('Forbidden') }) }) })