Files
Kordant/packages/integration-tests/src/e2e/push.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

335 lines
9.6 KiB
TypeScript

import { describe, it, expect, beforeAll, beforeEach, vi } from '@jest/globals';
import { PushService } from '@shieldai/shared-notifications';
import type { PushNotification } from '@shieldai/shared-notifications';
import * as admin from 'firebase-admin';
// Mock firebase-admin
vi.mock('firebase-admin', () => {
const mockMessaging = {
send: vi.fn().mockResolvedValue('push-token-123'),
};
const mockCredential = {
cert: vi.fn().mockReturnValue({
projectId: 'test-project',
clientEmail: 'test@test-project.iam.gserviceaccount.com',
privateKey: 'test-key',
}),
};
const mockApp = {
options: {},
};
return {
default: {
initializeApp: vi.fn().mockReturnValue(mockApp),
credential: {
cert: mockCredential,
},
messaging: vi.fn().mockReturnValue(mockMessaging),
},
messaging: vi.fn().mockReturnValue(mockMessaging),
app: {
App: Object,
},
credential: {
cert: mockCredential,
},
};
});
describe('PushService Integration Tests', () => {
let pushService: PushService;
let mockMessaging: any;
beforeAll(() => {
pushService = PushService.getInstance();
mockMessaging = (require('firebase-admin').messaging as any).mock.instances[0];
});
beforeEach(() => {
vi.clearAllMocks();
});
describe('send', () => {
it('should successfully send push notification', async () => {
const mockResponse = 'fcm-message-id-123';
(require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse);
const notification: PushNotification = {
channel: 'push',
userId: 'user-device-token-123',
title: 'Test Title',
body: 'Test Body',
};
const result = await pushService.send(notification);
expect(result.status).toBe('sent');
expect(result.channel).toBe('push');
expect(result.externalId).toBe(mockResponse);
expect(result.notificationId).toContain('push-');
expect(result.deliveredAt).toBeInstanceOf(Date);
});
it('should include notification data', async () => {
const mockResponse = 'data-push-456';
(require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse);
const notification: PushNotification = {
channel: 'push',
userId: 'user-device-token-456',
title: 'Test',
body: 'Test',
data: { key1: 'value1', key2: 'value2' },
};
await pushService.send(notification);
expect((require('firebase-admin').messaging as any).mock.instances[0].send).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
key1: 'value1',
key2: 'value2',
}),
})
);
});
it('should include badge and sound settings', async () => {
const mockResponse = 'apns-push-789';
(require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse);
const notification: PushNotification = {
channel: 'push',
userId: 'user-device-token-789',
title: 'Test',
body: 'Test',
badge: 5,
sound: 'custom-sound.caf',
category: 'ALERT_CATEGORY',
};
await pushService.send(notification);
expect((require('firebase-admin').messaging as any).mock.instances[0].send).toHaveBeenCalledWith(
expect.objectContaining({
apns: expect.objectContaining({
payload: expect.objectContaining({
aps: expect.objectContaining({
badge: 5,
sound: 'custom-sound.caf',
category: 'ALERT_CATEGORY',
}),
}),
}),
})
);
});
it('should handle undefined data gracefully', async () => {
const mockResponse = 'no-data-push-101';
(require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse);
const notification: PushNotification = {
channel: 'push',
userId: 'user-device-token-101',
title: 'Test',
body: 'Test',
};
await pushService.send(notification);
expect((require('firebase-admin').messaging as any).mock.instances[0].send).toHaveBeenCalledWith(
expect.objectContaining({
data: undefined,
})
);
});
it('should handle FCM API error', async () => {
(require('firebase-admin').messaging as any).mock.instances[0].send.mockRejectedValueOnce(
new Error('Invalid registration token')
);
const notification: PushNotification = {
channel: 'push',
userId: 'invalid-token',
title: 'Test',
body: 'Test',
};
const result = await pushService.send(notification);
expect(result.status).toBe('failed');
expect(result.error).toBe('Invalid registration token');
});
it('should enforce push rate limiting', async () => {
process.env.PUSH_RATE_LIMIT = '2';
vi.clearAllMocks();
pushService = PushService.getInstance();
const notification: PushNotification = {
channel: 'push',
userId: 'rate-test-user',
title: 'Test',
body: 'Test',
};
// First two should succeed
const result1 = await pushService.send(notification);
const result2 = await pushService.send(notification);
expect(result1.status).toBe('sent');
expect(result2.status).toBe('sent');
// Third should throw due to rate limit
await expect(pushService.send(notification)).rejects.toThrow(
'Push rate limit exceeded'
);
});
it('should handle network timeout', async () => {
(require('firebase-admin').messaging as any).mock.instances[0].send.mockRejectedValueOnce(
new Error('Network timeout')
);
const notification: PushNotification = {
channel: 'push',
userId: 'timeout-user',
title: 'Test',
body: 'Test',
};
const result = await pushService.send(notification);
expect(result.status).toBe('failed');
expect(result.error).toBe('Network timeout');
});
});
describe('sendBatch', () => {
beforeEach(() => {
vi.clearAllMocks();
pushService = PushService.getInstance();
});
it('should send multiple push notifications successfully', async () => {
(require('firebase-admin').messaging as any).mock.instances[0].send
.mockResolvedValueOnce('batch-push-1')
.mockResolvedValueOnce('batch-push-2')
.mockResolvedValueOnce('batch-push-3');
const notifications: PushNotification[] = [
{
channel: 'push',
userId: 'user-token-1',
title: 'Batch 1',
body: 'Test 1',
},
{
channel: 'push',
userId: 'user-token-2',
title: 'Batch 2',
body: 'Test 2',
},
{
channel: 'push',
userId: 'user-token-3',
title: 'Batch 3',
body: 'Test 3',
},
];
const results = await pushService.sendBatch(notifications);
expect(results).toHaveLength(3);
expect(results.every(r => r.status === 'sent')).toBe(true);
expect(results.map(r => r.externalId)).toEqual(['batch-push-1', 'batch-push-2', 'batch-push-3']);
});
it('should handle partial failures in batch', async () => {
(require('firebase-admin').messaging as any).mock.instances[0].send
.mockResolvedValueOnce('partial-push-1')
.mockRejectedValueOnce(new Error('Invalid token'));
const notifications: PushNotification[] = [
{
channel: 'push',
userId: 'valid-token',
title: 'Valid',
body: 'Valid',
},
{
channel: 'push',
userId: 'invalid-token',
title: 'Invalid',
body: 'Invalid',
},
];
const results = await pushService.sendBatch(notifications);
expect(results).toHaveLength(2);
expect(results[0].status).toBe('sent');
expect(results[1].status).toBe('failed');
});
});
describe('getRateLimitStatus', () => {
beforeEach(() => {
vi.clearAllMocks();
pushService = PushService.getInstance();
});
it('should return rate limit status', () => {
const status = pushService.getRateLimitStatus();
expect(status).toHaveProperty('remaining');
expect(status).toHaveProperty('limit');
expect(status.limit).toBeGreaterThan(0);
expect(status.remaining).toBeLessThanOrEqual(status.limit);
});
});
describe('APNs configuration', () => {
beforeEach(() => {
vi.clearAllMocks();
pushService = PushService.getInstance();
});
it('should configure APNs payload correctly', async () => {
const mockResponse = 'apns-config-test';
(require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse);
const notification: PushNotification = {
channel: 'push',
userId: 'ios-device-token',
title: 'iOS Test',
body: 'iOS Body',
badge: 10,
sound: 'notification.caf',
category: 'MESSAGE_CATEGORY',
};
await pushService.send(notification);
const callArg = (require('firebase-admin').messaging as any).mock.instances[0].send.mock.calls[0][0];
expect(call_arg.apns).toEqual(
expect.objectContaining({
payload: expect.objectContaining({
aps: expect.objectContaining({
badge: 10,
sound: 'notification.caf',
category: 'MESSAGE_CATEGORY',
}),
}),
})
);
});
});
});