template-saas/apps/backend/dist/modules/notifications/__tests__/push-notification.service.spec.js
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

565 lines
26 KiB
JavaScript

"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