"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const typeorm_1 = require("@nestjs/typeorm"); const bullmq_1 = require("@nestjs/bullmq"); const common_1 = require("@nestjs/common"); const webhook_service_1 = require("../services/webhook.service"); const entities_1 = require("../entities"); describe('WebhookService', () => { let service; let webhookRepo; let deliveryRepo; let webhookQueue; const mockTenantId = '550e8400-e29b-41d4-a716-446655440001'; const mockUserId = '550e8400-e29b-41d4-a716-446655440002'; const mockWebhook = { id: 'webhook-001', tenantId: mockTenantId, name: 'Test Webhook', description: 'Test webhook description', url: 'https://example.com/webhook', events: ['user.created', 'user.updated'], headers: { 'X-Custom': 'header' }, secret: 'whsec_testsecret123', isActive: true, createdBy: mockUserId, createdAt: new Date(), }; const mockDelivery = { id: 'delivery-001', webhookId: 'webhook-001', tenantId: mockTenantId, eventType: 'user.created', payload: { type: 'user.created', data: { id: 'user-001' } }, status: entities_1.DeliveryStatus.PENDING, attempt: 1, maxAttempts: 3, createdAt: new Date(), }; beforeEach(async () => { const mockWebhookRepo = { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), find: jest.fn(), remove: jest.fn(), }; const mockDeliveryRepo = { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), find: jest.fn(), createQueryBuilder: jest.fn(), }; const mockQueue = { add: jest.fn(), }; const module = await testing_1.Test.createTestingModule({ providers: [ webhook_service_1.WebhookService, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.WebhookEntity), useValue: mockWebhookRepo }, { provide: (0, typeorm_1.getRepositoryToken)(entities_1.WebhookDeliveryEntity), useValue: mockDeliveryRepo }, { provide: (0, bullmq_1.getQueueToken)('webhooks'), useValue: mockQueue }, ], }).compile(); service = module.get(webhook_service_1.WebhookService); webhookRepo = module.get((0, typeorm_1.getRepositoryToken)(entities_1.WebhookEntity)); deliveryRepo = module.get((0, typeorm_1.getRepositoryToken)(entities_1.WebhookDeliveryEntity)); webhookQueue = module.get((0, bullmq_1.getQueueToken)('webhooks')); }); afterEach(() => { jest.clearAllMocks(); }); const mockStatsQueryBuilder = () => { const qb = { select: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), setParameters: jest.fn().mockReturnThis(), getRawOne: jest.fn().mockResolvedValue({ totalDeliveries: 10, successfulDeliveries: 8, failedDeliveries: 2, pendingDeliveries: 0, lastDeliveryAt: new Date(), }), }; deliveryRepo.createQueryBuilder.mockReturnValue(qb); return qb; }; describe('create', () => { it('should create webhook successfully', async () => { webhookRepo.create.mockReturnValue(mockWebhook); webhookRepo.save.mockResolvedValue(mockWebhook); mockStatsQueryBuilder(); const dto = { name: 'Test Webhook', url: 'https://example.com/webhook', events: ['user.created', 'user.updated'], }; const result = await service.create(mockTenantId, mockUserId, dto); expect(result).toHaveProperty('id'); expect(result).toHaveProperty('secret'); expect(webhookRepo.create).toHaveBeenCalled(); expect(webhookRepo.save).toHaveBeenCalled(); }); it('should throw for invalid events', async () => { const dto = { name: 'Test Webhook', url: 'https://example.com/webhook', events: ['invalid.event'], }; await expect(service.create(mockTenantId, mockUserId, dto)).rejects.toThrow(common_1.BadRequestException); }); }); describe('findAll', () => { it('should return all webhooks for tenant', async () => { webhookRepo.find.mockResolvedValue([mockWebhook]); mockStatsQueryBuilder(); const result = await service.findAll(mockTenantId); expect(result).toHaveLength(1); expect(webhookRepo.find).toHaveBeenCalledWith({ where: { tenantId: mockTenantId }, order: { createdAt: 'DESC' }, }); }); it('should return empty array when no webhooks', async () => { webhookRepo.find.mockResolvedValue([]); const result = await service.findAll(mockTenantId); expect(result).toHaveLength(0); }); }); describe('findOne', () => { it('should return webhook by id', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); mockStatsQueryBuilder(); const result = await service.findOne(mockTenantId, 'webhook-001'); expect(result).toHaveProperty('id', 'webhook-001'); expect(result).not.toHaveProperty('secret'); }); it('should throw when webhook not found', async () => { webhookRepo.findOne.mockResolvedValue(null); await expect(service.findOne(mockTenantId, 'invalid')).rejects.toThrow(common_1.NotFoundException); }); }); describe('update', () => { it('should update webhook successfully', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); webhookRepo.save.mockResolvedValue({ ...mockWebhook, name: 'Updated Webhook', }); mockStatsQueryBuilder(); const result = await service.update(mockTenantId, 'webhook-001', { name: 'Updated Webhook', }); expect(result.name).toBe('Updated Webhook'); }); it('should validate events on update', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); await expect(service.update(mockTenantId, 'webhook-001', { events: ['invalid.event'], })).rejects.toThrow(common_1.BadRequestException); }); it('should throw when webhook not found', async () => { webhookRepo.findOne.mockResolvedValue(null); await expect(service.update(mockTenantId, 'invalid', { name: 'New' })).rejects.toThrow(common_1.NotFoundException); }); it('should update isActive status', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); webhookRepo.save.mockResolvedValue({ ...mockWebhook, isActive: false, }); mockStatsQueryBuilder(); const result = await service.update(mockTenantId, 'webhook-001', { isActive: false, }); expect(result.isActive).toBe(false); }); }); describe('remove', () => { it('should remove webhook successfully', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); webhookRepo.remove.mockResolvedValue(mockWebhook); await service.remove(mockTenantId, 'webhook-001'); expect(webhookRepo.remove).toHaveBeenCalledWith(mockWebhook); }); it('should throw when webhook not found', async () => { webhookRepo.findOne.mockResolvedValue(null); await expect(service.remove(mockTenantId, 'invalid')).rejects.toThrow(common_1.NotFoundException); }); }); describe('regenerateSecret', () => { it('should regenerate secret successfully', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); webhookRepo.save.mockResolvedValue({ ...mockWebhook, secret: 'whsec_newsecret123', }); const result = await service.regenerateSecret(mockTenantId, 'webhook-001'); expect(result).toHaveProperty('secret'); expect(result.secret).toMatch(/^whsec_/); }); it('should throw when webhook not found', async () => { webhookRepo.findOne.mockResolvedValue(null); await expect(service.regenerateSecret(mockTenantId, 'invalid')).rejects.toThrow(common_1.NotFoundException); }); }); describe('testWebhook', () => { it('should queue test delivery', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); deliveryRepo.create.mockReturnValue(mockDelivery); deliveryRepo.save.mockResolvedValue(mockDelivery); webhookQueue.add.mockResolvedValue({}); const result = await service.testWebhook(mockTenantId, 'webhook-001', {}); expect(result).toHaveProperty('id'); expect(result.status).toBe(entities_1.DeliveryStatus.PENDING); expect(webhookQueue.add).toHaveBeenCalledWith('deliver', expect.any(Object), { priority: 1 }); }); it('should use custom payload', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); deliveryRepo.create.mockReturnValue(mockDelivery); deliveryRepo.save.mockResolvedValue(mockDelivery); webhookQueue.add.mockResolvedValue({}); await service.testWebhook(mockTenantId, 'webhook-001', { eventType: 'custom.event', payload: { custom: 'data' }, }); expect(deliveryRepo.create).toHaveBeenCalledWith(expect.objectContaining({ eventType: 'custom.event', })); }); }); describe('getDeliveries', () => { it('should return paginated deliveries', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); const qb = { where: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), skip: jest.fn().mockReturnThis(), take: jest.fn().mockReturnThis(), getManyAndCount: jest .fn() .mockResolvedValue([[mockDelivery], 1]), }; deliveryRepo.createQueryBuilder.mockReturnValue(qb); const result = await service.getDeliveries(mockTenantId, 'webhook-001', {}); expect(result.items).toHaveLength(1); expect(result.total).toBe(1); }); it('should filter by status', async () => { webhookRepo.findOne.mockResolvedValue(mockWebhook); const qb = { where: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), skip: jest.fn().mockReturnThis(), take: jest.fn().mockReturnThis(), getManyAndCount: jest.fn().mockResolvedValue([[], 0]), }; deliveryRepo.createQueryBuilder.mockReturnValue(qb); await service.getDeliveries(mockTenantId, 'webhook-001', { status: entities_1.DeliveryStatus.FAILED, }); expect(qb.andWhere).toHaveBeenCalledWith('d.status = :status', { status: entities_1.DeliveryStatus.FAILED, }); }); it('should throw when webhook not found', async () => { webhookRepo.findOne.mockResolvedValue(null); await expect(service.getDeliveries(mockTenantId, 'invalid', {})).rejects.toThrow(common_1.NotFoundException); }); }); describe('retryDelivery', () => { it('should retry failed delivery', async () => { const failedDelivery = { ...mockDelivery, status: entities_1.DeliveryStatus.FAILED, webhook: mockWebhook, }; deliveryRepo.findOne.mockResolvedValue(failedDelivery); deliveryRepo.save.mockResolvedValue({ ...failedDelivery, status: entities_1.DeliveryStatus.RETRYING, }); webhookQueue.add.mockResolvedValue({}); const result = await service.retryDelivery(mockTenantId, 'webhook-001', 'delivery-001'); expect(result.status).toBe(entities_1.DeliveryStatus.RETRYING); expect(webhookQueue.add).toHaveBeenCalled(); }); it('should throw for non-failed delivery', async () => { deliveryRepo.findOne.mockResolvedValue({ ...mockDelivery, status: entities_1.DeliveryStatus.DELIVERED, }); await expect(service.retryDelivery(mockTenantId, 'webhook-001', 'delivery-001')).rejects.toThrow(common_1.BadRequestException); }); it('should throw when delivery not found', async () => { deliveryRepo.findOne.mockResolvedValue(null); await expect(service.retryDelivery(mockTenantId, 'webhook-001', 'invalid')).rejects.toThrow(common_1.NotFoundException); }); }); describe('getStats', () => { it('should return webhook statistics', async () => { mockStatsQueryBuilder(); const result = await service.getStats('webhook-001'); expect(result).toHaveProperty('totalDeliveries', 10); expect(result).toHaveProperty('successfulDeliveries', 8); expect(result).toHaveProperty('failedDeliveries', 2); expect(result).toHaveProperty('successRate', 80); }); }); describe('dispatch', () => { it('should dispatch event to subscribed webhooks', async () => { webhookRepo.find.mockResolvedValue([mockWebhook]); deliveryRepo.create.mockReturnValue(mockDelivery); deliveryRepo.save.mockResolvedValue(mockDelivery); webhookQueue.add.mockResolvedValue({}); await service.dispatch(mockTenantId, 'user.created', { id: 'user-001' }); expect(webhookQueue.add).toHaveBeenCalled(); expect(deliveryRepo.save).toHaveBeenCalled(); }); it('should not dispatch for unsubscribed events', async () => { webhookRepo.find.mockResolvedValue([mockWebhook]); await service.dispatch(mockTenantId, 'invoice.paid', { id: 'inv-001' }); expect(webhookQueue.add).not.toHaveBeenCalled(); }); it('should skip inactive webhooks', async () => { webhookRepo.find.mockResolvedValue([]); await service.dispatch(mockTenantId, 'user.created', { id: 'user-001' }); expect(webhookQueue.add).not.toHaveBeenCalled(); }); }); describe('getAvailableEvents', () => { it('should return list of available events', () => { const events = service.getAvailableEvents(); expect(events).toBeInstanceOf(Array); expect(events.length).toBeGreaterThan(0); expect(events[0]).toHaveProperty('name'); expect(events[0]).toHaveProperty('description'); }); }); describe('signPayload', () => { it('should sign payload correctly', () => { const payload = { test: 'data' }; const secret = 'whsec_testsecret'; const signature = service.signPayload(payload, secret); expect(signature).toMatch(/^t=\d+,v1=[a-f0-9]+$/); }); }); }); //# sourceMappingURL=webhook.service.spec.js.map