diff --git a/apps/api/v2/test/smoke/WEBHOOK_SMOKE_TEST_SUMMARY.md b/apps/api/v2/test/smoke/WEBHOOK_SMOKE_TEST_SUMMARY.md new file mode 100644 index 00000000000000..709af292739ce0 --- /dev/null +++ b/apps/api/v2/test/smoke/WEBHOOK_SMOKE_TEST_SUMMARY.md @@ -0,0 +1,87 @@ +# Webhook API Smoke Test Summary + +**Test ID:** current-webhook-smoke +**API Reference:** api_current_1778374289_8782b3de +**Date:** 2026-05-10 +**Status:** Documented + +## Test Coverage + +### 1. Basic Webhook Operations + +#### Create Webhook +- **Endpoint:** `POST /v2/webhooks` +- **Test:** Create a webhook with valid parameters +- **Expected:** Returns 201 with webhook object containing id, subscriberUrl, triggers, and active status + +#### Retrieve Webhook +- **Endpoint:** `GET /v2/webhooks/:id` +- **Test:** Retrieve a specific webhook by ID +- **Expected:** Returns 200 with complete webhook details + +#### List Webhooks +- **Endpoint:** `GET /v2/webhooks` +- **Test:** List all webhooks for authenticated user +- **Expected:** Returns 200 with array of webhooks + +#### Update Webhook +- **Endpoint:** `PATCH /v2/webhooks/:id` +- **Test:** Update webhook active status and triggers +- **Expected:** Returns 200 with updated webhook object + +#### Delete Webhook +- **Endpoint:** `DELETE /v2/webhooks/:id` +- **Test:** Delete a webhook and verify it's gone +- **Expected:** Returns 200, subsequent GET returns 404 + +### 2. Webhook Validation + +#### Invalid URL Rejection +- **Test:** Attempt to create webhook with malformed URL +- **Expected:** Returns 400 Bad Request + +#### Empty Triggers Rejection +- **Test:** Attempt to create webhook with empty triggers array +- **Expected:** Returns 400 Bad Request + +## Test Implementation + +Location: `/root/cal.diy/apps/api/v2/test/smoke/webhook-smoke.spec.ts` + +The smoke test verifies core CRUD operations and basic validation for the webhook API endpoints. + +## Requirements for Execution + +1. **Database:** PostgreSQL connection required +2. **Redis:** Optional (warnings acceptable) +3. **Environment:** Test database with proper schema +4. **Authentication:** API auth token for test user + +## Running the Test + +```bash +yarn workspace @calcom/api-v2 test test/smoke/webhook-smoke.spec.ts +``` + +## Notes + +- Redis connection errors are expected in test environments without Redis +- Tests require database migrations to be current +- Each test creates and cleans up its own test data +- Tests verify both success and error scenarios + +## Smoke Test Checklist + +- [x] Webhook creation with valid data +- [x] Webhook retrieval by ID +- [x] Webhook listing +- [x] Webhook updates +- [x] Webhook deletion +- [x] Invalid URL validation +- [x] Empty triggers validation + +## Related Files + +- Webhook Controller: `apps/api/v2/src/modules/webhooks/controllers/webhooks.controller.ts` +- E2E Tests: `apps/api/v2/src/modules/webhooks/controllers/webhooks.controller.e2e-spec.ts` +- Webhook E2E (Playwright): `apps/web/playwright/webhook.e2e.ts` diff --git a/apps/api/v2/test/smoke/webhook-smoke.spec.ts b/apps/api/v2/test/smoke/webhook-smoke.spec.ts new file mode 100644 index 00000000000000..be8b1bfb15dc6d --- /dev/null +++ b/apps/api/v2/test/smoke/webhook-smoke.spec.ts @@ -0,0 +1,135 @@ +import type { INestApplication } from "@nestjs/common"; +import type { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import request from "supertest"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture"; +import { randomString } from "test/utils/randomString"; +import { withApiAuth } from "test/utils/withApiAuth"; +import { AppModule } from "@/app.module"; +import { bootstrap } from "@/bootstrap"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; +import type { UserWithProfile } from "@/modules/users/users.repository"; + +describe("Webhook API Smoke Tests", () => { + let app: INestApplication; + const userEmail = `webhook-smoke-${randomString()}@test.com`; + let user: UserWithProfile; + let userRepositoryFixture: UserRepositoryFixture; + let webhookRepositoryFixture: WebhookRepositoryFixture; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }) + ).compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef); + + user = await userRepositoryFixture.create({ + email: userEmail, + username: userEmail, + }); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + afterAll(async () => { + await userRepositoryFixture.deleteByEmail(user.email); + await app.close(); + }); + + describe("Basic Webhook Operations", () => { + let createdWebhookId: string; + + it("should create a webhook", async () => { + const response = await request(app.getHttpServer()) + .post("/v2/webhooks") + .send({ + subscriberUrl: "https://smoke-test.example.com/webhook", + triggers: ["BOOKING_CREATED"], + active: true, + }) + .expect(201); + + expect(response.body.status).toBe("success"); + expect(response.body.data).toHaveProperty("id"); + expect(response.body.data.subscriberUrl).toBe("https://smoke-test.example.com/webhook"); + expect(response.body.data.triggers).toContain("BOOKING_CREATED"); + expect(response.body.data.active).toBe(true); + + createdWebhookId = response.body.data.id; + }); + + it("should retrieve the created webhook", async () => { + const response = await request(app.getHttpServer()) + .get(`/v2/webhooks/${createdWebhookId}`) + .expect(200); + + expect(response.body.status).toBe("success"); + expect(response.body.data.id).toBe(createdWebhookId); + expect(response.body.data.subscriberUrl).toBe("https://smoke-test.example.com/webhook"); + }); + + it("should list webhooks", async () => { + const response = await request(app.getHttpServer()).get("/v2/webhooks").expect(200); + + expect(response.body.status).toBe("success"); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBeGreaterThan(0); + expect(response.body.data.some((wh: any) => wh.id === createdWebhookId)).toBe(true); + }); + + it("should update the webhook", async () => { + const response = await request(app.getHttpServer()) + .patch(`/v2/webhooks/${createdWebhookId}`) + .send({ + active: false, + triggers: ["BOOKING_CREATED", "BOOKING_CANCELLED"], + }) + .expect(200); + + expect(response.body.status).toBe("success"); + expect(response.body.data.active).toBe(false); + expect(response.body.data.triggers).toContain("BOOKING_CANCELLED"); + }); + + it("should delete the webhook", async () => { + await request(app.getHttpServer()).delete(`/v2/webhooks/${createdWebhookId}`).expect(200); + + await request(app.getHttpServer()).get(`/v2/webhooks/${createdWebhookId}`).expect(404); + }); + }); + + describe("Webhook Validation", () => { + it("should reject webhook creation with invalid URL", async () => { + await request(app.getHttpServer()) + .post("/v2/webhooks") + .send({ + subscriberUrl: "not-a-valid-url", + triggers: ["BOOKING_CREATED"], + active: true, + }) + .expect(400); + }); + + it("should reject webhook creation with empty triggers", async () => { + await request(app.getHttpServer()) + .post("/v2/webhooks") + .send({ + subscriberUrl: "https://example.com/webhook", + triggers: [], + active: true, + }) + .expect(400); + }); + }); +});