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', }), }), }) ); }); }); });