- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
353 lines
16 KiB
JavaScript
353 lines
16 KiB
JavaScript
"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
|