import { beforeEach, describe, expect, it, vi } from 'vitest' import { CampaignsApi } from './CampaignsApi' import type { ApiClient } from '../../core/ApiClient' import type { Campaign, CreateCampaignPayload, CampaignStats, SetIntervalPayload, AutoPauseModePayload, ExportCampaignResultPayload, ExportTemplateCsvPayload, HsmLimit, } 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 campaign: Campaign = { id: 'cmp-1', title: 'Test Campaign', status: 'ready', config: { minInterval: 1000, maxInterval: 5000 }, sendsAt: null, isScheduled: false, startedAt: null, finishedAt: null, totalMessagesCount: null, sentMessagesCount: null, totalContacts: null, totalContactsImported: null, totalValidContacts: null, sentContacts: null, viewedContacts: null, contactCount: null, mustOpenTicket: false, marketingIntegration: false, accountId: 'acc-1', serviceId: 'svc-1', defaultDepartmentId: null, defaultUserId: null, createdById: null, sentById: null, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', deletedAt: null, } const paginated: Paginated = { data: [campaign], total: 1, limit: 10, skip: 0, currentPage: 1, lastPage: 1, from: 1, to: 1, } // ── Tests ──────────────────────────────────────────────────────────── describe('CampaignsApi', () => { let client: ReturnType let api: CampaignsApi beforeEach(() => { client = createMockClient() api = new CampaignsApi(client) }) // ── getMany ────────────────────────────────────────────────────── describe('getMany', () => { it('calls client.get with /campaigns when no query is provided', async () => { vi.mocked(client.get).mockResolvedValue(paginated) const result = await api.getMany() expect(client.get).toHaveBeenCalledWith('/campaigns', 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('/campaigns?') 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('/campaigns', headers) }) }) // ── getById ────────────────────────────────────────────────────── describe('getById', () => { it('calls client.get with /campaigns/:id', async () => { vi.mocked(client.get).mockResolvedValue(campaign) const result = await api.getById('cmp-1') expect(client.get).toHaveBeenCalledWith('/campaigns/cmp-1', undefined) expect(result).toEqual(campaign) }) it('forwards custom headers', async () => { vi.mocked(client.get).mockResolvedValue(campaign) const headers = { 'X-Tenant': 'abc' } await api.getById('cmp-1', undefined, headers) expect(client.get).toHaveBeenCalledWith('/campaigns/cmp-1', headers) }) }) // ── create ─────────────────────────────────────────────────────── describe('create', () => { it('calls client.post with /campaigns and body', async () => { vi.mocked(client.post).mockResolvedValue(campaign) const body: CreateCampaignPayload = { title: 'New Campaign', serviceId: 'svc-1' } const result = await api.create(body) expect(client.post).toHaveBeenCalledWith('/campaigns', body, undefined) expect(result).toEqual(campaign) }) it('forwards custom headers', async () => { vi.mocked(client.post).mockResolvedValue(campaign) const headers = { 'X-Req': 'r-1' } await api.create({ title: 'x', serviceId: 'svc-1' }, headers) expect(client.post).toHaveBeenCalledWith( '/campaigns', { title: 'x', serviceId: 'svc-1' }, headers, ) }) }) // ── updateById ─────────────────────────────────────────────────── describe('updateById', () => { it('calls client.put with /campaigns/:id and body', async () => { vi.mocked(client.put).mockResolvedValue(campaign) const result = await api.updateById('cmp-1', { title: 'Updated' }) expect(client.put).toHaveBeenCalledWith('/campaigns/cmp-1', { title: 'Updated' }, undefined) expect(result).toEqual(campaign) }) it('forwards custom headers', async () => { vi.mocked(client.put).mockResolvedValue(campaign) const headers = { 'If-Match': 'etag' } await api.updateById('cmp-1', { title: 'x' }, headers) expect(client.put).toHaveBeenCalledWith('/campaigns/cmp-1', { title: 'x' }, headers) }) }) // ── deleteById ─────────────────────────────────────────────────── describe('deleteById', () => { it('calls client.delete with /campaigns/:id', async () => { vi.mocked(client.delete).mockResolvedValue(campaign) const result = await api.deleteById('cmp-1') expect(client.delete).toHaveBeenCalledWith('/campaigns/cmp-1', undefined) expect(result).toEqual(campaign) }) it('forwards custom headers', async () => { vi.mocked(client.delete).mockResolvedValue(campaign) const headers = { 'X-Reason': 'cleanup' } await api.deleteById('cmp-1', headers) expect(client.delete).toHaveBeenCalledWith('/campaigns/cmp-1', headers) }) }) // ── sendById ───────────────────────────────────────────────────── describe('sendById', () => { it('calls client.post with /campaigns/:id/send', async () => { vi.mocked(client.post).mockResolvedValue(campaign) const result = await api.sendById('cmp-1') expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/send') expect(result).toEqual(campaign) }) }) // ── pauseById ──────────────────────────────────────────────────── describe('pauseById', () => { it('calls client.post with /campaigns/:id/pause', async () => { vi.mocked(client.post).mockResolvedValue(campaign) const result = await api.pauseById('cmp-1') expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/pause') expect(result).toEqual(campaign) }) }) // ── resumeById ─────────────────────────────────────────────────── describe('resumeById', () => { it('calls client.post with /campaigns/:id/resume', async () => { vi.mocked(client.post).mockResolvedValue(campaign) const result = await api.resumeById('cmp-1') expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/resume') expect(result).toEqual(campaign) }) }) // ── duplicateById ──────────────────────────────────────────────── describe('duplicateById', () => { it('calls client.post with /campaigns/:id/duplicate', async () => { vi.mocked(client.post).mockResolvedValue(campaign) const result = await api.duplicateById('cmp-1') expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/duplicate') expect(result).toEqual(campaign) }) }) // ── getStatsById ───────────────────────────────────────────────── describe('getStatsById', () => { it('calls client.get with /campaigns/:id/stats', async () => { const stats: CampaignStats = { campaignInfo: { totalValidContacts: 100, totalContacts: 110, totalContactsImported: 120, firedSuccessCount: 90, firedFailedCount: 10, totalMessagesCount: 100, totalFiredCount: 100, firedBlockedByMessageRuleCount: 0, }, messageInfo: { error: 5, received: 85, viewed: 70, answered: 30 }, } vi.mocked(client.get).mockResolvedValue(stats) const result = await api.getStatsById('cmp-1') expect(client.get).toHaveBeenCalledWith('/campaigns/cmp-1/stats') expect(result).toEqual(stats) }) }) // ── setIntervalById ────────────────────────────────────────────── describe('setIntervalById', () => { it('calls client.post with /campaigns/:id/interval and body', async () => { vi.mocked(client.post).mockResolvedValue(campaign) const data: SetIntervalPayload = { minInterval: '1000', maxInterval: '5000' } const result = await api.setIntervalById('cmp-1', data) expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/interval', data) expect(result).toEqual(campaign) }) }) // ── getContactCount ────────────────────────────────────────────── describe('getContactCount', () => { it('calls client.get with /campaigns/:id/contactCount', async () => { vi.mocked(client.get).mockResolvedValue(42) const result = await api.getContactCount('cmp-1') expect(client.get).toHaveBeenCalledWith('/campaigns/cmp-1/contactCount') expect(result).toBe(42) }) }) // ── setAutoPauseMode ───────────────────────────────────────────── describe('setAutoPauseMode', () => { it('calls client.put with /campaigns/autoPauseMode and body', async () => { vi.mocked(client.put).mockResolvedValue({ ok: true }) const data: AutoPauseModePayload = { mode: 'auto' } const result = await api.setAutoPauseMode(data) expect(client.put).toHaveBeenCalledWith('/campaigns/autoPauseMode', data) expect(result).toEqual({ ok: true }) }) }) // ── exportCampaignResult ───────────────────────────────────────── describe('exportCampaignResult', () => { it('calls client.post with /campaigns/export/csv and body', async () => { vi.mocked(client.post).mockResolvedValue({ ok: true }) const data: ExportCampaignResultPayload = { campaignId: 'cmp-1' } const result = await api.exportCampaignResult(data) expect(client.post).toHaveBeenCalledWith('/campaigns/export/csv', data) expect(result).toEqual({ ok: true }) }) }) // ── exportTemplateCsv ──────────────────────────────────────────── describe('exportTemplateCsv', () => { it('calls client.post with /campaigns/exportTemplate and body', async () => { vi.mocked(client.post).mockResolvedValue({ ok: true }) const data: ExportTemplateCsvPayload = { hsmId: 'hsm-1' } const result = await api.exportTemplateCsv(data) expect(client.post).toHaveBeenCalledWith('/campaigns/exportTemplate', data) expect(result).toEqual({ ok: true }) }) }) // ── getHsmLimit ────────────────────────────────────────────────── describe('getHsmLimit', () => { it('calls client.get with /whatsapp-business-templates/:accountId/hsmLimit', async () => { const limit: HsmLimit = { hsmLimit: 1000, hsmUsedLimit: 150 } vi.mocked(client.get).mockResolvedValue(limit) const result = await api.getHsmLimit('acc-1') expect(client.get).toHaveBeenCalledWith('/whatsapp-business-templates/acc-1/hsmLimit') expect(result).toEqual(limit) }) }) // ── 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 sendById', async () => { vi.mocked(client.post).mockRejectedValue(new Error('Conflict')) await expect(api.sendById('cmp-1')).rejects.toThrow('Conflict') }) it('propagates errors from getHsmLimit', async () => { vi.mocked(client.get).mockRejectedValue(new Error('Forbidden')) await expect(api.getHsmLimit('acc-1')).rejects.toThrow('Forbidden') }) }) })