From 56dffa4c88efab41f5df43f44f9361c137450bc5 Mon Sep 17 00:00:00 2001 From: orebams Date: Sat, 26 Sep 2026 22:33:38 +0100 Subject: [PATCH] feat: expose webhook registration ID header --- .../webhooks/webhooks.controller.spec.ts | 24 +++++++++++++++++++ src/common/webhooks/webhooks.controller.ts | 7 ++++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 src/common/webhooks/webhooks.controller.spec.ts diff --git a/src/common/webhooks/webhooks.controller.spec.ts b/src/common/webhooks/webhooks.controller.spec.ts new file mode 100644 index 0000000..419f8ab --- /dev/null +++ b/src/common/webhooks/webhooks.controller.spec.ts @@ -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'); + }); +}); \ No newline at end of file diff --git a/src/common/webhooks/webhooks.controller.ts b/src/common/webhooks/webhooks.controller.ts index fb8a7ba..f63a184 100644 --- a/src/common/webhooks/webhooks.controller.ts +++ b/src/common/webhooks/webhooks.controller.ts @@ -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'; @@ -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' + @@ -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 }; }