"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const typeorm_1 = require("@nestjs/typeorm"); const config_1 = require("@nestjs/config"); const push_notification_service_1 = require("../services/push-notification.service"); const entities_1 = require("../entities"); const webpush = __importStar(require("web-push")); describe('PushNotificationService', () => { let service; let deviceRepository; let logRepository; let configService; const mockDevice = { id: 'device-1', tenant_id: 'tenant-1', user_id: 'user-1', device_type: 'web', device_token: JSON.stringify({ endpoint: 'https://fcm.googleapis.com/fcm/send/test', keys: { p256dh: 'test-key', auth: 'test-auth' }, }), device_name: 'Chrome on Windows', browser: 'Chrome', browser_version: '120', os: 'Windows', os_version: '11', is_active: true, last_used_at: new Date(), created_at: new Date(), }; const mockDeviceRepository = { find: jest.fn(), save: jest.fn(), }; const mockLogRepository = { save: jest.fn(), }; const createMockConfigService = () => ({ get: jest.fn((key, defaultValue) => { const config = { VAPID_PUBLIC_KEY: 'BN4GvZtEZiZuqFJiLNpT1234567890', VAPID_PRIVATE_KEY: 'aB3cDefGh4IjKlM5nOpQr6StUvWxYz', VAPID_SUBJECT: 'mailto:admin@example.com', }; return config[key] || defaultValue; }), }); let mockConfigService; beforeEach(async () => { jest.clearAllMocks(); mockConfigService = createMockConfigService(); const module = await testing_1.Test.createTestingModule({ providers: [ push_notification_service_1.PushNotificationService, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.UserDevice), useValue: mockDeviceRepository, }, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog), useValue: mockLogRepository, }, { provide: config_1.ConfigService, useValue: mockConfigService, }, ], }).compile(); service = module.get(push_notification_service_1.PushNotificationService); deviceRepository = module.get((0, typeorm_1.getRepositoryToken)(entities_1.UserDevice)); logRepository = module.get((0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog)); configService = module.get(config_1.ConfigService); service.onModuleInit(); }); describe('onModuleInit', () => { it('should configure VAPID if keys are provided', () => { expect(webpush.setVapidDetails).toHaveBeenCalledWith('mailto:admin@example.com', 'BN4GvZtEZiZuqFJiLNpT1234567890', 'aB3cDefGh4IjKlM5nOpQr6StUvWxYz'); expect(service.isEnabled()).toBe(true); }); it('should not configure if keys are missing', async () => { jest.clearAllMocks(); mockConfigService.get.mockReturnValue(undefined); const module = await testing_1.Test.createTestingModule({ providers: [ push_notification_service_1.PushNotificationService, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.UserDevice), useValue: mockDeviceRepository, }, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog), useValue: mockLogRepository, }, { provide: config_1.ConfigService, useValue: { get: () => undefined }, }, ], }).compile(); const unconfiguredService = module.get(push_notification_service_1.PushNotificationService); unconfiguredService.onModuleInit(); expect(unconfiguredService.isEnabled()).toBe(false); }); }); describe('getVapidPublicKey', () => { it('should return VAPID public key when configured', () => { const key = service.getVapidPublicKey(); expect(key).toBe('BN4GvZtEZiZuqFJiLNpT1234567890'); }); }); describe('sendToUser', () => { const payload = { title: 'Test Notification', body: 'This is a test', url: '/test', }; it('should send to all active devices', async () => { mockDeviceRepository.find.mockResolvedValue([mockDevice]); webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); const results = await service.sendToUser('user-1', 'tenant-1', payload); expect(results).toHaveLength(1); expect(results[0].success).toBe(true); expect(webpush.sendNotification).toHaveBeenCalled(); }); it('should return empty array if no devices', async () => { mockDeviceRepository.find.mockResolvedValue([]); const results = await service.sendToUser('user-1', 'tenant-1', payload); expect(results).toHaveLength(0); }); it('should handle expired subscription (410)', async () => { mockDeviceRepository.find.mockResolvedValue([mockDevice]); webpush.sendNotification.mockRejectedValue({ statusCode: 410, message: 'Subscription expired', }); mockDeviceRepository.save.mockResolvedValue({ ...mockDevice, is_active: false, }); const results = await service.sendToUser('user-1', 'tenant-1', payload); expect(results).toHaveLength(1); expect(results[0].success).toBe(false); expect(results[0].statusCode).toBe(410); expect(mockDeviceRepository.save).toHaveBeenCalledWith(expect.objectContaining({ is_active: false })); }); it('should create log on success with notificationId', async () => { mockDeviceRepository.find.mockResolvedValue([mockDevice]); webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); mockLogRepository.save.mockResolvedValue({}); await service.sendToUser('user-1', 'tenant-1', payload, 'notification-1'); expect(mockLogRepository.save).toHaveBeenCalledWith(expect.objectContaining({ notification_id: 'notification-1', channel: 'push', status: 'sent', provider: 'web-push', })); }); }); describe('validateSubscription', () => { it('should return true for valid subscription', () => { const validSubscription = JSON.stringify({ endpoint: 'https://fcm.googleapis.com/fcm/send/test', keys: { p256dh: 'test-key', auth: 'test-auth' }, }); expect(service.validateSubscription(validSubscription)).toBe(true); }); it('should return false for missing endpoint', () => { const invalidSubscription = JSON.stringify({ keys: { p256dh: 'test-key', auth: 'test-auth' }, }); expect(service.validateSubscription(invalidSubscription)).toBe(false); }); it('should return false for missing keys', () => { const invalidSubscription = JSON.stringify({ endpoint: 'https://fcm.googleapis.com/fcm/send/test', }); expect(service.validateSubscription(invalidSubscription)).toBe(false); }); it('should return false for invalid JSON', () => { expect(service.validateSubscription('invalid-json')).toBe(false); }); }); describe('sendBroadcast', () => { const payload = { title: 'Broadcast Test', body: 'This is a broadcast', }; it('should send to all tenant devices', async () => { mockDeviceRepository.find.mockResolvedValue([mockDevice, mockDevice]); webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); const result = await service.sendBroadcast('tenant-1', payload); expect(result.total).toBe(2); expect(result.successful).toBe(2); expect(result.failed).toBe(0); }); it('should count failures correctly', async () => { mockDeviceRepository.find.mockResolvedValue([mockDevice, mockDevice]); webpush.sendNotification .mockResolvedValueOnce({}) .mockRejectedValueOnce(new Error('Failed')); mockDeviceRepository.save.mockResolvedValue(mockDevice); const result = await service.sendBroadcast('tenant-1', payload); expect(result.total).toBe(2); expect(result.successful).toBe(1); expect(result.failed).toBe(1); }); it('should return zeros when not configured', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ push_notification_service_1.PushNotificationService, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.UserDevice), useValue: mockDeviceRepository, }, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog), useValue: mockLogRepository, }, { provide: config_1.ConfigService, useValue: { get: () => undefined }, }, ], }).compile(); const unconfiguredService = module.get(push_notification_service_1.PushNotificationService); unconfiguredService.onModuleInit(); const result = await unconfiguredService.sendBroadcast('tenant-1', payload); expect(result).toEqual({ total: 0, successful: 0, failed: 0 }); expect(mockDeviceRepository.find).not.toHaveBeenCalled(); }); it('should return zeros when no devices in tenant', async () => { mockDeviceRepository.find.mockResolvedValue([]); const result = await service.sendBroadcast('tenant-1', payload); expect(result.total).toBe(0); expect(result.successful).toBe(0); expect(result.failed).toBe(0); }); }); describe('sendToDevice', () => { const testPayload = JSON.stringify({ title: 'Direct Device Test', body: 'Testing direct send', }); it('should send push to valid device', async () => { webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); const result = await service.sendToDevice(mockDevice, testPayload); expect(result.success).toBe(true); expect(result.deviceId).toBe('device-1'); expect(webpush.sendNotification).toHaveBeenCalledWith(JSON.parse(mockDevice.device_token), testPayload); }); it('should update last_used_at on success', async () => { webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); await service.sendToDevice(mockDevice, testPayload); expect(mockDeviceRepository.save).toHaveBeenCalledWith(expect.objectContaining({ last_used_at: expect.any(Date), })); }); it('should create log on success with notificationId', async () => { webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); mockLogRepository.save.mockResolvedValue({}); await service.sendToDevice(mockDevice, testPayload, 'notif-123'); expect(mockLogRepository.save).toHaveBeenCalledWith(expect.objectContaining({ notification_id: 'notif-123', channel: 'push', status: 'sent', provider: 'web-push', device_id: 'device-1', delivered_at: expect.any(Date), })); }); it('should not create log when notificationId is not provided', async () => { webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); await service.sendToDevice(mockDevice, testPayload); expect(mockLogRepository.save).not.toHaveBeenCalled(); }); it('should handle invalid device (404)', async () => { webpush.sendNotification.mockRejectedValue({ statusCode: 404, message: 'Subscription not found', }); mockDeviceRepository.save.mockResolvedValue({ ...mockDevice, is_active: false, }); const result = await service.sendToDevice(mockDevice, testPayload); expect(result.success).toBe(false); expect(result.statusCode).toBe(404); expect(mockDeviceRepository.save).toHaveBeenCalledWith(expect.objectContaining({ is_active: false })); }); it('should handle generic send error', async () => { webpush.sendNotification.mockRejectedValue({ statusCode: 500, message: 'Internal server error', }); const result = await service.sendToDevice(mockDevice, testPayload); expect(result.success).toBe(false); expect(result.statusCode).toBe(500); expect(result.error).toBe('Internal server error'); }); it('should create failure log with notificationId', async () => { webpush.sendNotification.mockRejectedValue({ statusCode: 500, message: 'Server error', }); mockLogRepository.save.mockResolvedValue({}); await service.sendToDevice(mockDevice, testPayload, 'notif-456'); expect(mockLogRepository.save).toHaveBeenCalledWith(expect.objectContaining({ notification_id: 'notif-456', channel: 'push', status: 'failed', provider: 'web-push', device_id: 'device-1', error_code: '500', error_message: 'Server error', })); }); it('should not create failure log without notificationId', async () => { webpush.sendNotification.mockRejectedValue({ statusCode: 500, message: 'Server error', }); await service.sendToDevice(mockDevice, testPayload); expect(mockLogRepository.save).not.toHaveBeenCalled(); }); }); describe('sendToUser - additional cases', () => { const payload = { title: 'Test Notification', body: 'This is a test', }; it('should return empty array when service not configured', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ push_notification_service_1.PushNotificationService, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.UserDevice), useValue: mockDeviceRepository, }, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog), useValue: mockLogRepository, }, { provide: config_1.ConfigService, useValue: { get: () => undefined }, }, ], }).compile(); const unconfiguredService = module.get(push_notification_service_1.PushNotificationService); unconfiguredService.onModuleInit(); const results = await unconfiguredService.sendToUser('user-1', 'tenant-1', payload); expect(results).toHaveLength(0); expect(mockDeviceRepository.find).not.toHaveBeenCalled(); }); it('should send to multiple devices and aggregate results', async () => { const device2 = { ...mockDevice, id: 'device-2', device_name: 'Firefox on Linux', }; mockDeviceRepository.find.mockResolvedValue([mockDevice, device2]); webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); const results = await service.sendToUser('user-1', 'tenant-1', payload); expect(results).toHaveLength(2); expect(results.every((r) => r.success)).toBe(true); expect(webpush.sendNotification).toHaveBeenCalledTimes(2); }); it('should continue sending to other devices after one fails', async () => { const device2 = { ...mockDevice, id: 'device-2', }; mockDeviceRepository.find.mockResolvedValue([mockDevice, device2]); webpush.sendNotification .mockRejectedValueOnce({ statusCode: 500, message: 'Error' }) .mockResolvedValueOnce({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); const results = await service.sendToUser('user-1', 'tenant-1', payload); expect(results).toHaveLength(2); expect(results[0].success).toBe(false); expect(results[1].success).toBe(true); }); it('should include custom payload data and actions', async () => { const customPayload = { title: 'Test', body: 'Test body', icon: '/custom-icon.png', badge: '/custom-badge.png', url: '/custom-url', data: { orderId: '123' }, actions: [{ action: 'open', title: 'Open' }], }; mockDeviceRepository.find.mockResolvedValue([mockDevice]); webpush.sendNotification.mockResolvedValue({}); mockDeviceRepository.save.mockResolvedValue(mockDevice); await service.sendToUser('user-1', 'tenant-1', customPayload); const sentPayload = JSON.parse(webpush.sendNotification.mock.calls[0][1]); expect(sentPayload.icon).toBe('/custom-icon.png'); expect(sentPayload.badge).toBe('/custom-badge.png'); expect(sentPayload.url).toBe('/custom-url'); expect(sentPayload.data).toEqual({ orderId: '123' }); expect(sentPayload.actions).toEqual([{ action: 'open', title: 'Open' }]); }); }); describe('getVapidPublicKey - additional cases', () => { it('should return null when not configured', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ push_notification_service_1.PushNotificationService, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.UserDevice), useValue: mockDeviceRepository, }, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog), useValue: mockLogRepository, }, { provide: config_1.ConfigService, useValue: { get: () => undefined }, }, ], }).compile(); const unconfiguredService = module.get(push_notification_service_1.PushNotificationService); unconfiguredService.onModuleInit(); expect(unconfiguredService.getVapidPublicKey()).toBeNull(); }); }); describe('validateSubscription - additional cases', () => { it('should return false for missing p256dh key', () => { const invalidSubscription = JSON.stringify({ endpoint: 'https://fcm.googleapis.com/fcm/send/test', keys: { auth: 'test-auth' }, }); expect(service.validateSubscription(invalidSubscription)).toBe(false); }); it('should return false for missing auth key', () => { const invalidSubscription = JSON.stringify({ endpoint: 'https://fcm.googleapis.com/fcm/send/test', keys: { p256dh: 'test-key' }, }); expect(service.validateSubscription(invalidSubscription)).toBe(false); }); it('should return false for empty keys object', () => { const invalidSubscription = JSON.stringify({ endpoint: 'https://fcm.googleapis.com/fcm/send/test', keys: {}, }); expect(service.validateSubscription(invalidSubscription)).toBe(false); }); it('should return false for null keys', () => { const invalidSubscription = JSON.stringify({ endpoint: 'https://fcm.googleapis.com/fcm/send/test', keys: null, }); expect(service.validateSubscription(invalidSubscription)).toBe(false); }); it('should return false for empty string', () => { expect(service.validateSubscription('')).toBe(false); }); }); describe('onModuleInit - additional cases', () => { it('should handle setVapidDetails error gracefully', async () => { jest.clearAllMocks(); webpush.setVapidDetails.mockImplementation(() => { throw new Error('Invalid VAPID keys'); }); const module = await testing_1.Test.createTestingModule({ providers: [ push_notification_service_1.PushNotificationService, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.UserDevice), useValue: mockDeviceRepository, }, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog), useValue: mockLogRepository, }, { provide: config_1.ConfigService, useValue: mockConfigService, }, ], }).compile(); const errorService = module.get(push_notification_service_1.PushNotificationService); expect(() => errorService.onModuleInit()).not.toThrow(); expect(errorService.isEnabled()).toBe(false); }); it('should use default VAPID subject when not provided', async () => { jest.clearAllMocks(); const configWithoutSubject = { get: jest.fn((key, defaultValue) => { const config = { VAPID_PUBLIC_KEY: 'BN4GvZtEZiZuqFJiLNpT1234567890', VAPID_PRIVATE_KEY: 'aB3cDefGh4IjKlM5nOpQr6StUvWxYz', }; return config[key] || defaultValue; }), }; const module = await testing_1.Test.createTestingModule({ providers: [ push_notification_service_1.PushNotificationService, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.UserDevice), useValue: mockDeviceRepository, }, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog), useValue: mockLogRepository, }, { provide: config_1.ConfigService, useValue: configWithoutSubject, }, ], }).compile(); const serviceWithDefault = module.get(push_notification_service_1.PushNotificationService); serviceWithDefault.onModuleInit(); expect(webpush.setVapidDetails).toHaveBeenCalledWith('mailto:admin@example.com', 'BN4GvZtEZiZuqFJiLNpT1234567890', 'aB3cDefGh4IjKlM5nOpQr6StUvWxYz'); }); }); }); //# sourceMappingURL=push-notification.service.spec.js.map