import { describe, it, expect, vi } from 'vitest'; import type { CapabilityRequest } from '@insignia/iios-contracts'; import { TwilioSmsProvider, type TwilioCreds } from './twilio-sms.provider'; const CREDS: TwilioCreds = { accountSid: 'AC123', authToken: 'tok_secret', fromNumber: '+15550001111' }; const req = (over: Partial = {}): CapabilityRequest => ({ capability: 'channel.send', channelType: 'SMS', scopeId: 'scope_1', target: '+15557654321', payload: { text: 'hello world' }, idempotencyKey: 'idem-1', ...over, }); describe('TwilioSmsProvider', () => { it('resolves the scope creds and POSTs to Twilio, returning SENT with the message SID', async () => { const http = vi.fn().mockResolvedValue({ ok: true, status: 201, json: async () => ({ sid: 'SM999' }), text: async () => '' }); const resolve = vi.fn().mockResolvedValue(CREDS); const p = new TwilioSmsProvider(resolve, http); const res = await p.send(req()); expect(resolve).toHaveBeenCalledWith('scope_1'); const [url, init] = http.mock.calls[0]; expect(url).toBe('https://api.twilio.com/2010-04-01/Accounts/AC123/Messages.json'); expect(init.method).toBe('POST'); expect(init.headers.authorization).toBe(`Basic ${Buffer.from('AC123:tok_secret').toString('base64')}`); const body = new URLSearchParams(init.body as string); expect(body.get('To')).toBe('+15557654321'); expect(body.get('From')).toBe('+15550001111'); expect(body.get('Body')).toBe('hello world'); expect(res).toMatchObject({ outcome: 'SENT', providerRef: 'SM999' }); }); it('fails closed as NOT_CONFIGURED when the scope has no creds', async () => { const http = vi.fn(); const p = new TwilioSmsProvider(async () => null, http); const res = await p.send(req()); expect(res.outcome).toBe('FAILED'); expect(res.errorCode).toBe('NOT_CONFIGURED'); expect(http).not.toHaveBeenCalled(); }); it('fails closed as NOT_CONFIGURED when the request has no scope', async () => { const p = new TwilioSmsProvider(async () => CREDS, vi.fn()); const res = await p.send(req({ scopeId: undefined })); expect(res.outcome).toBe('FAILED'); expect(res.errorCode).toBe('NOT_CONFIGURED'); }); it('surfaces a Twilio API error as FAILED (never throws)', async () => { const http = vi.fn().mockResolvedValue({ ok: false, status: 401, json: async () => ({ code: 20003, message: 'Authenticate' }), text: async () => 'Authenticate' }); const p = new TwilioSmsProvider(async () => CREDS, http); const res = await p.send(req()); expect(res.outcome).toBe('FAILED'); expect(res.errorCode).toBe('TWILIO_20003'); }); it('surfaces a transport throw as FAILED', async () => { const http = vi.fn().mockRejectedValue(new Error('network down')); const p = new TwilioSmsProvider(async () => CREDS, http); const res = await p.send(req()); expect(res.outcome).toBe('FAILED'); }); });