Files
Kordant/packages/integration-tests/src/e2e/sms.service.integration.test.ts
Michael Freno 67622a2f11 Add integration tests for notification services (FRE-4522)
Comprehensive integration test suite for notification services:
- EmailService integration tests (Resend provider)
- SMSService integration tests (Twilio provider)
- PushService integration tests (FCM/APNs providers)
- NotificationService integration tests (orchestration layer)

Test coverage includes:
- Successful notification delivery
- Error handling (API errors, network timeouts, invalid inputs)
- Rate limiting enforcement
- Batch operations with partial failures
- Notification preferences and deduplication
- Template-based email sending
- Metadata and attachment handling

Total: ~1400 lines across 4 test files
2026-05-02 13:22:41 -04:00

367 lines
9.5 KiB
TypeScript

import { describe, it, expect, beforeAll, beforeEach, vi } from '@jest/globals';
import { SMSService } from '@shieldai/shared-notifications';
import type { SMSNotification } from '@shieldai/shared-notifications';
import twilio from 'twilio';
// Mock twilio
vi.mock('twilio', () => {
const mockMessages = {
create: vi.fn().mockResolvedValue({
sid: 'SM1234567890abcdef1234567890abcdef',
from: '+14155552671',
to: '+14155552672',
body: 'Test message',
status: 'sent',
}),
};
return vi.fn().mockReturnValue({
messages: mockMessages,
});
});
describe('SMSService Integration Tests', () => {
let smsService: SMSService;
let mockTwilio: any;
beforeAll(() => {
smsService = SMSService.getInstance();
mockTwilio = (require('twilio') as any).mock.results[0].value;
});
beforeEach(() => {
vi.clearAllMocks();
});
describe('send', () => {
it('should successfully send SMS notification', async () => {
mockTwilio.messages.create.mockResolvedValueOnce({
sid: 'SM1234567890abcdef',
from: '+14155552671',
to: '+14155552672',
body: 'Test SMS',
status: 'sent',
});
const notification: SMSNotification = {
channel: 'sms',
to: '+14155552672',
body: 'Test SMS',
};
const result = await smsService.send(notification);
expect(result.status).toBe('sent');
expect(result.channel).toBe('sms');
expect(result.externalId).toBe('SM1234567890abcdef');
expect(result.notificationId).toContain('sms-');
expect(result.deliveredAt).toBeInstanceOf(Date);
});
it('should use default from number when not provided', async () => {
mockTwilio.messages.create.mockResolvedValueOnce({
sid: 'SM-default-from',
from: '+14155552671',
to: '+14155552672',
body: 'Test',
status: 'sent',
});
const notification: SMSNotification = {
channel: 'sms',
to: '+14155552672',
body: 'Test',
};
await smsService.send(notification);
expect(mockTwilio.messages.create).toHaveBeenCalledWith(
expect.objectContaining({
from: expect.any(String),
})
);
});
it('should use custom from number when provided', async () => {
mockTwilio.messages.create.mockResolvedValueOnce({
sid: 'SM-custom-from',
from: '+14155559999',
to: '+14155552672',
body: 'Test',
status: 'sent',
});
const notification: SMSNotification = {
channel: 'sms',
to: '+14155552672',
from: '+14155559999',
body: 'Test',
};
await smsService.send(notification);
expect(mockTwilio.messages.create).toHaveBeenCalledWith(
expect.objectContaining({
from: '+14155559999',
})
);
});
it('should include metadata in SMS', async () => {
mockTwilio.messages.create.mockResolvedValueOnce({
sid: 'SM-metadata-test',
from: '+14155552671',
to: '+14155552672',
body: 'Test',
status: 'sent',
});
const notification: SMSNotification = {
channel: 'sms',
to: '+14155552672',
body: 'Test',
metadata: { userId: 'user-123', campaign: 'promo' },
};
await smsService.send(notification);
expect(mockTwilio.messages.create).toHaveBeenCalledWith(
expect.objectContaining({
metadata: { userId: 'user-123', campaign: 'promo' },
})
);
});
it('should handle Twilio API error', async () => {
mockTwilio.messages.create.mockRejectedValueOnce(
new Error('Invalid phone number')
);
const notification: SMSNotification = {
channel: 'sms',
to: 'invalid-number',
body: 'Test',
};
const result = await smsService.send(notification);
expect(result.status).toBe('failed');
expect(result.error).toBe('Invalid phone number');
});
it('should handle rate limiting', async () => {
process.env.SMS_RATE_LIMIT = '2';
vi.clearAllMocks();
smsService = SMSService.getInstance();
mockTwilio.messages.create
.mockResolvedValueOnce({
sid: 'SM-rate-1',
from: '+14155552671',
to: '+14155552672',
body: 'Test',
status: 'sent',
})
.mockResolvedValueOnce({
sid: 'SM-rate-2',
from: '+14155552671',
to: '+14155552672',
body: 'Test',
status: 'sent',
});
const notification: SMSNotification = {
channel: 'sms',
to: '+14155552672',
body: 'Test',
};
// First two should succeed
const result1 = await smsService.send(notification);
const result2 = await smsService.send(notification);
expect(result1.status).toBe('sent');
expect(result2.status).toBe('sent');
// Third should throw due to rate limit
await expect(smsService.send(notification)).rejects.toThrow(
'SMS rate limit exceeded'
);
});
it('should handle network timeout', async () => {
mockTwilio.messages.create.mockRejectedValueOnce(
new Error('Network timeout')
);
const notification: SMSNotification = {
channel: 'sms',
to: '+14155552672',
body: 'Test',
};
const result = await smsService.send(notification);
expect(result.status).toBe('failed');
expect(result.error).toBe('Network timeout');
});
it('should handle invalid phone number format', async () => {
mockTwilio.messages.create.mockRejectedValueOnce(
new Error('Number not in E.164 format')
);
const notification: SMSNotification = {
channel: 'sms',
to: '12345',
body: 'Test',
};
const result = await smsService.send(notification);
expect(result.status).toBe('failed');
expect(result.error).toContain('Number not in E.164 format');
});
});
describe('sendBatch', () => {
beforeEach(() => {
vi.clearAllMocks();
smsService = SMSService.getInstance();
mockTwilio.messages.create
.mockResolvedValueOnce({
sid: 'batch-sms-1',
from: '+14155552671',
to: '+14155552672',
body: 'Test 1',
status: 'sent',
})
.mockResolvedValueOnce({
sid: 'batch-sms-2',
from: '+14155552671',
to: '+14155552673',
body: 'Test 2',
status: 'sent',
})
.mockResolvedValueOnce({
sid: 'batch-sms-3',
from: '+14155552671',
to: '+14155552674',
body: 'Test 3',
status: 'sent',
});
});
it('should send multiple SMS successfully', async () => {
const notifications: SMSNotification[] = [
{
channel: 'sms',
to: '+14155552672',
body: 'Batch 1',
},
{
channel: 'sms',
to: '+14155552673',
body: 'Batch 2',
},
{
channel: 'sms',
to: '+14155552674',
body: 'Batch 3',
},
];
const results = await smsService.sendBatch(notifications);
expect(results).toHaveLength(3);
expect(results.every(r => r.status === 'sent')).toBe(true);
expect(results.map(r => r.externalId)).toEqual(['batch-sms-1', 'batch-sms-2', 'batch-sms-3']);
});
it('should handle partial failures in batch', async () => {
mockTwilio.messages.create
.mockResolvedValueOnce({
sid: 'partial-sms-1',
from: '+14155552671',
to: '+14155552672',
body: 'Valid',
status: 'sent',
})
.mockRejectedValueOnce(new Error('Invalid number'));
const notifications: SMSNotification[] = [
{
channel: 'sms',
to: '+14155552672',
body: 'Valid',
},
{
channel: 'sms',
to: 'invalid',
body: 'Invalid',
},
];
const results = await smsService.sendBatch(notifications);
expect(results).toHaveLength(2);
expect(results[0].status).toBe('sent');
expect(results[1].status).toBe('failed');
});
});
describe('getRateLimitStatus', () => {
beforeEach(() => {
vi.clearAllMocks();
smsService = SMSService.getInstance();
});
it('should return rate limit status', () => {
const status = smsService.getRateLimitStatus();
expect(status).toHaveProperty('remaining');
expect(status).toHaveProperty('limit');
expect(status.limit).toBeGreaterThan(0);
expect(status.remaining).toBeLessThanOrEqual(status.limit);
});
});
describe('Twilio configuration', () => {
beforeEach(() => {
vi.clearAllMocks();
smsService = SMSService.getInstance();
});
it('should use configured account SID and auth token', () => {
expect(require('twilio')).toHaveBeenCalledWith(
expect.any(String),
expect.any(String)
);
});
it('should use configured messaging service SID', async () => {
mockTwilio.messages.create.mockResolvedValueOnce({
sid: 'SM-config-test',
from: '+14155552671',
to: '+14155552672',
body: 'Test',
status: 'sent',
});
const notification: SMSNotification = {
channel: 'sms',
to: '+14155552672',
body: 'Test',
};
await smsService.send(notification);
expect(mockTwilio.messages.create).toHaveBeenCalledWith(
expect.objectContaining({
from: expect.any(String),
})
);
});
});
});