Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/common/webhooks/webhooks.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { WebhooksController } from './webhooks.controller';

describe('WebhooksController', () => {
it('returns and exposes the registered webhook ID', async () => {
const webhooks = {
registerWebhook: jest.fn().mockResolvedValue({
id: 'webhook-1',
status: 'registered',
}),
};
const response = { setHeader: jest.fn() };
const controller = new WebhooksController(webhooks as any, {} as any);

await expect(controller.register({
url: 'https://example.com/webhook',
events: [],
}, response as any)).resolves.toEqual({
success: true,
data: { id: 'webhook-1', status: 'registered' },
});

expect(response.setHeader).toHaveBeenCalledWith('X-Webhook-Id', 'webhook-1');
});
});
7 changes: 5 additions & 2 deletions src/common/webhooks/webhooks.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller, Post, Body, Get } from '@nestjs/common';
import { Body, Controller, Get, Post, Res } from '@nestjs/common';
import type { Response } from 'express';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiExtraModels, getSchemaPath } from '@nestjs/swagger';
import { ApiErrorResponse } from '../swagger/api-error-responses';
import { WebhooksService } from '../events/webhooks.service';
Expand Down Expand Up @@ -31,6 +32,7 @@ export class WebhooksController {
'|-------|-------------|---------|\n' +
'| `policy.status.change` | A policy status transition (e.g. ACTIVE → CLAIMED) | `{ policyId, fromStatus, toStatus, timestamp }` |\n' +
'| `claim.status.change` | A claim status transition (e.g. PROCESSING → PAID) | `{ claimId, fromStatus, toStatus, timestamp }` |\n\n' +
'**Registration:** The response includes the registration ID in both the response body and the `X-Webhook-Id` header.\n\n' +
'**Signature verification:** If a `secret` is provided, each delivery includes an `X-Webhook-Signature` header ' +
'containing an HMAC-SHA256 digest of the JSON payload, base64-encoded. Verify with:\n' +
'```\n' +
Expand All @@ -42,12 +44,13 @@ export class WebhooksController {
@ApiBearerAuth()
@ApiResponse({ status: 201, description: 'Webhook registered successfully', schema: { $ref: getSchemaPath(WebhookRegistrationResponseDto) } })
@ApiErrorResponse(400, 'Request body failed validation (missing url, unsupported event type, etc.).', undefined, 'url must be a URL address; events must contain only supported event types')
async register(@Body() dto: RegisterWebhookDto) {
async register(@Body() dto: RegisterWebhookDto, @Res({ passthrough: true }) response: Response) {
const result = await this.webhooks.registerWebhook({
url: dto.url,
events: dto.events,
secret: dto.secret,
});
response.setHeader('X-Webhook-Id', result.id);
return { success: true, data: result };
}

Expand Down