import { beforeEach, describe, expect, it, vi } from 'vitest' import { ContactsApi } from './ContactsApi' import type { ApiClient } from '../../core/ApiClient' import type { Contact, CreateContactPayload, ExportTemplatePayload, CountContactsQuery, ExistsContactsParams, CountMediaResult, } 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 contact: Contact = { id: 'cnt-1', idFromService: null, name: 'Alice', internalName: null, alternativeName: null, isGroup: false, isBroadcast: false, isMe: false, isMyContact: true, hadChat: true, visible: true, status: 'online', isSilenced: false, data: {}, note: null, unread: 0, lastMessageAt: null, unsubscribed: false, lastContactMessageAt: null, acceptedTermAt: null, hsmExpirationTime: null, block: null, dataBlock: null, origin: null, archivedAt: null, accountId: 'acc-1', serviceId: 'svc-1', defaultDepartmentId: null, defaultUserId: null, currentTicketId: null, lastMessageId: null, personId: null, contactBlockListControlId: null, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', deletedAt: null, } const paginated: Paginated = { data: [contact], total: 1, limit: 10, skip: 0, currentPage: 1, lastPage: 1, from: 1, to: 1, } // ── Tests ──────────────────────────────────────────────────────────── describe('ContactsApi', () => { let client: ReturnType let api: ContactsApi beforeEach(() => { client = createMockClient() api = new ContactsApi(client) }) // ── getMany ────────────────────────────────────────────────────── describe('getMany', () => { it('calls client.get with /contacts when no query is provided', async () => { vi.mocked(client.get).mockResolvedValue(paginated) const result = await api.getMany() expect(client.get).toHaveBeenCalledWith('/contacts', 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('/contacts?') expect(url).toContain('page=2') }) 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('/contacts', headers) }) }) // ── getById ────────────────────────────────────────────────────── describe('getById', () => { it('calls client.get with /contacts/:id', async () => { vi.mocked(client.get).mockResolvedValue(contact) const result = await api.getById('cnt-1') expect(client.get).toHaveBeenCalledWith('/contacts/cnt-1', undefined) expect(result).toEqual(contact) }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue(contact) const headers = { 'X-Tenant': 'abc' } await api.getById('cnt-1', undefined, headers) expect(client.get).toHaveBeenCalledWith('/contacts/cnt-1', headers) }) }) // ── create ─────────────────────────────────────────────────────── describe('create', () => { it('calls client.post with /contacts and body', async () => { vi.mocked(client.post).mockResolvedValue(contact) const body: CreateContactPayload = { serviceId: 'svc-1', name: 'Alice' } const result = await api.create(body) expect(client.post).toHaveBeenCalledWith('/contacts', body, undefined) expect(result).toEqual(contact) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(contact) const headers = { 'X-Req': 'r-1' } await api.create({ serviceId: 'svc-1' }, headers) expect(client.post).toHaveBeenCalledWith('/contacts', { serviceId: 'svc-1' }, headers) }) }) // ── updateById ─────────────────────────────────────────────────── describe('updateById', () => { it('calls client.put with /contacts/:id and body', async () => { vi.mocked(client.put).mockResolvedValue(contact) const result = await api.updateById('cnt-1', { name: 'Alice Updated' }) expect(client.put).toHaveBeenCalledWith( '/contacts/cnt-1', { name: 'Alice Updated' }, undefined, ) expect(result).toEqual(contact) }) it('forwards custom headers', async () => { vi.mocked(client.put).mockResolvedValue(contact) const headers = { 'If-Match': 'etag' } await api.updateById('cnt-1', { name: 'x' }, headers) expect(client.put).toHaveBeenCalledWith('/contacts/cnt-1', { name: 'x' }, headers) }) }) // ── deleteById ─────────────────────────────────────────────────── describe('deleteById', () => { it('calls client.delete with /contacts/:id', async () => { vi.mocked(client.delete).mockResolvedValue(contact) const result = await api.deleteById('cnt-1') expect(client.delete).toHaveBeenCalledWith('/contacts/cnt-1', undefined) expect(result).toEqual(contact) }) it('forwards custom headers', async () => { vi.mocked(client.delete).mockResolvedValue(contact) const headers = { 'X-Reason': 'cleanup' } await api.deleteById('cnt-1', headers) expect(client.delete).toHaveBeenCalledWith('/contacts/cnt-1', headers) }) }) // ── exportTemplate ─────────────────────────────────────────────── describe('exportTemplate', () => { it('calls client.post with /contacts/export-template and body', async () => { vi.mocked(client.post).mockResolvedValue({ ok: true }) const body: ExportTemplatePayload = { serviceType: 'whatsapp', type: 'csv' } const result = await api.exportTemplate(body) expect(client.post).toHaveBeenCalledWith('/contacts/export-template', body, undefined) expect(result).toEqual({ ok: true }) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(null) const headers = { 'X-Custom': 'h' } const body: ExportTemplatePayload = { serviceType: 'webchat', type: 'csv' } await api.exportTemplate(body, headers) expect(client.post).toHaveBeenCalledWith('/contacts/export-template', body, headers) }) }) // ── count ──────────────────────────────────────────────────────── describe('count', () => { it('calls client.get with /contacts/count when no query is provided', async () => { vi.mocked(client.get).mockResolvedValue({ count: 42 }) const result = await api.count() expect(client.get).toHaveBeenCalledWith('/contacts/count', undefined) expect(result).toEqual({ count: 42 }) }) it('serializes query parameters', async () => { vi.mocked(client.get).mockResolvedValue({ count: 10 }) const query: CountContactsQuery = { where: { name: 'Alice' } } await api.count(query) const url = vi.mocked(client.get).mock.calls[0][0] as string expect(url).toContain('/contacts/count?') expect(url).toContain('where') }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue({ count: 0 }) const headers = { 'X-Custom': 'h' } await api.count(undefined, headers) expect(client.get).toHaveBeenCalledWith('/contacts/count', headers) }) }) // ── countPost ──────────────────────────────────────────────────── describe('countPost', () => { it('calls client.post with /contacts/count and body', async () => { vi.mocked(client.post).mockResolvedValue({ count: 5 }) const body: CountContactsQuery = { where: { name: 'Alice' } } const result = await api.countPost(body) expect(client.post).toHaveBeenCalledWith('/contacts/count', body, undefined) expect(result).toEqual({ count: 5 }) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue({ count: 0 }) const headers = { 'X-Custom': 'h' } await api.countPost({}, headers) expect(client.post).toHaveBeenCalledWith('/contacts/count', {}, headers) }) }) // ── forward ────────────────────────────────────────────────────── describe('forward', () => { it('calls client.get with /contacts/forward when no query is provided', async () => { vi.mocked(client.get).mockResolvedValue(paginated) const result = await api.forward() expect(client.get).toHaveBeenCalledWith('/contacts/forward', undefined) expect(result).toEqual(paginated) }) it('serializes query parameters', async () => { vi.mocked(client.get).mockResolvedValue(paginated) await api.forward({ page: 1, perPage: 10 }) const url = vi.mocked(client.get).mock.calls[0][0] as string expect(url).toContain('/contacts/forward?') }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue(paginated) const headers = { 'X-Custom': 'h' } await api.forward(undefined, headers) expect(client.get).toHaveBeenCalledWith('/contacts/forward', headers) }) }) // ── exists ─────────────────────────────────────────────────────── describe('exists', () => { it('calls client.get with /contacts/exists and query params', async () => { vi.mocked(client.get).mockResolvedValue({ '1234567890': true }) const params: ExistsContactsParams = { serviceId: 'svc-1', numbers: ['1234567890'] } const result = await api.exists(params) const url = vi.mocked(client.get).mock.calls[0][0] as string expect(url).toContain('/contacts/exists?') expect(url).toContain('serviceId=svc-1') expect(result).toEqual({ '1234567890': true }) }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue({}) const headers = { 'X-Custom': 'h' } await api.exists({ serviceId: 'svc-1', numbers: [] }, headers) expect(vi.mocked(client.get).mock.calls[0][1]).toEqual(headers) }) }) // ── countMedia ─────────────────────────────────────────────────── describe('countMedia', () => { it('calls client.get with /contacts/:id/media/count', async () => { const mediaResult: CountMediaResult = { image: 5, video: 2 } vi.mocked(client.get).mockResolvedValue(mediaResult) const result = await api.countMedia('cnt-1') expect(client.get).toHaveBeenCalledWith('/contacts/cnt-1/media/count', undefined) expect(result).toEqual(mediaResult) }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue({}) const headers = { 'X-Custom': 'h' } await api.countMedia('cnt-1', headers) expect(client.get).toHaveBeenCalledWith('/contacts/cnt-1/media/count', headers) }) }) // ── list ───────────────────────────────────────────────────────── describe('list', () => { it('calls client.post with /contacts/list and body', async () => { vi.mocked(client.post).mockResolvedValue(paginated) const body = { where: { name: 'Alice' } } const result = await api.list(body) expect(client.post).toHaveBeenCalledWith('/contacts/list', body, undefined) expect(result).toEqual(paginated) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(paginated) const headers = { 'X-Custom': 'h' } await api.list({}, headers) expect(client.post).toHaveBeenCalledWith('/contacts/list', {}, headers) }) }) describe('transferTicket', () => { it('calls client.post with /contacts/:id/ticket/transfer and payload', async () => { vi.mocked(client.post).mockResolvedValue({ ok: true }) const result = await api.transferTicket('c1', { departmentId: 'd1' }) expect(client.post).toHaveBeenCalledWith( '/contacts/c1/ticket/transfer', { departmentId: 'd1' }, undefined, ) expect(result).toEqual({ ok: true }) }) it('includes optional userId and forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue({ ok: true }) const headers = { 'X-Custom': 'h' } await api.transferTicket('c1', { departmentId: 'd1', userId: 'u1' }, headers) expect(client.post).toHaveBeenCalledWith( '/contacts/c1/ticket/transfer', { departmentId: 'd1', userId: 'u1' }, 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 count', async () => { vi.mocked(client.get).mockRejectedValue(new Error('Forbidden')) await expect(api.count()).rejects.toThrow('Forbidden') }) it('propagates errors from exportTemplate', async () => { vi.mocked(client.post).mockRejectedValue(new Error('Bad request')) await expect(api.exportTemplate({ serviceType: 'whatsapp', type: 'csv' })).rejects.toThrow( 'Bad request', ) }) }) })