template-saas/apps/backend/dist/modules/notifications/__tests__/notification-queue.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

446 lines
21 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const testing_1 = require("@nestjs/testing");
const typeorm_1 = require("@nestjs/typeorm");
const notification_queue_service_1 = require("../services/notification-queue.service");
const entities_1 = require("../entities");
describe('NotificationQueueService', () => {
let service;
let queueRepository;
let logRepository;
let notificationRepository;
const mockNotificationId = '550e8400-e29b-41d4-a716-446655440000';
const mockQueueId = '550e8400-e29b-41d4-a716-446655440001';
const mockTenantId = '550e8400-e29b-41d4-a716-446655440002';
const mockUserId = '550e8400-e29b-41d4-a716-446655440003';
const mockNotification = {
id: mockNotificationId,
tenant_id: mockTenantId,
user_id: mockUserId,
type: 'info',
channel: 'email',
title: 'Test Notification',
message: 'This is a test notification',
is_read: false,
delivery_status: 'pending',
created_at: new Date(),
};
const mockQueueItem = {
id: mockQueueId,
notification_id: mockNotificationId,
channel: 'email',
scheduled_for: new Date(),
priority_value: 0,
attempts: 0,
max_attempts: 3,
status: 'queued',
error_count: 0,
metadata: {},
created_at: new Date(),
};
const createMockQueryBuilder = (overrides = {}) => ({
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
addOrderBy: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([]),
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
addGroupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
delete: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 0 }),
...overrides,
});
const mockQueueRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
createQueryBuilder: jest.fn(),
};
const mockLogRepo = {
save: jest.fn(),
};
const mockNotificationRepo = {
update: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
mockQueueRepo.createQueryBuilder.mockReturnValue(createMockQueryBuilder());
const module = await testing_1.Test.createTestingModule({
providers: [
notification_queue_service_1.NotificationQueueService,
{
provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationQueue),
useValue: mockQueueRepo,
},
{
provide: (0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog),
useValue: mockLogRepo,
},
{
provide: (0, typeorm_1.getRepositoryToken)(entities_1.Notification),
useValue: mockNotificationRepo,
},
],
}).compile();
service = module.get(notification_queue_service_1.NotificationQueueService);
queueRepository = module.get((0, typeorm_1.getRepositoryToken)(entities_1.NotificationQueue));
logRepository = module.get((0, typeorm_1.getRepositoryToken)(entities_1.NotificationLog));
notificationRepository = module.get((0, typeorm_1.getRepositoryToken)(entities_1.Notification));
});
afterEach(() => {
jest.clearAllMocks();
});
describe('enqueue', () => {
it('should enqueue notification with default priority', async () => {
mockQueueRepo.create.mockReturnValue(mockQueueItem);
mockQueueRepo.save.mockResolvedValue(mockQueueItem);
const result = await service.enqueue(mockNotificationId, 'email');
expect(mockQueueRepo.create).toHaveBeenCalledWith(expect.objectContaining({
notification_id: mockNotificationId,
channel: 'email',
priority_value: 0,
status: 'queued',
attempts: 0,
max_attempts: 3,
}));
expect(mockQueueRepo.save).toHaveBeenCalled();
expect(result).toEqual(mockQueueItem);
});
it('should enqueue notification with urgent priority', async () => {
const urgentQueueItem = { ...mockQueueItem, priority_value: 10 };
mockQueueRepo.create.mockReturnValue(urgentQueueItem);
mockQueueRepo.save.mockResolvedValue(urgentQueueItem);
const result = await service.enqueue(mockNotificationId, 'email', 'urgent');
expect(mockQueueRepo.create).toHaveBeenCalledWith(expect.objectContaining({
priority_value: 10,
}));
expect(result.priority_value).toBe(10);
});
it('should enqueue notification with high priority', async () => {
const highPriorityItem = { ...mockQueueItem, priority_value: 5 };
mockQueueRepo.create.mockReturnValue(highPriorityItem);
mockQueueRepo.save.mockResolvedValue(highPriorityItem);
const result = await service.enqueue(mockNotificationId, 'push', 'high');
expect(mockQueueRepo.create).toHaveBeenCalledWith(expect.objectContaining({
priority_value: 5,
}));
expect(result.priority_value).toBe(5);
});
it('should enqueue notification with low priority', async () => {
const lowPriorityItem = { ...mockQueueItem, priority_value: -5 };
mockQueueRepo.create.mockReturnValue(lowPriorityItem);
mockQueueRepo.save.mockResolvedValue(lowPriorityItem);
const result = await service.enqueue(mockNotificationId, 'sms', 'low');
expect(mockQueueRepo.create).toHaveBeenCalledWith(expect.objectContaining({
priority_value: -5,
}));
expect(result.priority_value).toBe(-5);
});
it('should enqueue notification with scheduled time', async () => {
const futureDate = new Date(Date.now() + 3600000);
const scheduledItem = { ...mockQueueItem, scheduled_for: futureDate };
mockQueueRepo.create.mockReturnValue(scheduledItem);
mockQueueRepo.save.mockResolvedValue(scheduledItem);
const result = await service.enqueue(mockNotificationId, 'email', 'normal', futureDate);
expect(mockQueueRepo.create).toHaveBeenCalledWith(expect.objectContaining({
scheduled_for: futureDate,
}));
expect(result.scheduled_for).toEqual(futureDate);
});
});
describe('enqueueBatch', () => {
it('should enqueue notification for multiple channels', async () => {
const channels = ['email', 'push', 'in_app'];
const queueItems = channels.map((channel) => ({
...mockQueueItem,
channel,
}));
mockQueueRepo.create.mockImplementation((data) => data);
mockQueueRepo.save.mockResolvedValue(queueItems);
const result = await service.enqueueBatch(mockNotificationId, channels);
expect(mockQueueRepo.create).toHaveBeenCalledTimes(3);
expect(mockQueueRepo.save).toHaveBeenCalled();
expect(result).toHaveLength(3);
});
it('should enqueue batch with specified priority', async () => {
const channels = ['email', 'sms'];
mockQueueRepo.create.mockImplementation((data) => data);
mockQueueRepo.save.mockResolvedValue([]);
await service.enqueueBatch(mockNotificationId, channels, 'urgent');
expect(mockQueueRepo.create).toHaveBeenCalledWith(expect.objectContaining({
priority_value: 10,
}));
});
});
describe('getPendingItems', () => {
it('should return pending queue items', async () => {
const pendingItems = [
{ ...mockQueueItem, notification: mockNotification },
];
const mockQueryBuilder = createMockQueryBuilder({
getMany: jest.fn().mockResolvedValue(pendingItems),
});
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
const result = await service.getPendingItems();
expect(mockQueryBuilder.where).toHaveBeenCalledWith('q.status IN (:...statuses)', { statuses: ['queued', 'retrying'] });
expect(result).toEqual(pendingItems);
});
it('should filter by channel when specified', async () => {
const mockQueryBuilder = createMockQueryBuilder();
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
await service.getPendingItems(100, 'email');
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('q.channel = :channel', { channel: 'email' });
});
it('should limit results', async () => {
const mockQueryBuilder = createMockQueryBuilder();
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
await service.getPendingItems(50);
expect(mockQueryBuilder.take).toHaveBeenCalledWith(50);
});
});
describe('markAsProcessing', () => {
it('should update queue item status to processing', async () => {
mockQueueRepo.update.mockResolvedValue({ affected: 1 });
await service.markAsProcessing(mockQueueId);
expect(mockQueueRepo.update).toHaveBeenCalledWith(mockQueueId, {
status: 'processing',
last_attempt_at: expect.any(Date),
});
});
});
describe('markAsSent', () => {
it('should mark queue item as sent and update notification', async () => {
mockQueueRepo.findOne.mockResolvedValue(mockQueueItem);
mockQueueRepo.update.mockResolvedValue({ affected: 1 });
mockNotificationRepo.update.mockResolvedValue({ affected: 1 });
mockLogRepo.save.mockResolvedValue({});
await service.markAsSent(mockQueueId, 'sendgrid', 'msg-123', { status: 'ok' });
expect(mockQueueRepo.update).toHaveBeenCalledWith(mockQueueId, {
status: 'sent',
completed_at: expect.any(Date),
attempts: 1,
});
expect(mockNotificationRepo.update).toHaveBeenCalledWith(mockNotificationId, {
delivery_status: 'sent',
sent_at: expect.any(Date),
});
expect(mockLogRepo.save).toHaveBeenCalledWith(expect.objectContaining({
notification_id: mockNotificationId,
queue_id: mockQueueId,
channel: 'email',
status: 'sent',
provider: 'sendgrid',
provider_message_id: 'msg-123',
}));
});
it('should do nothing if queue item not found', async () => {
mockQueueRepo.findOne.mockResolvedValue(null);
await service.markAsSent(mockQueueId);
expect(mockQueueRepo.update).not.toHaveBeenCalled();
expect(mockNotificationRepo.update).not.toHaveBeenCalled();
expect(mockLogRepo.save).not.toHaveBeenCalled();
});
});
describe('markAsFailed', () => {
it('should retry on failure when attempts < max_attempts', async () => {
const queueItemWithAttempts = { ...mockQueueItem, attempts: 0, max_attempts: 3 };
mockQueueRepo.findOne.mockResolvedValue(queueItemWithAttempts);
mockQueueRepo.update.mockResolvedValue({ affected: 1 });
mockLogRepo.save.mockResolvedValue({});
await service.markAsFailed(mockQueueId, 'Connection timeout', 'sendgrid');
expect(mockQueueRepo.update).toHaveBeenCalledWith(mockQueueId, {
status: 'retrying',
attempts: 1,
error_message: 'Connection timeout',
error_count: 1,
next_retry_at: expect.any(Date),
});
});
it('should mark as failed permanently after max retries', async () => {
const queueItemMaxRetries = { ...mockQueueItem, attempts: 2, max_attempts: 3, error_count: 2 };
mockQueueRepo.findOne.mockResolvedValue(queueItemMaxRetries);
mockQueueRepo.update.mockResolvedValue({ affected: 1 });
mockNotificationRepo.update.mockResolvedValue({ affected: 1 });
mockLogRepo.save.mockResolvedValue({});
await service.markAsFailed(mockQueueId, 'Final failure', 'sendgrid');
expect(mockQueueRepo.update).toHaveBeenCalledWith(mockQueueId, {
status: 'failed',
attempts: 3,
error_message: 'Final failure',
error_count: 3,
completed_at: expect.any(Date),
});
expect(mockNotificationRepo.update).toHaveBeenCalledWith(mockNotificationId, { delivery_status: 'failed' });
});
it('should create log entry on failure', async () => {
mockQueueRepo.findOne.mockResolvedValue(mockQueueItem);
mockQueueRepo.update.mockResolvedValue({ affected: 1 });
mockLogRepo.save.mockResolvedValue({});
await service.markAsFailed(mockQueueId, 'Test error', 'twilio');
expect(mockLogRepo.save).toHaveBeenCalledWith(expect.objectContaining({
notification_id: mockNotificationId,
queue_id: mockQueueId,
channel: 'email',
status: 'failed',
provider: 'twilio',
error_message: 'Test error',
}));
});
it('should do nothing if queue item not found', async () => {
mockQueueRepo.findOne.mockResolvedValue(null);
await service.markAsFailed(mockQueueId, 'Error');
expect(mockQueueRepo.update).not.toHaveBeenCalled();
});
it('should calculate exponential backoff for retry', async () => {
const queueItemWithOneAttempt = { ...mockQueueItem, attempts: 1, max_attempts: 3, error_count: 1 };
mockQueueRepo.findOne.mockResolvedValue(queueItemWithOneAttempt);
mockQueueRepo.update.mockResolvedValue({ affected: 1 });
mockLogRepo.save.mockResolvedValue({});
const beforeCall = Date.now();
await service.markAsFailed(mockQueueId, 'Retry error');
const afterCall = Date.now();
const updateCall = mockQueueRepo.update.mock.calls[0][1];
const nextRetryAt = new Date(updateCall.next_retry_at).getTime();
const expectedMinDelay = beforeCall + 120000;
const expectedMaxDelay = afterCall + 120000;
expect(nextRetryAt).toBeGreaterThanOrEqual(expectedMinDelay - 1000);
expect(nextRetryAt).toBeLessThanOrEqual(expectedMaxDelay + 1000);
});
});
describe('getStats', () => {
it('should return queue statistics', async () => {
const mockStats = [
{ status: 'queued', count: '10' },
{ status: 'processing', count: '5' },
{ status: 'sent', count: '100' },
{ status: 'failed', count: '3' },
{ status: 'retrying', count: '2' },
];
const mockQueryBuilder = createMockQueryBuilder({
getRawMany: jest.fn().mockResolvedValue(mockStats),
});
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
const result = await service.getStats();
expect(result).toEqual({
queued: 10,
processing: 5,
sent: 100,
failed: 3,
retrying: 2,
});
});
it('should return zero for missing statuses', async () => {
const mockStats = [
{ status: 'queued', count: '5' },
];
const mockQueryBuilder = createMockQueryBuilder({
getRawMany: jest.fn().mockResolvedValue(mockStats),
});
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
const result = await service.getStats();
expect(result).toEqual({
queued: 5,
processing: 0,
sent: 0,
failed: 0,
retrying: 0,
});
});
it('should return all zeros for empty queue', async () => {
const mockQueryBuilder = createMockQueryBuilder({
getRawMany: jest.fn().mockResolvedValue([]),
});
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
const result = await service.getStats();
expect(result).toEqual({
queued: 0,
processing: 0,
sent: 0,
failed: 0,
retrying: 0,
});
});
});
describe('getStatsByChannel', () => {
it('should return stats grouped by channel and status', async () => {
const mockStats = [
{ channel: 'email', status: 'sent', count: 50 },
{ channel: 'push', status: 'sent', count: 30 },
{ channel: 'email', status: 'failed', count: 2 },
];
const mockQueryBuilder = createMockQueryBuilder({
getRawMany: jest.fn().mockResolvedValue(mockStats),
});
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
const result = await service.getStatsByChannel();
expect(result).toEqual(mockStats);
expect(mockQueryBuilder.groupBy).toHaveBeenCalledWith('q.channel');
expect(mockQueryBuilder.addGroupBy).toHaveBeenCalledWith('q.status');
});
});
describe('cleanupOldItems', () => {
it('should delete old completed queue items', async () => {
const mockQueryBuilder = createMockQueryBuilder({
execute: jest.fn().mockResolvedValue({ affected: 50 }),
});
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
const result = await service.cleanupOldItems(30);
expect(result).toBe(50);
expect(mockQueryBuilder.delete).toHaveBeenCalled();
expect(mockQueryBuilder.where).toHaveBeenCalledWith('status IN (:...statuses)', { statuses: ['sent', 'failed'] });
});
it('should use default 30 days if not specified', async () => {
const mockQueryBuilder = createMockQueryBuilder();
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
await service.cleanupOldItems();
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('completed_at < :cutoff', expect.objectContaining({ cutoff: expect.any(Date) }));
});
it('should return 0 if no items to delete', async () => {
const mockQueryBuilder = createMockQueryBuilder({
execute: jest.fn().mockResolvedValue({ affected: 0 }),
});
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
const result = await service.cleanupOldItems(7);
expect(result).toBe(0);
});
it('should handle undefined affected count', async () => {
const mockQueryBuilder = createMockQueryBuilder({
execute: jest.fn().mockResolvedValue({}),
});
mockQueueRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
const result = await service.cleanupOldItems();
expect(result).toBe(0);
});
});
describe('cancelPending', () => {
it('should cancel pending items for notification', async () => {
mockQueueRepo.update.mockResolvedValue({ affected: 3 });
const result = await service.cancelPending(mockNotificationId);
expect(result).toBe(3);
expect(mockQueueRepo.update).toHaveBeenCalledWith(expect.objectContaining({
notification_id: mockNotificationId,
}), expect.objectContaining({
status: 'failed',
error_message: 'Cancelled',
completed_at: expect.any(Date),
}));
});
it('should return 0 if no pending items', async () => {
mockQueueRepo.update.mockResolvedValue({ affected: 0 });
const result = await service.cancelPending(mockNotificationId);
expect(result).toBe(0);
});
it('should handle undefined affected count', async () => {
mockQueueRepo.update.mockResolvedValue({});
const result = await service.cancelPending(mockNotificationId);
expect(result).toBe(0);
});
});
});
//# sourceMappingURL=notification-queue.service.spec.js.map