@open-mercato/webhooks 0.6.6-develop.6409.1.4bfed821c0 → 0.6.6-develop.6410.1.2ad881013e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-002.spec.js +5 -9
  2. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-002.spec.js.map +2 -2
  3. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-003.spec.js +18 -11
  4. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-003.spec.js.map +2 -2
  5. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-005.spec.js +2 -1
  6. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-005.spec.js.map +2 -2
  7. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-006.spec.js +2 -1
  8. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-006.spec.js.map +2 -2
  9. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-008.spec.js +4 -2
  10. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-008.spec.js.map +2 -2
  11. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-010.spec.js +2 -1
  12. package/dist/modules/webhooks/__integration__/TC-WEBHOOK-010.spec.js.map +2 -2
  13. package/dist/modules/webhooks/__integration__/helpers/fixtures.js +12 -0
  14. package/dist/modules/webhooks/__integration__/helpers/fixtures.js.map +2 -2
  15. package/package.json +6 -6
  16. package/src/modules/webhooks/__integration__/TC-WEBHOOK-002.spec.ts +5 -9
  17. package/src/modules/webhooks/__integration__/TC-WEBHOOK-003.spec.ts +26 -11
  18. package/src/modules/webhooks/__integration__/TC-WEBHOOK-005.spec.ts +2 -1
  19. package/src/modules/webhooks/__integration__/TC-WEBHOOK-006.spec.ts +2 -1
  20. package/src/modules/webhooks/__integration__/TC-WEBHOOK-008.spec.ts +4 -2
  21. package/src/modules/webhooks/__integration__/TC-WEBHOOK-010.spec.ts +2 -1
  22. package/src/modules/webhooks/__integration__/helpers/fixtures.ts +18 -0
@@ -3,6 +3,8 @@ import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration
3
3
  import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
4
4
  import {
5
5
  buildMockInboundUrl,
6
+ mockInboundAuthHeaders,
7
+ mockInboundInvalidAuthHeaders,
6
8
  createWebhookFixture,
7
9
  deleteWebhookIfExists,
8
10
  getDeliveryDetail,
@@ -18,9 +20,7 @@ test.describe("TC-WEBHOOK-002: Webhook delivery lifecycle", () => {
18
20
  name: `Webhook Delivery ${Date.now()}`,
19
21
  url: buildMockInboundUrl(),
20
22
  subscribedEvents: ["catalog.product.created"],
21
- customHeaders: {
22
- "x-mock-webhook-signature": "valid"
23
- }
23
+ customHeaders: mockInboundAuthHeaders()
24
24
  });
25
25
  webhookId = created.id;
26
26
  const testResponse = await apiRequest(request, "POST", `/api/webhooks/${created.id}/test`, {
@@ -76,9 +76,7 @@ test.describe("TC-WEBHOOK-002: Webhook delivery lifecycle", () => {
76
76
  name: `Webhook Retry ${Date.now()}`,
77
77
  url: buildMockInboundUrl(),
78
78
  subscribedEvents: ["catalog.product.updated"],
79
- customHeaders: {
80
- "x-mock-webhook-signature": "invalid"
81
- }
79
+ customHeaders: mockInboundInvalidAuthHeaders()
82
80
  });
83
81
  webhookId = created.id;
84
82
  const failedResponse = await apiRequest(request, "POST", `/api/webhooks/${created.id}/test`, {
@@ -103,9 +101,7 @@ test.describe("TC-WEBHOOK-002: Webhook delivery lifecycle", () => {
103
101
  const updateResponse = await apiRequest(request, "PUT", `/api/webhooks/${created.id}`, {
104
102
  token,
105
103
  data: {
106
- customHeaders: {
107
- "x-mock-webhook-signature": "valid"
108
- }
104
+ customHeaders: mockInboundAuthHeaders()
109
105
  }
110
106
  });
111
107
  expect(updateResponse.status()).toBe(200);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/__integration__/TC-WEBHOOK-002.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n buildMockInboundUrl,\n createWebhookFixture,\n deleteWebhookIfExists,\n getDeliveryDetail,\n listWebhookDeliveries,\n waitForDeliveryStatus,\n} from './helpers/fixtures';\n\ntest.describe('TC-WEBHOOK-002: Webhook delivery lifecycle', () => {\n test('should deliver a synchronous test webhook and expose the delivery through canonical and aliased routes', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Delivery ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: ['catalog.product.created'],\n customHeaders: {\n 'x-mock-webhook-signature': 'valid',\n },\n });\n webhookId = created.id;\n\n const testResponse = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/test`, {\n token,\n data: {\n eventType: 'catalog.product.created',\n payload: {\n type: 'catalog.product.created',\n timestamp: new Date().toISOString(),\n data: {\n sku: `SKU-${Date.now()}`,\n },\n },\n },\n });\n expect(testResponse.status()).toBe(200);\n const testBody = await readJsonSafe<{ success: boolean; delivery: { id: string; status: string; responseStatus: number | null; responseBody: string | null; responseHeaders: Record<string, string> | null } }>(testResponse);\n expect(testBody?.success).toBe(true);\n expect(testBody?.delivery.status).toBe('delivered');\n expect(testBody?.delivery.responseStatus).toBe(200);\n expect(testBody?.delivery.responseBody).toBe(JSON.stringify({ ok: true }));\n expect(testBody?.delivery.responseHeaders?.['content-type']).toContain('application/json');\n\n const deliveryId = testBody?.delivery.id;\n expect(typeof deliveryId).toBe('string');\n\n const canonicalList = await listWebhookDeliveries(request, token, created.id);\n expect(canonicalList.items.some((item) => item.id === deliveryId)).toBe(true);\n\n const aliasedList = await listWebhookDeliveries(\n request,\n token,\n created.id,\n `/api/webhooks/webhook-deliveries?webhookId=${encodeURIComponent(created.id)}`,\n );\n expect(aliasedList.items.some((item) => item.id === deliveryId)).toBe(true);\n\n const canonicalDetail = await getDeliveryDetail(request, token, deliveryId as string);\n expect(canonicalDetail.status).toBe('delivered');\n expect(canonicalDetail.payload.type).toBe('catalog.product.created');\n\n const aliasedDetail = await getDeliveryDetail(\n request,\n token,\n deliveryId as string,\n `/api/webhooks/webhook-deliveries/${deliveryId}`,\n );\n expect(aliasedDetail.id).toBe(deliveryId);\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n\n test('should allow retrying a failed delivery after fixing the endpoint configuration', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Retry ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: ['catalog.product.updated'],\n customHeaders: {\n 'x-mock-webhook-signature': 'invalid',\n },\n });\n webhookId = created.id;\n\n const failedResponse = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/test`, {\n token,\n data: {\n eventType: 'catalog.product.updated',\n payload: {\n type: 'catalog.product.updated',\n timestamp: new Date().toISOString(),\n data: {\n sku: `SKU-RETRY-${Date.now()}`,\n },\n },\n },\n });\n expect(failedResponse.status()).toBe(200);\n const failedBody = await readJsonSafe<{ delivery: { id: string; status: string; responseStatus: number | null } }>(failedResponse);\n expect(failedBody?.delivery.status).toBe('expired');\n expect(failedBody?.delivery.responseStatus).toBe(400);\n\n const deliveryId = failedBody?.delivery.id;\n expect(typeof deliveryId).toBe('string');\n\n const updateResponse = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token,\n data: {\n customHeaders: {\n 'x-mock-webhook-signature': 'valid',\n },\n },\n });\n expect(updateResponse.status()).toBe(200);\n\n const retryResponse = await apiRequest(request, 'POST', `/api/webhooks/deliveries/${deliveryId}/retry`, { token });\n expect(retryResponse.status()).toBe(200);\n\n const deliveryAfterRetry = await waitForDeliveryStatus(request, token, deliveryId as string, 'delivered');\n expect(deliveryAfterRetry.responseStatus).toBe(200);\n expect(deliveryAfterRetry.attemptNumber).toBeGreaterThanOrEqual(2);\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n});\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n buildMockInboundUrl,\n mockInboundAuthHeaders,\n mockInboundInvalidAuthHeaders,\n createWebhookFixture,\n deleteWebhookIfExists,\n getDeliveryDetail,\n listWebhookDeliveries,\n waitForDeliveryStatus,\n} from './helpers/fixtures';\n\ntest.describe('TC-WEBHOOK-002: Webhook delivery lifecycle', () => {\n test('should deliver a synchronous test webhook and expose the delivery through canonical and aliased routes', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Delivery ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: ['catalog.product.created'],\n customHeaders: mockInboundAuthHeaders(),\n });\n webhookId = created.id;\n\n const testResponse = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/test`, {\n token,\n data: {\n eventType: 'catalog.product.created',\n payload: {\n type: 'catalog.product.created',\n timestamp: new Date().toISOString(),\n data: {\n sku: `SKU-${Date.now()}`,\n },\n },\n },\n });\n expect(testResponse.status()).toBe(200);\n const testBody = await readJsonSafe<{ success: boolean; delivery: { id: string; status: string; responseStatus: number | null; responseBody: string | null; responseHeaders: Record<string, string> | null } }>(testResponse);\n expect(testBody?.success).toBe(true);\n expect(testBody?.delivery.status).toBe('delivered');\n expect(testBody?.delivery.responseStatus).toBe(200);\n expect(testBody?.delivery.responseBody).toBe(JSON.stringify({ ok: true }));\n expect(testBody?.delivery.responseHeaders?.['content-type']).toContain('application/json');\n\n const deliveryId = testBody?.delivery.id;\n expect(typeof deliveryId).toBe('string');\n\n const canonicalList = await listWebhookDeliveries(request, token, created.id);\n expect(canonicalList.items.some((item) => item.id === deliveryId)).toBe(true);\n\n const aliasedList = await listWebhookDeliveries(\n request,\n token,\n created.id,\n `/api/webhooks/webhook-deliveries?webhookId=${encodeURIComponent(created.id)}`,\n );\n expect(aliasedList.items.some((item) => item.id === deliveryId)).toBe(true);\n\n const canonicalDetail = await getDeliveryDetail(request, token, deliveryId as string);\n expect(canonicalDetail.status).toBe('delivered');\n expect(canonicalDetail.payload.type).toBe('catalog.product.created');\n\n const aliasedDetail = await getDeliveryDetail(\n request,\n token,\n deliveryId as string,\n `/api/webhooks/webhook-deliveries/${deliveryId}`,\n );\n expect(aliasedDetail.id).toBe(deliveryId);\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n\n test('should allow retrying a failed delivery after fixing the endpoint configuration', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Retry ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: ['catalog.product.updated'],\n customHeaders: mockInboundInvalidAuthHeaders(),\n });\n webhookId = created.id;\n\n const failedResponse = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/test`, {\n token,\n data: {\n eventType: 'catalog.product.updated',\n payload: {\n type: 'catalog.product.updated',\n timestamp: new Date().toISOString(),\n data: {\n sku: `SKU-RETRY-${Date.now()}`,\n },\n },\n },\n });\n expect(failedResponse.status()).toBe(200);\n const failedBody = await readJsonSafe<{ delivery: { id: string; status: string; responseStatus: number | null } }>(failedResponse);\n expect(failedBody?.delivery.status).toBe('expired');\n expect(failedBody?.delivery.responseStatus).toBe(400);\n\n const deliveryId = failedBody?.delivery.id;\n expect(typeof deliveryId).toBe('string');\n\n const updateResponse = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token,\n data: {\n customHeaders: mockInboundAuthHeaders(),\n },\n });\n expect(updateResponse.status()).toBe(200);\n\n const retryResponse = await apiRequest(request, 'POST', `/api/webhooks/deliveries/${deliveryId}/retry`, { token });\n expect(retryResponse.status()).toBe(200);\n\n const deliveryAfterRetry = await waitForDeliveryStatus(request, token, deliveryId as string, 'delivered');\n expect(deliveryAfterRetry.responseStatus).toBe(200);\n expect(deliveryAfterRetry.attemptNumber).toBeGreaterThanOrEqual(2);\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n});\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,10 @@
1
+ import { createHmac } from "node:crypto";
1
2
  import { expect, test } from "@playwright/test";
2
3
  const BASE_URL = process.env.BASE_URL?.trim() || "http://localhost:3000";
4
+ const MOCK_INBOUND_WEBHOOK_SECRET = process.env.MOCK_INBOUND_WEBHOOK_SECRET?.trim() || "open-mercato-mock-dev-inbound-webhook-secret";
5
+ function signMockInboundWebhook(rawBody) {
6
+ return createHmac("sha256", MOCK_INBOUND_WEBHOOK_SECRET).update(rawBody, "utf-8").digest("hex");
7
+ }
3
8
  test.describe("TC-WEBHOOK-003: Inbound webhook receiver", () => {
4
9
  test("should accept valid inbound webhooks, mark duplicates, and reject invalid endpoints or signatures", async ({ request }) => {
5
10
  const messageId = `msg-${Date.now()}`;
@@ -11,16 +16,18 @@ test.describe("TC-WEBHOOK-003: Inbound webhook receiver", () => {
11
16
  externalId: `ext-${Date.now()}`
12
17
  }
13
18
  };
19
+ const rawBody = JSON.stringify(payload);
20
+ const validSignature = signMockInboundWebhook(rawBody);
14
21
  const firstResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {
15
22
  method: "POST",
16
23
  headers: {
17
24
  "content-type": "application/json",
18
- "x-mock-webhook-signature": "valid",
25
+ "x-mock-webhook-signature": validSignature,
19
26
  "webhook-id": messageId,
20
27
  "webhook-timestamp": replayTimestamp,
21
28
  "webhook-signature": "v1,mock"
22
29
  },
23
- data: JSON.stringify(payload)
30
+ data: rawBody
24
31
  });
25
32
  expect(firstResponse.status()).toBe(200);
26
33
  await expect(firstResponse.json()).resolves.toEqual({ ok: true });
@@ -28,12 +35,12 @@ test.describe("TC-WEBHOOK-003: Inbound webhook receiver", () => {
28
35
  method: "POST",
29
36
  headers: {
30
37
  "content-type": "application/json",
31
- "x-mock-webhook-signature": "valid",
38
+ "x-mock-webhook-signature": validSignature,
32
39
  "webhook-id": messageId,
33
40
  "webhook-timestamp": replayTimestamp,
34
41
  "webhook-signature": "v1,mock"
35
42
  },
36
- data: JSON.stringify(payload)
43
+ data: rawBody
37
44
  });
38
45
  expect(duplicateResponse.status()).toBe(200);
39
46
  await expect(duplicateResponse.json()).resolves.toEqual({ ok: true, duplicate: true });
@@ -41,11 +48,11 @@ test.describe("TC-WEBHOOK-003: Inbound webhook receiver", () => {
41
48
  method: "POST",
42
49
  headers: {
43
50
  "content-type": "application/json",
44
- "x-mock-webhook-signature": "valid",
51
+ "x-mock-webhook-signature": validSignature,
45
52
  "webhook-timestamp": replayTimestamp,
46
53
  "webhook-signature": "v1,mock"
47
54
  },
48
- data: JSON.stringify(payload)
55
+ data: rawBody
49
56
  });
50
57
  expect(replayWithoutMessageIdResponse.status()).toBe(200);
51
58
  await expect(replayWithoutMessageIdResponse.json()).resolves.toEqual({ ok: true });
@@ -53,11 +60,11 @@ test.describe("TC-WEBHOOK-003: Inbound webhook receiver", () => {
53
60
  method: "POST",
54
61
  headers: {
55
62
  "content-type": "application/json",
56
- "x-mock-webhook-signature": "valid",
63
+ "x-mock-webhook-signature": validSignature,
57
64
  "webhook-timestamp": replayTimestamp,
58
65
  "webhook-signature": "v1,mock"
59
66
  },
60
- data: JSON.stringify(payload)
67
+ data: rawBody
61
68
  });
62
69
  expect(replayWithoutMessageIdDuplicateResponse.status()).toBe(200);
63
70
  await expect(replayWithoutMessageIdDuplicateResponse.json()).resolves.toEqual({ ok: true, duplicate: true });
@@ -67,7 +74,7 @@ test.describe("TC-WEBHOOK-003: Inbound webhook receiver", () => {
67
74
  "content-type": "application/json",
68
75
  "x-mock-webhook-signature": "invalid"
69
76
  },
70
- data: JSON.stringify(payload)
77
+ data: rawBody
71
78
  });
72
79
  expect(invalidSignatureResponse.status()).toBe(400);
73
80
  await expect(invalidSignatureResponse.json()).resolves.toEqual({ error: "Verification failed" });
@@ -75,9 +82,9 @@ test.describe("TC-WEBHOOK-003: Inbound webhook receiver", () => {
75
82
  method: "POST",
76
83
  headers: {
77
84
  "content-type": "application/json",
78
- "x-mock-webhook-signature": "valid"
85
+ "x-mock-webhook-signature": validSignature
79
86
  },
80
- data: JSON.stringify(payload)
87
+ data: rawBody
81
88
  });
82
89
  expect(unknownEndpointResponse.status()).toBe(404);
83
90
  await expect(unknownEndpointResponse.json()).resolves.toEqual({ error: "Webhook endpoint not found" });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/__integration__/TC-WEBHOOK-003.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test';\n\nconst BASE_URL = process.env.BASE_URL?.trim() || 'http://localhost:3000';\n\ntest.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {\n test('should accept valid inbound webhooks, mark duplicates, and reject invalid endpoints or signatures', async ({ request }) => {\n const messageId = `msg-${Date.now()}`;\n const replayTimestamp = String(Math.floor(Date.now() / 1000));\n const payload = {\n type: 'mock.inbound.received',\n timestamp: new Date().toISOString(),\n data: {\n externalId: `ext-${Date.now()}`,\n },\n };\n\n const firstResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': 'valid',\n 'webhook-id': messageId,\n 'webhook-timestamp': replayTimestamp,\n 'webhook-signature': 'v1,mock',\n },\n data: JSON.stringify(payload),\n });\n expect(firstResponse.status()).toBe(200);\n await expect(firstResponse.json()).resolves.toEqual({ ok: true });\n\n const duplicateResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': 'valid',\n 'webhook-id': messageId,\n 'webhook-timestamp': replayTimestamp,\n 'webhook-signature': 'v1,mock',\n },\n data: JSON.stringify(payload),\n });\n expect(duplicateResponse.status()).toBe(200);\n await expect(duplicateResponse.json()).resolves.toEqual({ ok: true, duplicate: true });\n\n const replayWithoutMessageIdResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': 'valid',\n 'webhook-timestamp': replayTimestamp,\n 'webhook-signature': 'v1,mock',\n },\n data: JSON.stringify(payload),\n });\n expect(replayWithoutMessageIdResponse.status()).toBe(200);\n await expect(replayWithoutMessageIdResponse.json()).resolves.toEqual({ ok: true });\n\n const replayWithoutMessageIdDuplicateResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': 'valid',\n 'webhook-timestamp': replayTimestamp,\n 'webhook-signature': 'v1,mock',\n },\n data: JSON.stringify(payload),\n });\n expect(replayWithoutMessageIdDuplicateResponse.status()).toBe(200);\n await expect(replayWithoutMessageIdDuplicateResponse.json()).resolves.toEqual({ ok: true, duplicate: true });\n\n const invalidSignatureResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': 'invalid',\n },\n data: JSON.stringify(payload),\n });\n expect(invalidSignatureResponse.status()).toBe(400);\n await expect(invalidSignatureResponse.json()).resolves.toEqual({ error: 'Verification failed' });\n\n const unknownEndpointResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/missing_endpoint`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': 'valid',\n },\n data: JSON.stringify(payload),\n });\n expect(unknownEndpointResponse.status()).toBe(404);\n await expect(unknownEndpointResponse.json()).resolves.toEqual({ error: 'Webhook endpoint not found' });\n });\n});\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHmac } from 'node:crypto';\nimport { expect, test } from '@playwright/test';\n\nconst BASE_URL = process.env.BASE_URL?.trim() || 'http://localhost:3000';\n\n// Mirrors the create-app mock inbound webhook adapter, which verifies an HMAC-SHA256\n// signature over the raw request body in the x-mock-webhook-signature header. The running\n// app resolves its secret from MOCK_INBOUND_WEBHOOK_SECRET (exported by the CI workflow so\n// it works even when `yarn start` forces NODE_ENV=production); we read the SAME env var here\n// so the test signs with the same key. The dev fallback is only a local-default convenience.\nconst MOCK_INBOUND_WEBHOOK_SECRET =\n process.env.MOCK_INBOUND_WEBHOOK_SECRET?.trim() || 'open-mercato-mock-dev-inbound-webhook-secret';\n\nfunction signMockInboundWebhook(rawBody: string): string {\n return createHmac('sha256', MOCK_INBOUND_WEBHOOK_SECRET).update(rawBody, 'utf-8').digest('hex');\n}\n\ntest.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {\n test('should accept valid inbound webhooks, mark duplicates, and reject invalid endpoints or signatures', async ({ request }) => {\n const messageId = `msg-${Date.now()}`;\n const replayTimestamp = String(Math.floor(Date.now() / 1000));\n const payload = {\n type: 'mock.inbound.received',\n timestamp: new Date().toISOString(),\n data: {\n externalId: `ext-${Date.now()}`,\n },\n };\n const rawBody = JSON.stringify(payload);\n const validSignature = signMockInboundWebhook(rawBody);\n\n const firstResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': validSignature,\n 'webhook-id': messageId,\n 'webhook-timestamp': replayTimestamp,\n 'webhook-signature': 'v1,mock',\n },\n data: rawBody,\n });\n expect(firstResponse.status()).toBe(200);\n await expect(firstResponse.json()).resolves.toEqual({ ok: true });\n\n const duplicateResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': validSignature,\n 'webhook-id': messageId,\n 'webhook-timestamp': replayTimestamp,\n 'webhook-signature': 'v1,mock',\n },\n data: rawBody,\n });\n expect(duplicateResponse.status()).toBe(200);\n await expect(duplicateResponse.json()).resolves.toEqual({ ok: true, duplicate: true });\n\n const replayWithoutMessageIdResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': validSignature,\n 'webhook-timestamp': replayTimestamp,\n 'webhook-signature': 'v1,mock',\n },\n data: rawBody,\n });\n expect(replayWithoutMessageIdResponse.status()).toBe(200);\n await expect(replayWithoutMessageIdResponse.json()).resolves.toEqual({ ok: true });\n\n const replayWithoutMessageIdDuplicateResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': validSignature,\n 'webhook-timestamp': replayTimestamp,\n 'webhook-signature': 'v1,mock',\n },\n data: rawBody,\n });\n expect(replayWithoutMessageIdDuplicateResponse.status()).toBe(200);\n await expect(replayWithoutMessageIdDuplicateResponse.json()).resolves.toEqual({ ok: true, duplicate: true });\n\n const invalidSignatureResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': 'invalid',\n },\n data: rawBody,\n });\n expect(invalidSignatureResponse.status()).toBe(400);\n await expect(invalidSignatureResponse.json()).resolves.toEqual({ error: 'Verification failed' });\n\n const unknownEndpointResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/missing_endpoint`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-mock-webhook-signature': validSignature,\n },\n data: rawBody,\n });\n expect(unknownEndpointResponse.status()).toBe(404);\n await expect(unknownEndpointResponse.json()).resolves.toEqual({ error: 'Webhook endpoint not found' });\n });\n});\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAC3B,SAAS,QAAQ,YAAY;AAE7B,MAAM,WAAW,QAAQ,IAAI,UAAU,KAAK,KAAK;AAOjD,MAAM,8BACJ,QAAQ,IAAI,6BAA6B,KAAK,KAAK;AAErD,SAAS,uBAAuB,SAAyB;AACvD,SAAO,WAAW,UAAU,2BAA2B,EAAE,OAAO,SAAS,OAAO,EAAE,OAAO,KAAK;AAChG;AAEA,KAAK,SAAS,4CAA4C,MAAM;AAC9D,OAAK,qGAAqG,OAAO,EAAE,QAAQ,MAAM;AAC/H,UAAM,YAAY,OAAO,KAAK,IAAI,CAAC;AACnC,UAAM,kBAAkB,OAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,CAAC;AAC5D,UAAM,UAAU;AAAA,MACd,MAAM;AAAA,MACN,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,MAAM;AAAA,QACJ,YAAY,OAAO,KAAK,IAAI,CAAC;AAAA,MAC/B;AAAA,IACF;AACA,UAAM,UAAU,KAAK,UAAU,OAAO;AACtC,UAAM,iBAAiB,uBAAuB,OAAO;AAErD,UAAM,gBAAgB,MAAM,QAAQ,MAAM,GAAG,QAAQ,sCAAsC;AAAA,MACzF,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,4BAA4B;AAAA,QAC5B,cAAc;AAAA,QACd,qBAAqB;AAAA,QACrB,qBAAqB;AAAA,MACvB;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AACD,WAAO,cAAc,OAAO,CAAC,EAAE,KAAK,GAAG;AACvC,UAAM,OAAO,cAAc,KAAK,CAAC,EAAE,SAAS,QAAQ,EAAE,IAAI,KAAK,CAAC;AAEhE,UAAM,oBAAoB,MAAM,QAAQ,MAAM,GAAG,QAAQ,sCAAsC;AAAA,MAC7F,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,4BAA4B;AAAA,QAC5B,cAAc;AAAA,QACd,qBAAqB;AAAA,QACrB,qBAAqB;AAAA,MACvB;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AACD,WAAO,kBAAkB,OAAO,CAAC,EAAE,KAAK,GAAG;AAC3C,UAAM,OAAO,kBAAkB,KAAK,CAAC,EAAE,SAAS,QAAQ,EAAE,IAAI,MAAM,WAAW,KAAK,CAAC;AAErF,UAAM,iCAAiC,MAAM,QAAQ,MAAM,GAAG,QAAQ,sCAAsC;AAAA,MAC1G,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,4BAA4B;AAAA,QAC5B,qBAAqB;AAAA,QACrB,qBAAqB;AAAA,MACvB;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AACD,WAAO,+BAA+B,OAAO,CAAC,EAAE,KAAK,GAAG;AACxD,UAAM,OAAO,+BAA+B,KAAK,CAAC,EAAE,SAAS,QAAQ,EAAE,IAAI,KAAK,CAAC;AAEjF,UAAM,0CAA0C,MAAM,QAAQ,MAAM,GAAG,QAAQ,sCAAsC;AAAA,MACnH,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,4BAA4B;AAAA,QAC5B,qBAAqB;AAAA,QACrB,qBAAqB;AAAA,MACvB;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AACD,WAAO,wCAAwC,OAAO,CAAC,EAAE,KAAK,GAAG;AACjE,UAAM,OAAO,wCAAwC,KAAK,CAAC,EAAE,SAAS,QAAQ,EAAE,IAAI,MAAM,WAAW,KAAK,CAAC;AAE3G,UAAM,2BAA2B,MAAM,QAAQ,MAAM,GAAG,QAAQ,sCAAsC;AAAA,MACpG,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,4BAA4B;AAAA,MAC9B;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AACD,WAAO,yBAAyB,OAAO,CAAC,EAAE,KAAK,GAAG;AAClD,UAAM,OAAO,yBAAyB,KAAK,CAAC,EAAE,SAAS,QAAQ,EAAE,OAAO,sBAAsB,CAAC;AAE/F,UAAM,0BAA0B,MAAM,QAAQ,MAAM,GAAG,QAAQ,0CAA0C;AAAA,MACvG,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,4BAA4B;AAAA,MAC9B;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AACD,WAAO,wBAAwB,OAAO,CAAC,EAAE,KAAK,GAAG;AACjD,UAAM,OAAO,wBAAwB,KAAK,CAAC,EAAE,SAAS,QAAQ,EAAE,OAAO,6BAA6B,CAAC;AAAA,EACvG,CAAC;AACH,CAAC;",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,7 @@ import {
5
5
  cleanupWebhooksUser,
6
6
  createWebhookFixture,
7
7
  deleteWebhookIfExists,
8
+ mockInboundAuthHeaders,
8
9
  provisionWebhooksUser
9
10
  } from "./helpers/fixtures.js";
10
11
  test.describe("TC-WEBHOOK-005: RBAC permission gates by feature flag", () => {
@@ -31,7 +32,7 @@ test.describe("TC-WEBHOOK-005: RBAC permission gates by feature flag", () => {
31
32
  name: `Webhook RBAC ${Date.now()}`,
32
33
  url: "https://example.com/webhook-rbac",
33
34
  subscribedEvents: ["catalog.product.created"],
34
- customHeaders: { "x-mock-webhook-signature": "valid" }
35
+ customHeaders: mockInboundAuthHeaders()
35
36
  });
36
37
  webhookId = created.id;
37
38
  const adminList = await apiRequest(request, "GET", "/api/webhooks", { token: adminToken });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/__integration__/TC-WEBHOOK-005.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { getTokenScope } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n cleanupWebhooksUser,\n createWebhookFixture,\n deleteWebhookIfExists,\n provisionWebhooksUser,\n type ProvisionedWebhooksUser,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-005: RBAC permission gates \u2014 unauthorized access by feature flag\n * Source: https://github.com/open-mercato/open-mercato/issues/2482\n *\n * Verifies every webhooks API surface is gated by its declared feature:\n * webhooks.view \u2192 GET list + GET detail + deliveries\n * webhooks.manage \u2192 POST + PUT + DELETE\n * webhooks.secrets \u2192 POST rotate-secret\n * webhooks.test \u2192 POST test\n * A viewer holding only webhooks.view passes reads but is denied every write/secret/test\n * surface; a user holding no webhooks features is denied reads. Admin (webhooks.*) proves\n * the positive paths. Denials accept 401 or 403 per the issue's acceptance criteria.\n */\ntest.describe('TC-WEBHOOK-005: RBAC permission gates by feature flag', () => {\n test('should gate each webhooks surface behind its declared feature', async ({ request }) => {\n const adminToken = await getAuthToken(request);\n const scope = getTokenScope(adminToken);\n\n let webhookId: string | null = null;\n let viewer: ProvisionedWebhooksUser | null = null;\n let noAccess: ProvisionedWebhooksUser | null = null;\n\n try {\n viewer = await provisionWebhooksUser(request, adminToken, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: ['webhooks.view'],\n slug: 'viewer',\n });\n noAccess = await provisionWebhooksUser(request, adminToken, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: [],\n slug: 'noaccess',\n });\n\n // --- Positive: admin (webhooks.*) can exercise every surface ---\n const created = await createWebhookFixture(request, adminToken, {\n name: `Webhook RBAC ${Date.now()}`,\n url: 'https://example.com/webhook-rbac',\n subscribedEvents: ['catalog.product.created'],\n customHeaders: { 'x-mock-webhook-signature': 'valid' },\n });\n webhookId = created.id;\n\n const adminList = await apiRequest(request, 'GET', '/api/webhooks', { token: adminToken });\n expect(adminList.status()).toBe(200);\n const adminDetail = await apiRequest(request, 'GET', `/api/webhooks/${created.id}`, { token: adminToken });\n expect(adminDetail.status()).toBe(200);\n\n // --- webhooks.view gate ---\n const viewerList = await apiRequest(request, 'GET', '/api/webhooks', { token: viewer.token });\n expect(viewerList.status()).toBe(200);\n const viewerDetail = await apiRequest(request, 'GET', `/api/webhooks/${created.id}`, { token: viewer.token });\n expect(viewerDetail.status()).toBe(200);\n\n const noAccessList = await apiRequest(request, 'GET', '/api/webhooks', { token: noAccess.token });\n expect([401, 403]).toContain(noAccessList.status());\n const noAccessDetail = await apiRequest(request, 'GET', `/api/webhooks/${created.id}`, { token: noAccess.token });\n expect([401, 403]).toContain(noAccessDetail.status());\n\n // --- webhooks.manage gate (viewer lacks manage) ---\n const viewerCreate = await apiRequest(request, 'POST', '/api/webhooks', {\n token: viewer.token,\n data: {\n name: `Webhook RBAC Denied ${Date.now()}`,\n url: 'https://example.com/webhook-denied',\n subscribedEvents: ['catalog.product.created'],\n },\n });\n expect([401, 403]).toContain(viewerCreate.status());\n\n const viewerUpdate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token: viewer.token,\n data: { name: `Webhook RBAC Hijack ${Date.now()}` },\n });\n expect([401, 403]).toContain(viewerUpdate.status());\n\n const viewerDelete = await apiRequest(request, 'DELETE', `/api/webhooks/${created.id}`, { token: viewer.token });\n expect([401, 403]).toContain(viewerDelete.status());\n\n // --- webhooks.secrets gate (viewer lacks secrets) ---\n const viewerRotate = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/rotate-secret`, {\n token: viewer.token,\n });\n expect([401, 403]).toContain(viewerRotate.status());\n\n // --- webhooks.test gate (viewer lacks test) ---\n const viewerTest = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/test`, {\n token: viewer.token,\n data: { eventType: 'catalog.product.created' },\n });\n expect([401, 403]).toContain(viewerTest.status());\n\n // The webhook must still exist \u2014 none of the denied writes mutated it.\n const stillThere = await apiRequest(request, 'GET', `/api/webhooks/${created.id}`, { token: adminToken });\n expect(stillThere.status()).toBe(200);\n\n // --- Positive secrets + test paths (admin holds both features) ---\n const adminRotate = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/rotate-secret`, {\n token: adminToken,\n });\n expect(adminRotate.status()).toBe(200);\n const adminTest = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/test`, {\n token: adminToken,\n data: { eventType: 'catalog.product.created' },\n });\n expect(adminTest.status()).toBe(200);\n } finally {\n await deleteWebhookIfExists(request, adminToken, webhookId);\n await cleanupWebhooksUser(request, adminToken, viewer);\n await cleanupWebhooksUser(request, adminToken, noAccess);\n }\n });\n});\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { getTokenScope } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n cleanupWebhooksUser,\n createWebhookFixture,\n deleteWebhookIfExists,\n mockInboundAuthHeaders,\n provisionWebhooksUser,\n type ProvisionedWebhooksUser,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-005: RBAC permission gates \u2014 unauthorized access by feature flag\n * Source: https://github.com/open-mercato/open-mercato/issues/2482\n *\n * Verifies every webhooks API surface is gated by its declared feature:\n * webhooks.view \u2192 GET list + GET detail + deliveries\n * webhooks.manage \u2192 POST + PUT + DELETE\n * webhooks.secrets \u2192 POST rotate-secret\n * webhooks.test \u2192 POST test\n * A viewer holding only webhooks.view passes reads but is denied every write/secret/test\n * surface; a user holding no webhooks features is denied reads. Admin (webhooks.*) proves\n * the positive paths. Denials accept 401 or 403 per the issue's acceptance criteria.\n */\ntest.describe('TC-WEBHOOK-005: RBAC permission gates by feature flag', () => {\n test('should gate each webhooks surface behind its declared feature', async ({ request }) => {\n const adminToken = await getAuthToken(request);\n const scope = getTokenScope(adminToken);\n\n let webhookId: string | null = null;\n let viewer: ProvisionedWebhooksUser | null = null;\n let noAccess: ProvisionedWebhooksUser | null = null;\n\n try {\n viewer = await provisionWebhooksUser(request, adminToken, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: ['webhooks.view'],\n slug: 'viewer',\n });\n noAccess = await provisionWebhooksUser(request, adminToken, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: [],\n slug: 'noaccess',\n });\n\n // --- Positive: admin (webhooks.*) can exercise every surface ---\n const created = await createWebhookFixture(request, adminToken, {\n name: `Webhook RBAC ${Date.now()}`,\n url: 'https://example.com/webhook-rbac',\n subscribedEvents: ['catalog.product.created'],\n customHeaders: mockInboundAuthHeaders(),\n });\n webhookId = created.id;\n\n const adminList = await apiRequest(request, 'GET', '/api/webhooks', { token: adminToken });\n expect(adminList.status()).toBe(200);\n const adminDetail = await apiRequest(request, 'GET', `/api/webhooks/${created.id}`, { token: adminToken });\n expect(adminDetail.status()).toBe(200);\n\n // --- webhooks.view gate ---\n const viewerList = await apiRequest(request, 'GET', '/api/webhooks', { token: viewer.token });\n expect(viewerList.status()).toBe(200);\n const viewerDetail = await apiRequest(request, 'GET', `/api/webhooks/${created.id}`, { token: viewer.token });\n expect(viewerDetail.status()).toBe(200);\n\n const noAccessList = await apiRequest(request, 'GET', '/api/webhooks', { token: noAccess.token });\n expect([401, 403]).toContain(noAccessList.status());\n const noAccessDetail = await apiRequest(request, 'GET', `/api/webhooks/${created.id}`, { token: noAccess.token });\n expect([401, 403]).toContain(noAccessDetail.status());\n\n // --- webhooks.manage gate (viewer lacks manage) ---\n const viewerCreate = await apiRequest(request, 'POST', '/api/webhooks', {\n token: viewer.token,\n data: {\n name: `Webhook RBAC Denied ${Date.now()}`,\n url: 'https://example.com/webhook-denied',\n subscribedEvents: ['catalog.product.created'],\n },\n });\n expect([401, 403]).toContain(viewerCreate.status());\n\n const viewerUpdate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token: viewer.token,\n data: { name: `Webhook RBAC Hijack ${Date.now()}` },\n });\n expect([401, 403]).toContain(viewerUpdate.status());\n\n const viewerDelete = await apiRequest(request, 'DELETE', `/api/webhooks/${created.id}`, { token: viewer.token });\n expect([401, 403]).toContain(viewerDelete.status());\n\n // --- webhooks.secrets gate (viewer lacks secrets) ---\n const viewerRotate = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/rotate-secret`, {\n token: viewer.token,\n });\n expect([401, 403]).toContain(viewerRotate.status());\n\n // --- webhooks.test gate (viewer lacks test) ---\n const viewerTest = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/test`, {\n token: viewer.token,\n data: { eventType: 'catalog.product.created' },\n });\n expect([401, 403]).toContain(viewerTest.status());\n\n // The webhook must still exist \u2014 none of the denied writes mutated it.\n const stillThere = await apiRequest(request, 'GET', `/api/webhooks/${created.id}`, { token: adminToken });\n expect(stillThere.status()).toBe(200);\n\n // --- Positive secrets + test paths (admin holds both features) ---\n const adminRotate = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/rotate-secret`, {\n token: adminToken,\n });\n expect(adminRotate.status()).toBe(200);\n const adminTest = await apiRequest(request, 'POST', `/api/webhooks/${created.id}/test`, {\n token: adminToken,\n data: { eventType: 'catalog.product.created' },\n });\n expect(adminTest.status()).toBe(200);\n } finally {\n await deleteWebhookIfExists(request, adminToken, webhookId);\n await cleanupWebhooksUser(request, adminToken, viewer);\n await cleanupWebhooksUser(request, adminToken, noAccess);\n }\n });\n});\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
2
2
  import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
3
3
  import {
4
4
  buildMockInboundUrl,
5
+ mockInboundAuthHeaders,
5
6
  createWebhookFixture,
6
7
  deleteWebhookIfExists,
7
8
  listWebhookDeliveries,
@@ -16,7 +17,7 @@ test.describe("TC-WEBHOOK-006: Webhook deactivation blocks delivery", () => {
16
17
  name: `Webhook Deactivation ${Date.now()}`,
17
18
  url: buildMockInboundUrl(),
18
19
  subscribedEvents: ["catalog.product.created"],
19
- customHeaders: { "x-mock-webhook-signature": "valid" }
20
+ customHeaders: mockInboundAuthHeaders()
20
21
  });
21
22
  webhookId = created.id;
22
23
  const activeDelivery = await sendTestDelivery(request, token, created.id, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/__integration__/TC-WEBHOOK-006.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n buildMockInboundUrl,\n createWebhookFixture,\n deleteWebhookIfExists,\n listWebhookDeliveries,\n sendTestDelivery,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-006: Webhook deactivation blocks delivery\n * Source: https://github.com/open-mercato/open-mercato/issues/2482\n *\n * The isActive flag must prevent delivery. The synchronous test route still records a\n * delivery attempt, but the delivery engine short-circuits inactive webhooks: the\n * delivery is marked `expired` with \"Webhook is inactive\" and no HTTP request is made\n * (responseStatus stays null). Re-activating restores successful delivery.\n */\ntest.describe('TC-WEBHOOK-006: Webhook deactivation blocks delivery', () => {\n test('should deliver while active, block while inactive, and resume after re-activation', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Deactivation ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: ['catalog.product.created'],\n customHeaders: { 'x-mock-webhook-signature': 'valid' },\n });\n webhookId = created.id;\n\n // 1) Active webhook delivers successfully.\n const activeDelivery = await sendTestDelivery(request, token, created.id, {\n eventType: 'catalog.product.created',\n });\n expect(activeDelivery.delivery.status).toBe('delivered');\n expect(activeDelivery.delivery.responseStatus).toBe(200);\n\n // 2) Deactivate.\n const deactivate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token,\n data: { isActive: false },\n });\n expect(deactivate.status()).toBe(200);\n\n // 3) Inactive webhook does not reach the endpoint \u2014 delivery is expired, no HTTP attempt.\n const blockedDelivery = await sendTestDelivery(request, token, created.id, {\n eventType: 'catalog.product.created',\n });\n expect(blockedDelivery.delivery.status).toBe('expired');\n expect(blockedDelivery.delivery.errorMessage).toBe('Webhook is inactive');\n expect(blockedDelivery.delivery.responseStatus).toBeNull();\n\n // The blocked attempt is visible in the delivery log with a non-delivered status.\n const expiredDeliveries = await listWebhookDeliveries(\n request,\n token,\n created.id,\n `/api/webhooks/deliveries?webhookId=${encodeURIComponent(created.id)}&status=expired`,\n );\n expect(expiredDeliveries.items.some((item) => item.id === blockedDelivery.delivery.id)).toBe(true);\n expect(expiredDeliveries.items.every((item) => item.status === 'expired')).toBe(true);\n\n // 4) Re-activate and confirm delivery resumes.\n const reactivate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token,\n data: { isActive: true },\n });\n expect(reactivate.status()).toBe(200);\n\n const resumedDelivery = await sendTestDelivery(request, token, created.id, {\n eventType: 'catalog.product.created',\n });\n expect(resumedDelivery.delivery.status).toBe('delivered');\n expect(resumedDelivery.delivery.responseStatus).toBe(200);\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n});\n"],
5
- "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWP,KAAK,SAAS,wDAAwD,MAAM;AAC1E,OAAK,qFAAqF,OAAO,EAAE,QAAQ,MAAM;AAC/G,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,QAAI,YAA2B;AAE/B,QAAI;AACF,YAAM,UAAU,MAAM,qBAAqB,SAAS,OAAO;AAAA,QACzD,MAAM,wBAAwB,KAAK,IAAI,CAAC;AAAA,QACxC,KAAK,oBAAoB;AAAA,QACzB,kBAAkB,CAAC,yBAAyB;AAAA,QAC5C,eAAe,EAAE,4BAA4B,QAAQ;AAAA,MACvD,CAAC;AACD,kBAAY,QAAQ;AAGpB,YAAM,iBAAiB,MAAM,iBAAiB,SAAS,OAAO,QAAQ,IAAI;AAAA,QACxE,WAAW;AAAA,MACb,CAAC;AACD,aAAO,eAAe,SAAS,MAAM,EAAE,KAAK,WAAW;AACvD,aAAO,eAAe,SAAS,cAAc,EAAE,KAAK,GAAG;AAGvD,YAAM,aAAa,MAAM,WAAW,SAAS,OAAO,iBAAiB,QAAQ,EAAE,IAAI;AAAA,QACjF;AAAA,QACA,MAAM,EAAE,UAAU,MAAM;AAAA,MAC1B,CAAC;AACD,aAAO,WAAW,OAAO,CAAC,EAAE,KAAK,GAAG;AAGpC,YAAM,kBAAkB,MAAM,iBAAiB,SAAS,OAAO,QAAQ,IAAI;AAAA,QACzE,WAAW;AAAA,MACb,CAAC;AACD,aAAO,gBAAgB,SAAS,MAAM,EAAE,KAAK,SAAS;AACtD,aAAO,gBAAgB,SAAS,YAAY,EAAE,KAAK,qBAAqB;AACxE,aAAO,gBAAgB,SAAS,cAAc,EAAE,SAAS;AAGzD,YAAM,oBAAoB,MAAM;AAAA,QAC9B;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,sCAAsC,mBAAmB,QAAQ,EAAE,CAAC;AAAA,MACtE;AACA,aAAO,kBAAkB,MAAM,KAAK,CAAC,SAAS,KAAK,OAAO,gBAAgB,SAAS,EAAE,CAAC,EAAE,KAAK,IAAI;AACjG,aAAO,kBAAkB,MAAM,MAAM,CAAC,SAAS,KAAK,WAAW,SAAS,CAAC,EAAE,KAAK,IAAI;AAGpF,YAAM,aAAa,MAAM,WAAW,SAAS,OAAO,iBAAiB,QAAQ,EAAE,IAAI;AAAA,QACjF;AAAA,QACA,MAAM,EAAE,UAAU,KAAK;AAAA,MACzB,CAAC;AACD,aAAO,WAAW,OAAO,CAAC,EAAE,KAAK,GAAG;AAEpC,YAAM,kBAAkB,MAAM,iBAAiB,SAAS,OAAO,QAAQ,IAAI;AAAA,QACzE,WAAW;AAAA,MACb,CAAC;AACD,aAAO,gBAAgB,SAAS,MAAM,EAAE,KAAK,WAAW;AACxD,aAAO,gBAAgB,SAAS,cAAc,EAAE,KAAK,GAAG;AAAA,IAC1D,UAAE;AACA,YAAM,sBAAsB,SAAS,OAAO,SAAS;AAAA,IACvD;AAAA,EACF,CAAC;AACH,CAAC;",
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n buildMockInboundUrl,\n mockInboundAuthHeaders,\n createWebhookFixture,\n deleteWebhookIfExists,\n listWebhookDeliveries,\n sendTestDelivery,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-006: Webhook deactivation blocks delivery\n * Source: https://github.com/open-mercato/open-mercato/issues/2482\n *\n * The isActive flag must prevent delivery. The synchronous test route still records a\n * delivery attempt, but the delivery engine short-circuits inactive webhooks: the\n * delivery is marked `expired` with \"Webhook is inactive\" and no HTTP request is made\n * (responseStatus stays null). Re-activating restores successful delivery.\n */\ntest.describe('TC-WEBHOOK-006: Webhook deactivation blocks delivery', () => {\n test('should deliver while active, block while inactive, and resume after re-activation', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Deactivation ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: ['catalog.product.created'],\n customHeaders: mockInboundAuthHeaders(),\n });\n webhookId = created.id;\n\n // 1) Active webhook delivers successfully.\n const activeDelivery = await sendTestDelivery(request, token, created.id, {\n eventType: 'catalog.product.created',\n });\n expect(activeDelivery.delivery.status).toBe('delivered');\n expect(activeDelivery.delivery.responseStatus).toBe(200);\n\n // 2) Deactivate.\n const deactivate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token,\n data: { isActive: false },\n });\n expect(deactivate.status()).toBe(200);\n\n // 3) Inactive webhook does not reach the endpoint \u2014 delivery is expired, no HTTP attempt.\n const blockedDelivery = await sendTestDelivery(request, token, created.id, {\n eventType: 'catalog.product.created',\n });\n expect(blockedDelivery.delivery.status).toBe('expired');\n expect(blockedDelivery.delivery.errorMessage).toBe('Webhook is inactive');\n expect(blockedDelivery.delivery.responseStatus).toBeNull();\n\n // The blocked attempt is visible in the delivery log with a non-delivered status.\n const expiredDeliveries = await listWebhookDeliveries(\n request,\n token,\n created.id,\n `/api/webhooks/deliveries?webhookId=${encodeURIComponent(created.id)}&status=expired`,\n );\n expect(expiredDeliveries.items.some((item) => item.id === blockedDelivery.delivery.id)).toBe(true);\n expect(expiredDeliveries.items.every((item) => item.status === 'expired')).toBe(true);\n\n // 4) Re-activate and confirm delivery resumes.\n const reactivate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token,\n data: { isActive: true },\n });\n expect(reactivate.status()).toBe(200);\n\n const resumedDelivery = await sendTestDelivery(request, token, created.id, {\n eventType: 'catalog.product.created',\n });\n expect(resumedDelivery.delivery.status).toBe('delivered');\n expect(resumedDelivery.delivery.responseStatus).toBe(200);\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n});\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWP,KAAK,SAAS,wDAAwD,MAAM;AAC1E,OAAK,qFAAqF,OAAO,EAAE,QAAQ,MAAM;AAC/G,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,QAAI,YAA2B;AAE/B,QAAI;AACF,YAAM,UAAU,MAAM,qBAAqB,SAAS,OAAO;AAAA,QACzD,MAAM,wBAAwB,KAAK,IAAI,CAAC;AAAA,QACxC,KAAK,oBAAoB;AAAA,QACzB,kBAAkB,CAAC,yBAAyB;AAAA,QAC5C,eAAe,uBAAuB;AAAA,MACxC,CAAC;AACD,kBAAY,QAAQ;AAGpB,YAAM,iBAAiB,MAAM,iBAAiB,SAAS,OAAO,QAAQ,IAAI;AAAA,QACxE,WAAW;AAAA,MACb,CAAC;AACD,aAAO,eAAe,SAAS,MAAM,EAAE,KAAK,WAAW;AACvD,aAAO,eAAe,SAAS,cAAc,EAAE,KAAK,GAAG;AAGvD,YAAM,aAAa,MAAM,WAAW,SAAS,OAAO,iBAAiB,QAAQ,EAAE,IAAI;AAAA,QACjF;AAAA,QACA,MAAM,EAAE,UAAU,MAAM;AAAA,MAC1B,CAAC;AACD,aAAO,WAAW,OAAO,CAAC,EAAE,KAAK,GAAG;AAGpC,YAAM,kBAAkB,MAAM,iBAAiB,SAAS,OAAO,QAAQ,IAAI;AAAA,QACzE,WAAW;AAAA,MACb,CAAC;AACD,aAAO,gBAAgB,SAAS,MAAM,EAAE,KAAK,SAAS;AACtD,aAAO,gBAAgB,SAAS,YAAY,EAAE,KAAK,qBAAqB;AACxE,aAAO,gBAAgB,SAAS,cAAc,EAAE,SAAS;AAGzD,YAAM,oBAAoB,MAAM;AAAA,QAC9B;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,sCAAsC,mBAAmB,QAAQ,EAAE,CAAC;AAAA,MACtE;AACA,aAAO,kBAAkB,MAAM,KAAK,CAAC,SAAS,KAAK,OAAO,gBAAgB,SAAS,EAAE,CAAC,EAAE,KAAK,IAAI;AACjG,aAAO,kBAAkB,MAAM,MAAM,CAAC,SAAS,KAAK,WAAW,SAAS,CAAC,EAAE,KAAK,IAAI;AAGpF,YAAM,aAAa,MAAM,WAAW,SAAS,OAAO,iBAAiB,QAAQ,EAAE,IAAI;AAAA,QACjF;AAAA,QACA,MAAM,EAAE,UAAU,KAAK;AAAA,MACzB,CAAC;AACD,aAAO,WAAW,OAAO,CAAC,EAAE,KAAK,GAAG;AAEpC,YAAM,kBAAkB,MAAM,iBAAiB,SAAS,OAAO,QAAQ,IAAI;AAAA,QACzE,WAAW;AAAA,MACb,CAAC;AACD,aAAO,gBAAgB,SAAS,MAAM,EAAE,KAAK,WAAW;AACxD,aAAO,gBAAgB,SAAS,cAAc,EAAE,KAAK,GAAG;AAAA,IAC1D,UAAE;AACA,YAAM,sBAAsB,SAAS,OAAO,SAAS;AAAA,IACvD;AAAA,EACF,CAAC;AACH,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,8 @@ import { expect, test } from "@playwright/test";
2
2
  import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
3
3
  import {
4
4
  buildMockInboundUrl,
5
+ mockInboundAuthHeaders,
6
+ mockInboundInvalidAuthHeaders,
5
7
  createWebhookFixture,
6
8
  deleteWebhookIfExists,
7
9
  listWebhookDeliveries,
@@ -22,7 +24,7 @@ test.describe("TC-WEBHOOK-008: Delivery list filtering by status and event type"
22
24
  name: `Webhook Filter ${Date.now()}`,
23
25
  url: buildMockInboundUrl(),
24
26
  subscribedEvents: [EVENT_CREATED, EVENT_UPDATED],
25
- customHeaders: { "x-mock-webhook-signature": "valid" }
27
+ customHeaders: mockInboundAuthHeaders()
26
28
  });
27
29
  webhookId = created.id;
28
30
  const deliveredCreated = await sendTestDelivery(request, token, created.id, { eventType: EVENT_CREATED });
@@ -31,7 +33,7 @@ test.describe("TC-WEBHOOK-008: Delivery list filtering by status and event type"
31
33
  expect(deliveredUpdated.delivery.status).toBe("delivered");
32
34
  const invalidate = await apiRequest(request, "PUT", `/api/webhooks/${created.id}`, {
33
35
  token,
34
- data: { customHeaders: { "x-mock-webhook-signature": "invalid" } }
36
+ data: { customHeaders: mockInboundInvalidAuthHeaders() }
35
37
  });
36
38
  expect(invalidate.status()).toBe(200);
37
39
  const expiredCreated = await sendTestDelivery(request, token, created.id, { eventType: EVENT_CREATED });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/__integration__/TC-WEBHOOK-008.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n buildMockInboundUrl,\n createWebhookFixture,\n deleteWebhookIfExists,\n listWebhookDeliveries,\n sendTestDelivery,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-008: Delivery list filtering by status and event type\n * Source: https://github.com/open-mercato/open-mercato/issues/2482\n *\n * GET /api/webhooks/deliveries supports webhookId, status, and eventType filters.\n * Seeds three deliveries on one webhook \u2014 delivered(created), delivered(updated),\n * expired(created) \u2014 then verifies each filter and their intersection.\n */\nconst EVENT_CREATED = 'catalog.product.created';\nconst EVENT_UPDATED = 'catalog.product.updated';\n\nfunction deliveriesPath(webhookId: string, extra: Record<string, string> = {}): string {\n const params = new URLSearchParams({ webhookId, ...extra });\n return `/api/webhooks/deliveries?${params.toString()}`;\n}\n\ntest.describe('TC-WEBHOOK-008: Delivery list filtering by status and event type', () => {\n test('should filter delivery logs by status, event type, and their combination', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Filter ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: [EVENT_CREATED, EVENT_UPDATED],\n customHeaders: { 'x-mock-webhook-signature': 'valid' },\n });\n webhookId = created.id;\n\n const deliveredCreated = await sendTestDelivery(request, token, created.id, { eventType: EVENT_CREATED });\n expect(deliveredCreated.delivery.status).toBe('delivered');\n const deliveredUpdated = await sendTestDelivery(request, token, created.id, { eventType: EVENT_UPDATED });\n expect(deliveredUpdated.delivery.status).toBe('delivered');\n\n // Flip the mock signature to invalid so the next attempt fails terminally (expired).\n const invalidate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token,\n data: { customHeaders: { 'x-mock-webhook-signature': 'invalid' } },\n });\n expect(invalidate.status()).toBe(200);\n\n const expiredCreated = await sendTestDelivery(request, token, created.id, { eventType: EVENT_CREATED });\n expect(expiredCreated.delivery.status).toBe('expired');\n\n const d1 = deliveredCreated.delivery.id;\n const d2 = deliveredUpdated.delivery.id;\n const d3 = expiredCreated.delivery.id;\n\n // Unfiltered (by webhook): all three present.\n const all = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id));\n expect(all.total).toBeGreaterThanOrEqual(3);\n for (const id of [d1, d2, d3]) {\n expect(all.items.some((item) => item.id === id)).toBe(true);\n }\n\n // status=delivered \u2192 d1, d2 only.\n const delivered = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id, { status: 'delivered' }));\n expect(delivered.items.every((item) => item.status === 'delivered')).toBe(true);\n expect(delivered.items.some((item) => item.id === d1)).toBe(true);\n expect(delivered.items.some((item) => item.id === d2)).toBe(true);\n expect(delivered.items.some((item) => item.id === d3)).toBe(false);\n\n // status=expired \u2192 d3 only.\n const expired = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id, { status: 'expired' }));\n expect(expired.items.every((item) => item.status === 'expired')).toBe(true);\n expect(expired.items.some((item) => item.id === d3)).toBe(true);\n expect(expired.items.some((item) => item.id === d1)).toBe(false);\n\n // eventType=created \u2192 d1, d3 only.\n const createdEvents = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id, { eventType: EVENT_CREATED }));\n expect(createdEvents.items.every((item) => item.eventType === EVENT_CREATED)).toBe(true);\n expect(createdEvents.items.some((item) => item.id === d1)).toBe(true);\n expect(createdEvents.items.some((item) => item.id === d3)).toBe(true);\n expect(createdEvents.items.some((item) => item.id === d2)).toBe(false);\n\n // Combined status=delivered & eventType=created \u2192 d1 only.\n const combined = await listWebhookDeliveries(\n request,\n token,\n created.id,\n deliveriesPath(created.id, { status: 'delivered', eventType: EVENT_CREATED }),\n );\n expect(combined.items.every((item) => item.status === 'delivered' && item.eventType === EVENT_CREATED)).toBe(true);\n expect(combined.items.some((item) => item.id === d1)).toBe(true);\n expect(combined.items.some((item) => item.id === d2)).toBe(false);\n expect(combined.items.some((item) => item.id === d3)).toBe(false);\n\n // Non-matching filter \u2192 empty result set with pagination metadata.\n const empty = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id, { status: 'pending' }));\n expect(empty.items).toEqual([]);\n expect(empty.total).toBe(0);\n expect(empty.page).toBe(1);\n expect(empty.totalPages).toBe(0);\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n});\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n buildMockInboundUrl,\n mockInboundAuthHeaders,\n mockInboundInvalidAuthHeaders,\n createWebhookFixture,\n deleteWebhookIfExists,\n listWebhookDeliveries,\n sendTestDelivery,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-008: Delivery list filtering by status and event type\n * Source: https://github.com/open-mercato/open-mercato/issues/2482\n *\n * GET /api/webhooks/deliveries supports webhookId, status, and eventType filters.\n * Seeds three deliveries on one webhook \u2014 delivered(created), delivered(updated),\n * expired(created) \u2014 then verifies each filter and their intersection.\n */\nconst EVENT_CREATED = 'catalog.product.created';\nconst EVENT_UPDATED = 'catalog.product.updated';\n\nfunction deliveriesPath(webhookId: string, extra: Record<string, string> = {}): string {\n const params = new URLSearchParams({ webhookId, ...extra });\n return `/api/webhooks/deliveries?${params.toString()}`;\n}\n\ntest.describe('TC-WEBHOOK-008: Delivery list filtering by status and event type', () => {\n test('should filter delivery logs by status, event type, and their combination', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Filter ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: [EVENT_CREATED, EVENT_UPDATED],\n customHeaders: mockInboundAuthHeaders(),\n });\n webhookId = created.id;\n\n const deliveredCreated = await sendTestDelivery(request, token, created.id, { eventType: EVENT_CREATED });\n expect(deliveredCreated.delivery.status).toBe('delivered');\n const deliveredUpdated = await sendTestDelivery(request, token, created.id, { eventType: EVENT_UPDATED });\n expect(deliveredUpdated.delivery.status).toBe('delivered');\n\n // Flip the mock signature to invalid so the next attempt fails terminally (expired).\n const invalidate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {\n token,\n data: { customHeaders: mockInboundInvalidAuthHeaders() },\n });\n expect(invalidate.status()).toBe(200);\n\n const expiredCreated = await sendTestDelivery(request, token, created.id, { eventType: EVENT_CREATED });\n expect(expiredCreated.delivery.status).toBe('expired');\n\n const d1 = deliveredCreated.delivery.id;\n const d2 = deliveredUpdated.delivery.id;\n const d3 = expiredCreated.delivery.id;\n\n // Unfiltered (by webhook): all three present.\n const all = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id));\n expect(all.total).toBeGreaterThanOrEqual(3);\n for (const id of [d1, d2, d3]) {\n expect(all.items.some((item) => item.id === id)).toBe(true);\n }\n\n // status=delivered \u2192 d1, d2 only.\n const delivered = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id, { status: 'delivered' }));\n expect(delivered.items.every((item) => item.status === 'delivered')).toBe(true);\n expect(delivered.items.some((item) => item.id === d1)).toBe(true);\n expect(delivered.items.some((item) => item.id === d2)).toBe(true);\n expect(delivered.items.some((item) => item.id === d3)).toBe(false);\n\n // status=expired \u2192 d3 only.\n const expired = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id, { status: 'expired' }));\n expect(expired.items.every((item) => item.status === 'expired')).toBe(true);\n expect(expired.items.some((item) => item.id === d3)).toBe(true);\n expect(expired.items.some((item) => item.id === d1)).toBe(false);\n\n // eventType=created \u2192 d1, d3 only.\n const createdEvents = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id, { eventType: EVENT_CREATED }));\n expect(createdEvents.items.every((item) => item.eventType === EVENT_CREATED)).toBe(true);\n expect(createdEvents.items.some((item) => item.id === d1)).toBe(true);\n expect(createdEvents.items.some((item) => item.id === d3)).toBe(true);\n expect(createdEvents.items.some((item) => item.id === d2)).toBe(false);\n\n // Combined status=delivered & eventType=created \u2192 d1 only.\n const combined = await listWebhookDeliveries(\n request,\n token,\n created.id,\n deliveriesPath(created.id, { status: 'delivered', eventType: EVENT_CREATED }),\n );\n expect(combined.items.every((item) => item.status === 'delivered' && item.eventType === EVENT_CREATED)).toBe(true);\n expect(combined.items.some((item) => item.id === d1)).toBe(true);\n expect(combined.items.some((item) => item.id === d2)).toBe(false);\n expect(combined.items.some((item) => item.id === d3)).toBe(false);\n\n // Non-matching filter \u2192 empty result set with pagination metadata.\n const empty = await listWebhookDeliveries(request, token, created.id, deliveriesPath(created.id, { status: 'pending' }));\n expect(empty.items).toEqual([]);\n expect(empty.total).toBe(0);\n expect(empty.page).toBe(1);\n expect(empty.totalPages).toBe(0);\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n});\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
2
2
  import { getAuthToken } from "@open-mercato/core/helpers/integration/api";
3
3
  import {
4
4
  buildMockInboundUrl,
5
+ mockInboundAuthHeaders,
5
6
  createWebhookFixture,
6
7
  deleteWebhookIfExists,
7
8
  getDeliveryDetail,
@@ -17,7 +18,7 @@ test.describe("TC-WEBHOOK-010: Delivery detail full payload and response body",
17
18
  name: `Webhook Detail ${Date.now()}`,
18
19
  url: buildMockInboundUrl(),
19
20
  subscribedEvents: ["catalog.product.created"],
20
- customHeaders: { "x-mock-webhook-signature": "valid" }
21
+ customHeaders: mockInboundAuthHeaders()
21
22
  });
22
23
  webhookId = created.id;
23
24
  const eventData = { sku: "TEST-123", nested: { value: 42 } };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/__integration__/TC-WEBHOOK-010.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n buildMockInboundUrl,\n createWebhookFixture,\n deleteWebhookIfExists,\n getDeliveryDetail,\n sendTestDelivery,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-010: Delivery detail response includes full payload and response body\n * Source: https://github.com/open-mercato/open-mercato/issues/2482\n *\n * The delivery detail endpoint must expose the complete sent payload and the full\n * response captured from the endpoint (status, body, headers) plus attempt/timing\n * metadata, so admins can debug a delivery end-to-end.\n */\nconst EVENT_TYPE = 'webhooks.test.detail';\n\ntest.describe('TC-WEBHOOK-010: Delivery detail full payload and response body', () => {\n test('should expose the complete payload and response details for a delivery', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Detail ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: ['catalog.product.created'],\n customHeaders: { 'x-mock-webhook-signature': 'valid' },\n });\n webhookId = created.id;\n\n const eventData = { sku: 'TEST-123', nested: { value: 42 } };\n\n const testResult = await sendTestDelivery(request, token, created.id, {\n eventType: EVENT_TYPE,\n payload: eventData,\n });\n expect(testResult.delivery.status).toBe('delivered');\n const deliveryId = testResult.delivery.id;\n\n const detail = await getDeliveryDetail(request, token, deliveryId);\n\n // Identity + routing\n expect(detail.id).toBe(deliveryId);\n expect(detail.webhookId).toBe(created.id);\n expect(detail.eventType).toBe(EVENT_TYPE);\n expect(detail.targetUrl).toBe(created.url);\n expect(typeof detail.messageId).toBe('string');\n expect(detail.messageId.length).toBeGreaterThan(0);\n\n // Outcome\n expect(detail.status).toBe('delivered');\n expect(detail.responseStatus).toBe(200);\n expect(detail.errorMessage).toBeNull();\n\n // The delivery body wraps the event data in the Standard-Webhooks envelope\n // { type, timestamp, data }; the sent payload is preserved under `data`.\n expect(detail.payload).toMatchObject({\n type: EVENT_TYPE,\n data: { sku: 'TEST-123', nested: { value: 42 } },\n });\n const envelopeTimestamp = (detail.payload as { timestamp?: unknown }).timestamp;\n expect(typeof envelopeTimestamp).toBe('string');\n expect(Number.isNaN(Date.parse(envelopeTimestamp as string))).toBe(false);\n\n // Full captured response (the mock receiver replies { ok: true } as JSON)\n expect(detail.responseBody).toBe(JSON.stringify({ ok: true }));\n expect(detail.responseHeaders).not.toBeNull();\n expect(detail.responseHeaders?.['content-type']).toContain('application/json');\n\n // Attempt + timing metadata\n expect(detail.attemptNumber).toBeGreaterThanOrEqual(1);\n expect(detail.maxAttempts).toBeGreaterThanOrEqual(1);\n expect(typeof detail.durationMs).toBe('number');\n expect(detail.durationMs as number).toBeGreaterThanOrEqual(0);\n expect(detail.nextRetryAt).toBeNull();\n\n for (const timestamp of [detail.createdAt, detail.enqueuedAt, detail.lastAttemptAt, detail.deliveredAt, detail.updatedAt]) {\n expect(typeof timestamp).toBe('string');\n expect(Number.isNaN(Date.parse(timestamp as string))).toBe(false);\n }\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n});\n"],
5
- "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAUP,MAAM,aAAa;AAEnB,KAAK,SAAS,kEAAkE,MAAM;AACpF,OAAK,0EAA0E,OAAO,EAAE,QAAQ,MAAM;AACpG,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,QAAI,YAA2B;AAE/B,QAAI;AACF,YAAM,UAAU,MAAM,qBAAqB,SAAS,OAAO;AAAA,QACzD,MAAM,kBAAkB,KAAK,IAAI,CAAC;AAAA,QAClC,KAAK,oBAAoB;AAAA,QACzB,kBAAkB,CAAC,yBAAyB;AAAA,QAC5C,eAAe,EAAE,4BAA4B,QAAQ;AAAA,MACvD,CAAC;AACD,kBAAY,QAAQ;AAEpB,YAAM,YAAY,EAAE,KAAK,YAAY,QAAQ,EAAE,OAAO,GAAG,EAAE;AAE3D,YAAM,aAAa,MAAM,iBAAiB,SAAS,OAAO,QAAQ,IAAI;AAAA,QACpE,WAAW;AAAA,QACX,SAAS;AAAA,MACX,CAAC;AACD,aAAO,WAAW,SAAS,MAAM,EAAE,KAAK,WAAW;AACnD,YAAM,aAAa,WAAW,SAAS;AAEvC,YAAM,SAAS,MAAM,kBAAkB,SAAS,OAAO,UAAU;AAGjE,aAAO,OAAO,EAAE,EAAE,KAAK,UAAU;AACjC,aAAO,OAAO,SAAS,EAAE,KAAK,QAAQ,EAAE;AACxC,aAAO,OAAO,SAAS,EAAE,KAAK,UAAU;AACxC,aAAO,OAAO,SAAS,EAAE,KAAK,QAAQ,GAAG;AACzC,aAAO,OAAO,OAAO,SAAS,EAAE,KAAK,QAAQ;AAC7C,aAAO,OAAO,UAAU,MAAM,EAAE,gBAAgB,CAAC;AAGjD,aAAO,OAAO,MAAM,EAAE,KAAK,WAAW;AACtC,aAAO,OAAO,cAAc,EAAE,KAAK,GAAG;AACtC,aAAO,OAAO,YAAY,EAAE,SAAS;AAIrC,aAAO,OAAO,OAAO,EAAE,cAAc;AAAA,QACnC,MAAM;AAAA,QACN,MAAM,EAAE,KAAK,YAAY,QAAQ,EAAE,OAAO,GAAG,EAAE;AAAA,MACjD,CAAC;AACD,YAAM,oBAAqB,OAAO,QAAoC;AACtE,aAAO,OAAO,iBAAiB,EAAE,KAAK,QAAQ;AAC9C,aAAO,OAAO,MAAM,KAAK,MAAM,iBAA2B,CAAC,CAAC,EAAE,KAAK,KAAK;AAGxE,aAAO,OAAO,YAAY,EAAE,KAAK,KAAK,UAAU,EAAE,IAAI,KAAK,CAAC,CAAC;AAC7D,aAAO,OAAO,eAAe,EAAE,IAAI,SAAS;AAC5C,aAAO,OAAO,kBAAkB,cAAc,CAAC,EAAE,UAAU,kBAAkB;AAG7E,aAAO,OAAO,aAAa,EAAE,uBAAuB,CAAC;AACrD,aAAO,OAAO,WAAW,EAAE,uBAAuB,CAAC;AACnD,aAAO,OAAO,OAAO,UAAU,EAAE,KAAK,QAAQ;AAC9C,aAAO,OAAO,UAAoB,EAAE,uBAAuB,CAAC;AAC5D,aAAO,OAAO,WAAW,EAAE,SAAS;AAEpC,iBAAW,aAAa,CAAC,OAAO,WAAW,OAAO,YAAY,OAAO,eAAe,OAAO,aAAa,OAAO,SAAS,GAAG;AACzH,eAAO,OAAO,SAAS,EAAE,KAAK,QAAQ;AACtC,eAAO,OAAO,MAAM,KAAK,MAAM,SAAmB,CAAC,CAAC,EAAE,KAAK,KAAK;AAAA,MAClE;AAAA,IACF,UAAE;AACA,YAAM,sBAAsB,SAAS,OAAO,SAAS;AAAA,IACvD;AAAA,EACF,CAAC;AACH,CAAC;",
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n buildMockInboundUrl,\n mockInboundAuthHeaders,\n createWebhookFixture,\n deleteWebhookIfExists,\n getDeliveryDetail,\n sendTestDelivery,\n} from './helpers/fixtures';\n\n/**\n * TC-WEBHOOK-010: Delivery detail response includes full payload and response body\n * Source: https://github.com/open-mercato/open-mercato/issues/2482\n *\n * The delivery detail endpoint must expose the complete sent payload and the full\n * response captured from the endpoint (status, body, headers) plus attempt/timing\n * metadata, so admins can debug a delivery end-to-end.\n */\nconst EVENT_TYPE = 'webhooks.test.detail';\n\ntest.describe('TC-WEBHOOK-010: Delivery detail full payload and response body', () => {\n test('should expose the complete payload and response details for a delivery', async ({ request }) => {\n const token = await getAuthToken(request);\n let webhookId: string | null = null;\n\n try {\n const created = await createWebhookFixture(request, token, {\n name: `Webhook Detail ${Date.now()}`,\n url: buildMockInboundUrl(),\n subscribedEvents: ['catalog.product.created'],\n customHeaders: mockInboundAuthHeaders(),\n });\n webhookId = created.id;\n\n const eventData = { sku: 'TEST-123', nested: { value: 42 } };\n\n const testResult = await sendTestDelivery(request, token, created.id, {\n eventType: EVENT_TYPE,\n payload: eventData,\n });\n expect(testResult.delivery.status).toBe('delivered');\n const deliveryId = testResult.delivery.id;\n\n const detail = await getDeliveryDetail(request, token, deliveryId);\n\n // Identity + routing\n expect(detail.id).toBe(deliveryId);\n expect(detail.webhookId).toBe(created.id);\n expect(detail.eventType).toBe(EVENT_TYPE);\n expect(detail.targetUrl).toBe(created.url);\n expect(typeof detail.messageId).toBe('string');\n expect(detail.messageId.length).toBeGreaterThan(0);\n\n // Outcome\n expect(detail.status).toBe('delivered');\n expect(detail.responseStatus).toBe(200);\n expect(detail.errorMessage).toBeNull();\n\n // The delivery body wraps the event data in the Standard-Webhooks envelope\n // { type, timestamp, data }; the sent payload is preserved under `data`.\n expect(detail.payload).toMatchObject({\n type: EVENT_TYPE,\n data: { sku: 'TEST-123', nested: { value: 42 } },\n });\n const envelopeTimestamp = (detail.payload as { timestamp?: unknown }).timestamp;\n expect(typeof envelopeTimestamp).toBe('string');\n expect(Number.isNaN(Date.parse(envelopeTimestamp as string))).toBe(false);\n\n // Full captured response (the mock receiver replies { ok: true } as JSON)\n expect(detail.responseBody).toBe(JSON.stringify({ ok: true }));\n expect(detail.responseHeaders).not.toBeNull();\n expect(detail.responseHeaders?.['content-type']).toContain('application/json');\n\n // Attempt + timing metadata\n expect(detail.attemptNumber).toBeGreaterThanOrEqual(1);\n expect(detail.maxAttempts).toBeGreaterThanOrEqual(1);\n expect(typeof detail.durationMs).toBe('number');\n expect(detail.durationMs as number).toBeGreaterThanOrEqual(0);\n expect(detail.nextRetryAt).toBeNull();\n\n for (const timestamp of [detail.createdAt, detail.enqueuedAt, detail.lastAttemptAt, detail.deliveredAt, detail.updatedAt]) {\n expect(typeof timestamp).toBe('string');\n expect(Number.isNaN(Date.parse(timestamp as string))).toBe(false);\n }\n } finally {\n await deleteWebhookIfExists(request, token, webhookId);\n }\n });\n});\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAUP,MAAM,aAAa;AAEnB,KAAK,SAAS,kEAAkE,MAAM;AACpF,OAAK,0EAA0E,OAAO,EAAE,QAAQ,MAAM;AACpG,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,QAAI,YAA2B;AAE/B,QAAI;AACF,YAAM,UAAU,MAAM,qBAAqB,SAAS,OAAO;AAAA,QACzD,MAAM,kBAAkB,KAAK,IAAI,CAAC;AAAA,QAClC,KAAK,oBAAoB;AAAA,QACzB,kBAAkB,CAAC,yBAAyB;AAAA,QAC5C,eAAe,uBAAuB;AAAA,MACxC,CAAC;AACD,kBAAY,QAAQ;AAEpB,YAAM,YAAY,EAAE,KAAK,YAAY,QAAQ,EAAE,OAAO,GAAG,EAAE;AAE3D,YAAM,aAAa,MAAM,iBAAiB,SAAS,OAAO,QAAQ,IAAI;AAAA,QACpE,WAAW;AAAA,QACX,SAAS;AAAA,MACX,CAAC;AACD,aAAO,WAAW,SAAS,MAAM,EAAE,KAAK,WAAW;AACnD,YAAM,aAAa,WAAW,SAAS;AAEvC,YAAM,SAAS,MAAM,kBAAkB,SAAS,OAAO,UAAU;AAGjE,aAAO,OAAO,EAAE,EAAE,KAAK,UAAU;AACjC,aAAO,OAAO,SAAS,EAAE,KAAK,QAAQ,EAAE;AACxC,aAAO,OAAO,SAAS,EAAE,KAAK,UAAU;AACxC,aAAO,OAAO,SAAS,EAAE,KAAK,QAAQ,GAAG;AACzC,aAAO,OAAO,OAAO,SAAS,EAAE,KAAK,QAAQ;AAC7C,aAAO,OAAO,UAAU,MAAM,EAAE,gBAAgB,CAAC;AAGjD,aAAO,OAAO,MAAM,EAAE,KAAK,WAAW;AACtC,aAAO,OAAO,cAAc,EAAE,KAAK,GAAG;AACtC,aAAO,OAAO,YAAY,EAAE,SAAS;AAIrC,aAAO,OAAO,OAAO,EAAE,cAAc;AAAA,QACnC,MAAM;AAAA,QACN,MAAM,EAAE,KAAK,YAAY,QAAQ,EAAE,OAAO,GAAG,EAAE;AAAA,MACjD,CAAC;AACD,YAAM,oBAAqB,OAAO,QAAoC;AACtE,aAAO,OAAO,iBAAiB,EAAE,KAAK,QAAQ;AAC9C,aAAO,OAAO,MAAM,KAAK,MAAM,iBAA2B,CAAC,CAAC,EAAE,KAAK,KAAK;AAGxE,aAAO,OAAO,YAAY,EAAE,KAAK,KAAK,UAAU,EAAE,IAAI,KAAK,CAAC,CAAC;AAC7D,aAAO,OAAO,eAAe,EAAE,IAAI,SAAS;AAC5C,aAAO,OAAO,kBAAkB,cAAc,CAAC,EAAE,UAAU,kBAAkB;AAG7E,aAAO,OAAO,aAAa,EAAE,uBAAuB,CAAC;AACrD,aAAO,OAAO,WAAW,EAAE,uBAAuB,CAAC;AACnD,aAAO,OAAO,OAAO,UAAU,EAAE,KAAK,QAAQ;AAC9C,aAAO,OAAO,UAAoB,EAAE,uBAAuB,CAAC;AAC5D,aAAO,OAAO,WAAW,EAAE,SAAS;AAEpC,iBAAW,aAAa,CAAC,OAAO,WAAW,OAAO,YAAY,OAAO,eAAe,OAAO,aAAa,OAAO,SAAS,GAAG;AACzH,eAAO,OAAO,SAAS,EAAE,KAAK,QAAQ;AACtC,eAAO,OAAO,MAAM,KAAK,MAAM,SAAmB,CAAC,CAAC,EAAE,KAAK,KAAK;AAAA,MAClE;AAAA,IACF,UAAE;AACA,YAAM,sBAAsB,SAAS,OAAO,SAAS;AAAA,IACvD;AAAA,EACF,CAAC;AACH,CAAC;",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,14 @@ const BASE_URL = process.env.BASE_URL?.trim() || "http://localhost:3000";
7
7
  function buildMockInboundUrl(endpointId = "mock_inbound") {
8
8
  return `${BASE_URL}/api/webhooks/inbound/${endpointId}`;
9
9
  }
10
+ const MOCK_INBOUND_TOKEN_HEADER = "x-mock-webhook-token";
11
+ const MOCK_INBOUND_WEBHOOK_SECRET = process.env.MOCK_INBOUND_WEBHOOK_SECRET?.trim() || "open-mercato-mock-dev-inbound-webhook-secret";
12
+ function mockInboundAuthHeaders() {
13
+ return { [MOCK_INBOUND_TOKEN_HEADER]: MOCK_INBOUND_WEBHOOK_SECRET };
14
+ }
15
+ function mockInboundInvalidAuthHeaders() {
16
+ return { [MOCK_INBOUND_TOKEN_HEADER]: "not-the-mock-inbound-secret" };
17
+ }
10
18
  async function createWebhookFixture(request, token, overrides = {}) {
11
19
  const response = await apiRequest(request, "POST", "/api/webhooks", {
12
20
  token,
@@ -142,6 +150,8 @@ async function cleanupWebhooksUser(request, adminToken, user) {
142
150
  await deleteUserIfExists(request, adminToken, user.userId).catch(() => void 0);
143
151
  }
144
152
  export {
153
+ MOCK_INBOUND_TOKEN_HEADER,
154
+ MOCK_INBOUND_WEBHOOK_SECRET,
145
155
  buildMockInboundUrl,
146
156
  cleanupWebhooksUser,
147
157
  createWebhookFixture,
@@ -151,6 +161,8 @@ export {
151
161
  listWebhookDeliveries,
152
162
  listWebhookEvents,
153
163
  listWebhooks,
164
+ mockInboundAuthHeaders,
165
+ mockInboundInvalidAuthHeaders,
154
166
  provisionWebhooksUser,
155
167
  rotateWebhookSecret,
156
168
  sendTestDelivery,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/webhooks/__integration__/helpers/fixtures.ts"],
4
- "sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { expectId, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport { createUserFixture, deleteUserIfExists } from '@open-mercato/core/helpers/integration/authFixtures';\nimport { deleteUserAclInDb, setUserAclInDb } from '@open-mercato/core/helpers/integration/dbFixtures';\n\nconst BASE_URL = process.env.BASE_URL?.trim() || 'http://localhost:3000';\n\nexport type WebhookCreateResponse = {\n id: string;\n name: string;\n url: string;\n secret: string;\n subscribedEvents: string[];\n isActive: boolean;\n};\n\nexport type WebhookDetailResponse = {\n id: string;\n name: string;\n description: string | null;\n url: string;\n subscribedEvents: string[];\n httpMethod: 'POST' | 'PUT' | 'PATCH';\n isActive: boolean;\n deliveryStrategy: string;\n maxRetries: number;\n consecutiveFailures: number;\n lastSuccessAt: string | null;\n lastFailureAt: string | null;\n createdAt: string;\n updatedAt: string;\n customHeaders: Record<string, string> | null;\n strategyConfig: Record<string, unknown> | null;\n timeoutMs: number;\n rateLimitPerMinute: number;\n autoDisableThreshold: number;\n integrationId: string | null;\n maskedSecret: string;\n previousSecretSetAt: string | null;\n};\n\nexport type WebhookDeliveryDetailResponse = {\n id: string;\n webhookId: string;\n eventType: string;\n messageId: string;\n status: string;\n responseStatus: number | null;\n errorMessage: string | null;\n attemptNumber: number;\n maxAttempts: number;\n targetUrl: string;\n durationMs: number | null;\n enqueuedAt: string;\n lastAttemptAt: string | null;\n deliveredAt: string | null;\n createdAt: string;\n payload: Record<string, unknown>;\n responseBody: string | null;\n responseHeaders: Record<string, string> | null;\n nextRetryAt: string | null;\n updatedAt: string;\n};\n\ntype DeliveryListResponse = {\n items: Array<{\n id: string;\n webhookId: string;\n webhookName: string | null;\n eventType: string;\n messageId: string;\n status: string;\n responseStatus: number | null;\n errorMessage: string | null;\n attemptNumber: number;\n maxAttempts: number;\n targetUrl: string;\n durationMs: number | null;\n enqueuedAt: string;\n lastAttemptAt: string | null;\n deliveredAt: string | null;\n createdAt: string;\n }>;\n total: number;\n page: number;\n pageSize: number;\n totalPages: number;\n};\n\ntype EventsResponse = {\n data: Array<{\n id: string;\n label: string;\n module?: string;\n category?: string;\n description?: string;\n }>;\n total: number;\n};\n\nexport function buildMockInboundUrl(endpointId = 'mock_inbound'): string {\n return `${BASE_URL}/api/webhooks/inbound/${endpointId}`;\n}\n\nexport async function createWebhookFixture(\n request: APIRequestContext,\n token: string,\n overrides: Partial<{\n name: string;\n description: string | null;\n url: string;\n subscribedEvents: string[];\n httpMethod: 'POST' | 'PUT' | 'PATCH';\n maxRetries: number;\n timeoutMs: number;\n rateLimitPerMinute: number;\n autoDisableThreshold: number;\n customHeaders: Record<string, string> | null;\n }> = {},\n): Promise<WebhookCreateResponse> {\n const response = await apiRequest(request, 'POST', '/api/webhooks', {\n token,\n data: {\n name: overrides.name ?? `Webhook ${Date.now()}`,\n description: overrides.description ?? 'Integration test webhook',\n url: overrides.url ?? buildMockInboundUrl(),\n subscribedEvents: overrides.subscribedEvents ?? ['catalog.product.created'],\n httpMethod: overrides.httpMethod ?? 'POST',\n maxRetries: overrides.maxRetries ?? 3,\n timeoutMs: overrides.timeoutMs ?? 5_000,\n rateLimitPerMinute: overrides.rateLimitPerMinute ?? 0,\n autoDisableThreshold: overrides.autoDisableThreshold ?? 5,\n customHeaders: overrides.customHeaders ?? null,\n },\n });\n\n expect(response.status()).toBe(201);\n const body = await readJsonSafe<WebhookCreateResponse>(response);\n if (!body) {\n throw new Error('Expected webhook create response body');\n }\n\n expectId(body.id, 'Expected webhook id');\n expect(typeof body.secret).toBe('string');\n expect(body.secret.startsWith('whsec_')).toBe(true);\n\n return body;\n}\n\nexport async function getWebhookDetail(\n request: APIRequestContext,\n token: string,\n webhookId: string,\n path = `/api/webhooks/${webhookId}`,\n): Promise<WebhookDetailResponse> {\n const response = await apiRequest(request, 'GET', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<WebhookDetailResponse>(response);\n if (!body) {\n throw new Error(`Expected webhook detail response for ${webhookId}`);\n }\n return body;\n}\n\nexport async function listWebhooks(\n request: APIRequestContext,\n token: string,\n path = '/api/webhooks',\n): Promise<{ items: Array<{ id: string; name: string; url: string }>; total: number }> {\n const response = await apiRequest(request, 'GET', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<{ items: Array<{ id: string; name: string; url: string }>; total: number }>(response);\n if (!body) {\n throw new Error('Expected webhook list response');\n }\n return body;\n}\n\nexport async function listWebhookEvents(request: APIRequestContext, token: string): Promise<EventsResponse> {\n const response = await apiRequest(request, 'GET', '/api/webhooks/events', { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<EventsResponse>(response);\n if (!body) {\n throw new Error('Expected webhook events response');\n }\n return body;\n}\n\nexport async function getDeliveryDetail(\n request: APIRequestContext,\n token: string,\n deliveryId: string,\n path = `/api/webhooks/deliveries/${deliveryId}`,\n): Promise<WebhookDeliveryDetailResponse> {\n const response = await apiRequest(request, 'GET', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<WebhookDeliveryDetailResponse>(response);\n if (!body) {\n throw new Error(`Expected webhook delivery detail for ${deliveryId}`);\n }\n return body;\n}\n\nexport async function listWebhookDeliveries(\n request: APIRequestContext,\n token: string,\n webhookId: string,\n path = `/api/webhooks/deliveries?webhookId=${encodeURIComponent(webhookId)}`,\n): Promise<DeliveryListResponse> {\n const response = await apiRequest(request, 'GET', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<DeliveryListResponse>(response);\n if (!body) {\n throw new Error(`Expected webhook deliveries for ${webhookId}`);\n }\n return body;\n}\n\nexport async function waitForDeliveryStatus(\n request: APIRequestContext,\n token: string,\n deliveryId: string,\n expectedStatus: string,\n): Promise<WebhookDeliveryDetailResponse> {\n for (let attempt = 0; attempt < 30; attempt += 1) {\n const detail = await getDeliveryDetail(request, token, deliveryId);\n if (detail.status === expectedStatus) {\n return detail;\n }\n await new Promise((resolve) => setTimeout(resolve, 200));\n }\n\n throw new Error(`Timed out waiting for delivery ${deliveryId} to reach status ${expectedStatus}`);\n}\n\nexport async function deleteWebhookIfExists(\n request: APIRequestContext,\n token: string | null,\n webhookId: string | null,\n): Promise<void> {\n if (!token || !webhookId) {\n return;\n }\n\n try {\n await apiRequest(request, 'DELETE', `/api/webhooks/${webhookId}`, { token });\n } catch {\n return;\n }\n}\n\nexport type WebhookRotateSecretResponse = {\n success: true;\n secret: string;\n previousSecretSetAt: string | null;\n};\n\nexport async function rotateWebhookSecret(\n request: APIRequestContext,\n token: string,\n webhookId: string,\n path = `/api/webhooks/${webhookId}/rotate-secret`,\n): Promise<WebhookRotateSecretResponse> {\n const response = await apiRequest(request, 'POST', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<WebhookRotateSecretResponse>(response);\n if (!body) {\n throw new Error(`Expected rotate-secret response for ${webhookId}`);\n }\n return body;\n}\n\nexport type WebhookTestDeliveryResponse = {\n success: true;\n delivery: WebhookDeliveryDetailResponse;\n};\n\nexport async function sendTestDelivery(\n request: APIRequestContext,\n token: string,\n webhookId: string,\n input: { eventType?: string; payload?: Record<string, unknown> } = {},\n path = `/api/webhooks/${webhookId}/test`,\n): Promise<WebhookTestDeliveryResponse> {\n const response = await apiRequest(request, 'POST', path, { token, data: input });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<WebhookTestDeliveryResponse>(response);\n if (!body) {\n throw new Error(`Expected test delivery response for ${webhookId}`);\n }\n return body;\n}\n\nexport type ProvisionedWebhooksUser = {\n userId: string;\n email: string;\n password: string;\n token: string;\n};\n\n/**\n * Creates a self-contained, role-less user and grants it an exact webhooks feature\n * set (and optional organization-visibility list) through a per-user ACL row, then\n * logs it in. The DB ACL path mirrors the org-scope fixtures: granting features via\n * the ACL API requires a super-admin actor, while `setUserAclInDb` keeps the spec\n * self-contained and deterministic. Login happens AFTER the ACL is written so the\n * fresh session resolves the new grants.\n */\nexport async function provisionWebhooksUser(\n request: APIRequestContext,\n adminToken: string,\n input: {\n tenantId: string;\n organizationId: string;\n features: string[];\n organizations?: string[] | null;\n slug: string;\n },\n): Promise<ProvisionedWebhooksUser> {\n const unique = `${Date.now()}${Math.floor(Math.random() * 1_000_000)}`;\n const email = `qa-webhooks-${input.slug}-${unique}@acme.com`;\n const password = 'Valid1!Webhooks';\n const userId = await createUserFixture(request, adminToken, {\n email,\n password,\n organizationId: input.organizationId,\n roles: [],\n name: `QA Webhooks ${input.slug}`,\n });\n await setUserAclInDb({\n userId,\n tenantId: input.tenantId,\n features: input.features,\n organizations: input.organizations ?? null,\n });\n const token = await getAuthToken(request, email, password);\n return { userId, email, password, token };\n}\n\nexport async function cleanupWebhooksUser(\n request: APIRequestContext,\n adminToken: string | null,\n user: ProvisionedWebhooksUser | null,\n): Promise<void> {\n if (!user) return;\n await deleteUserAclInDb(user.userId).catch(() => undefined);\n await deleteUserIfExists(request, adminToken, user.userId).catch(() => undefined);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { expectId, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport { createUserFixture, deleteUserIfExists } from '@open-mercato/core/helpers/integration/authFixtures';\nimport { deleteUserAclInDb, setUserAclInDb } from '@open-mercato/core/helpers/integration/dbFixtures';\n\nconst BASE_URL = process.env.BASE_URL?.trim() || 'http://localhost:3000';\n\nexport type WebhookCreateResponse = {\n id: string;\n name: string;\n url: string;\n secret: string;\n subscribedEvents: string[];\n isActive: boolean;\n};\n\nexport type WebhookDetailResponse = {\n id: string;\n name: string;\n description: string | null;\n url: string;\n subscribedEvents: string[];\n httpMethod: 'POST' | 'PUT' | 'PATCH';\n isActive: boolean;\n deliveryStrategy: string;\n maxRetries: number;\n consecutiveFailures: number;\n lastSuccessAt: string | null;\n lastFailureAt: string | null;\n createdAt: string;\n updatedAt: string;\n customHeaders: Record<string, string> | null;\n strategyConfig: Record<string, unknown> | null;\n timeoutMs: number;\n rateLimitPerMinute: number;\n autoDisableThreshold: number;\n integrationId: string | null;\n maskedSecret: string;\n previousSecretSetAt: string | null;\n};\n\nexport type WebhookDeliveryDetailResponse = {\n id: string;\n webhookId: string;\n eventType: string;\n messageId: string;\n status: string;\n responseStatus: number | null;\n errorMessage: string | null;\n attemptNumber: number;\n maxAttempts: number;\n targetUrl: string;\n durationMs: number | null;\n enqueuedAt: string;\n lastAttemptAt: string | null;\n deliveredAt: string | null;\n createdAt: string;\n payload: Record<string, unknown>;\n responseBody: string | null;\n responseHeaders: Record<string, string> | null;\n nextRetryAt: string | null;\n updatedAt: string;\n};\n\ntype DeliveryListResponse = {\n items: Array<{\n id: string;\n webhookId: string;\n webhookName: string | null;\n eventType: string;\n messageId: string;\n status: string;\n responseStatus: number | null;\n errorMessage: string | null;\n attemptNumber: number;\n maxAttempts: number;\n targetUrl: string;\n durationMs: number | null;\n enqueuedAt: string;\n lastAttemptAt: string | null;\n deliveredAt: string | null;\n createdAt: string;\n }>;\n total: number;\n page: number;\n pageSize: number;\n totalPages: number;\n};\n\ntype EventsResponse = {\n data: Array<{\n id: string;\n label: string;\n module?: string;\n category?: string;\n description?: string;\n }>;\n total: number;\n};\n\nexport function buildMockInboundUrl(endpointId = 'mock_inbound'): string {\n return `${BASE_URL}/api/webhooks/inbound/${endpointId}`;\n}\n\n// The mock inbound adapter (issue #2707) authenticates either an HMAC-SHA256 signature\n// over the raw body (x-mock-webhook-signature) or a static shared-secret token\n// (x-mock-webhook-token). Outbound delivery tests must use the static token because the\n// delivery body is generated server-side and cannot be pre-signed into customHeaders.\n// The secret mirrors the app process env (CI exports MOCK_INBOUND_WEBHOOK_SECRET to both).\nexport const MOCK_INBOUND_TOKEN_HEADER = 'x-mock-webhook-token';\n\nexport const MOCK_INBOUND_WEBHOOK_SECRET =\n process.env.MOCK_INBOUND_WEBHOOK_SECRET?.trim() || 'open-mercato-mock-dev-inbound-webhook-secret';\n\nexport function mockInboundAuthHeaders(): Record<string, string> {\n return { [MOCK_INBOUND_TOKEN_HEADER]: MOCK_INBOUND_WEBHOOK_SECRET };\n}\n\nexport function mockInboundInvalidAuthHeaders(): Record<string, string> {\n return { [MOCK_INBOUND_TOKEN_HEADER]: 'not-the-mock-inbound-secret' };\n}\n\nexport async function createWebhookFixture(\n request: APIRequestContext,\n token: string,\n overrides: Partial<{\n name: string;\n description: string | null;\n url: string;\n subscribedEvents: string[];\n httpMethod: 'POST' | 'PUT' | 'PATCH';\n maxRetries: number;\n timeoutMs: number;\n rateLimitPerMinute: number;\n autoDisableThreshold: number;\n customHeaders: Record<string, string> | null;\n }> = {},\n): Promise<WebhookCreateResponse> {\n const response = await apiRequest(request, 'POST', '/api/webhooks', {\n token,\n data: {\n name: overrides.name ?? `Webhook ${Date.now()}`,\n description: overrides.description ?? 'Integration test webhook',\n url: overrides.url ?? buildMockInboundUrl(),\n subscribedEvents: overrides.subscribedEvents ?? ['catalog.product.created'],\n httpMethod: overrides.httpMethod ?? 'POST',\n maxRetries: overrides.maxRetries ?? 3,\n timeoutMs: overrides.timeoutMs ?? 5_000,\n rateLimitPerMinute: overrides.rateLimitPerMinute ?? 0,\n autoDisableThreshold: overrides.autoDisableThreshold ?? 5,\n customHeaders: overrides.customHeaders ?? null,\n },\n });\n\n expect(response.status()).toBe(201);\n const body = await readJsonSafe<WebhookCreateResponse>(response);\n if (!body) {\n throw new Error('Expected webhook create response body');\n }\n\n expectId(body.id, 'Expected webhook id');\n expect(typeof body.secret).toBe('string');\n expect(body.secret.startsWith('whsec_')).toBe(true);\n\n return body;\n}\n\nexport async function getWebhookDetail(\n request: APIRequestContext,\n token: string,\n webhookId: string,\n path = `/api/webhooks/${webhookId}`,\n): Promise<WebhookDetailResponse> {\n const response = await apiRequest(request, 'GET', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<WebhookDetailResponse>(response);\n if (!body) {\n throw new Error(`Expected webhook detail response for ${webhookId}`);\n }\n return body;\n}\n\nexport async function listWebhooks(\n request: APIRequestContext,\n token: string,\n path = '/api/webhooks',\n): Promise<{ items: Array<{ id: string; name: string; url: string }>; total: number }> {\n const response = await apiRequest(request, 'GET', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<{ items: Array<{ id: string; name: string; url: string }>; total: number }>(response);\n if (!body) {\n throw new Error('Expected webhook list response');\n }\n return body;\n}\n\nexport async function listWebhookEvents(request: APIRequestContext, token: string): Promise<EventsResponse> {\n const response = await apiRequest(request, 'GET', '/api/webhooks/events', { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<EventsResponse>(response);\n if (!body) {\n throw new Error('Expected webhook events response');\n }\n return body;\n}\n\nexport async function getDeliveryDetail(\n request: APIRequestContext,\n token: string,\n deliveryId: string,\n path = `/api/webhooks/deliveries/${deliveryId}`,\n): Promise<WebhookDeliveryDetailResponse> {\n const response = await apiRequest(request, 'GET', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<WebhookDeliveryDetailResponse>(response);\n if (!body) {\n throw new Error(`Expected webhook delivery detail for ${deliveryId}`);\n }\n return body;\n}\n\nexport async function listWebhookDeliveries(\n request: APIRequestContext,\n token: string,\n webhookId: string,\n path = `/api/webhooks/deliveries?webhookId=${encodeURIComponent(webhookId)}`,\n): Promise<DeliveryListResponse> {\n const response = await apiRequest(request, 'GET', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<DeliveryListResponse>(response);\n if (!body) {\n throw new Error(`Expected webhook deliveries for ${webhookId}`);\n }\n return body;\n}\n\nexport async function waitForDeliveryStatus(\n request: APIRequestContext,\n token: string,\n deliveryId: string,\n expectedStatus: string,\n): Promise<WebhookDeliveryDetailResponse> {\n for (let attempt = 0; attempt < 30; attempt += 1) {\n const detail = await getDeliveryDetail(request, token, deliveryId);\n if (detail.status === expectedStatus) {\n return detail;\n }\n await new Promise((resolve) => setTimeout(resolve, 200));\n }\n\n throw new Error(`Timed out waiting for delivery ${deliveryId} to reach status ${expectedStatus}`);\n}\n\nexport async function deleteWebhookIfExists(\n request: APIRequestContext,\n token: string | null,\n webhookId: string | null,\n): Promise<void> {\n if (!token || !webhookId) {\n return;\n }\n\n try {\n await apiRequest(request, 'DELETE', `/api/webhooks/${webhookId}`, { token });\n } catch {\n return;\n }\n}\n\nexport type WebhookRotateSecretResponse = {\n success: true;\n secret: string;\n previousSecretSetAt: string | null;\n};\n\nexport async function rotateWebhookSecret(\n request: APIRequestContext,\n token: string,\n webhookId: string,\n path = `/api/webhooks/${webhookId}/rotate-secret`,\n): Promise<WebhookRotateSecretResponse> {\n const response = await apiRequest(request, 'POST', path, { token });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<WebhookRotateSecretResponse>(response);\n if (!body) {\n throw new Error(`Expected rotate-secret response for ${webhookId}`);\n }\n return body;\n}\n\nexport type WebhookTestDeliveryResponse = {\n success: true;\n delivery: WebhookDeliveryDetailResponse;\n};\n\nexport async function sendTestDelivery(\n request: APIRequestContext,\n token: string,\n webhookId: string,\n input: { eventType?: string; payload?: Record<string, unknown> } = {},\n path = `/api/webhooks/${webhookId}/test`,\n): Promise<WebhookTestDeliveryResponse> {\n const response = await apiRequest(request, 'POST', path, { token, data: input });\n expect(response.status()).toBe(200);\n const body = await readJsonSafe<WebhookTestDeliveryResponse>(response);\n if (!body) {\n throw new Error(`Expected test delivery response for ${webhookId}`);\n }\n return body;\n}\n\nexport type ProvisionedWebhooksUser = {\n userId: string;\n email: string;\n password: string;\n token: string;\n};\n\n/**\n * Creates a self-contained, role-less user and grants it an exact webhooks feature\n * set (and optional organization-visibility list) through a per-user ACL row, then\n * logs it in. The DB ACL path mirrors the org-scope fixtures: granting features via\n * the ACL API requires a super-admin actor, while `setUserAclInDb` keeps the spec\n * self-contained and deterministic. Login happens AFTER the ACL is written so the\n * fresh session resolves the new grants.\n */\nexport async function provisionWebhooksUser(\n request: APIRequestContext,\n adminToken: string,\n input: {\n tenantId: string;\n organizationId: string;\n features: string[];\n organizations?: string[] | null;\n slug: string;\n },\n): Promise<ProvisionedWebhooksUser> {\n const unique = `${Date.now()}${Math.floor(Math.random() * 1_000_000)}`;\n const email = `qa-webhooks-${input.slug}-${unique}@acme.com`;\n const password = 'Valid1!Webhooks';\n const userId = await createUserFixture(request, adminToken, {\n email,\n password,\n organizationId: input.organizationId,\n roles: [],\n name: `QA Webhooks ${input.slug}`,\n });\n await setUserAclInDb({\n userId,\n tenantId: input.tenantId,\n features: input.features,\n organizations: input.organizations ?? null,\n });\n const token = await getAuthToken(request, email, password);\n return { userId, email, password, token };\n}\n\nexport async function cleanupWebhooksUser(\n request: APIRequestContext,\n adminToken: string | null,\n user: ProvisionedWebhooksUser | null,\n): Promise<void> {\n if (!user) return;\n await deleteUserAclInDb(user.userId).catch(() => undefined);\n await deleteUserIfExists(request, adminToken, user.userId).catch(() => undefined);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/webhooks",
3
- "version": "0.6.6-develop.6409.1.4bfed821c0",
3
+ "version": "0.6.6-develop.6410.1.2ad881013e",
4
4
  "license": "MIT",
5
5
  "description": "Webhooks module for Open Mercato — Standard Webhooks compliant outbound/inbound delivery",
6
6
  "type": "module",
@@ -70,19 +70,19 @@
70
70
  }
71
71
  },
72
72
  "dependencies": {
73
- "@open-mercato/core": "0.6.6-develop.6409.1.4bfed821c0",
74
- "@open-mercato/queue": "0.6.6-develop.6409.1.4bfed821c0",
75
- "@open-mercato/ui": "0.6.6-develop.6409.1.4bfed821c0",
73
+ "@open-mercato/core": "0.6.6-develop.6410.1.2ad881013e",
74
+ "@open-mercato/queue": "0.6.6-develop.6410.1.2ad881013e",
75
+ "@open-mercato/ui": "0.6.6-develop.6410.1.2ad881013e",
76
76
  "svix": "^1.96.0"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "@mikro-orm/postgresql": "^7.0.14",
80
- "@open-mercato/shared": "0.6.6-develop.6409.1.4bfed821c0",
80
+ "@open-mercato/shared": "0.6.6-develop.6410.1.2ad881013e",
81
81
  "react": "^19.0.0",
82
82
  "react-dom": "^19.0.0"
83
83
  },
84
84
  "devDependencies": {
85
- "@open-mercato/shared": "0.6.6-develop.6409.1.4bfed821c0",
85
+ "@open-mercato/shared": "0.6.6-develop.6410.1.2ad881013e",
86
86
  "@types/jest": "^30.0.0",
87
87
  "@types/react": "^19.2.17",
88
88
  "@types/react-dom": "^19.2.3",
@@ -3,6 +3,8 @@ import { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration
3
3
  import { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';
4
4
  import {
5
5
  buildMockInboundUrl,
6
+ mockInboundAuthHeaders,
7
+ mockInboundInvalidAuthHeaders,
6
8
  createWebhookFixture,
7
9
  deleteWebhookIfExists,
8
10
  getDeliveryDetail,
@@ -20,9 +22,7 @@ test.describe('TC-WEBHOOK-002: Webhook delivery lifecycle', () => {
20
22
  name: `Webhook Delivery ${Date.now()}`,
21
23
  url: buildMockInboundUrl(),
22
24
  subscribedEvents: ['catalog.product.created'],
23
- customHeaders: {
24
- 'x-mock-webhook-signature': 'valid',
25
- },
25
+ customHeaders: mockInboundAuthHeaders(),
26
26
  });
27
27
  webhookId = created.id;
28
28
 
@@ -86,9 +86,7 @@ test.describe('TC-WEBHOOK-002: Webhook delivery lifecycle', () => {
86
86
  name: `Webhook Retry ${Date.now()}`,
87
87
  url: buildMockInboundUrl(),
88
88
  subscribedEvents: ['catalog.product.updated'],
89
- customHeaders: {
90
- 'x-mock-webhook-signature': 'invalid',
91
- },
89
+ customHeaders: mockInboundInvalidAuthHeaders(),
92
90
  });
93
91
  webhookId = created.id;
94
92
 
@@ -116,9 +114,7 @@ test.describe('TC-WEBHOOK-002: Webhook delivery lifecycle', () => {
116
114
  const updateResponse = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {
117
115
  token,
118
116
  data: {
119
- customHeaders: {
120
- 'x-mock-webhook-signature': 'valid',
121
- },
117
+ customHeaders: mockInboundAuthHeaders(),
122
118
  },
123
119
  });
124
120
  expect(updateResponse.status()).toBe(200);
@@ -1,7 +1,20 @@
1
+ import { createHmac } from 'node:crypto';
1
2
  import { expect, test } from '@playwright/test';
2
3
 
3
4
  const BASE_URL = process.env.BASE_URL?.trim() || 'http://localhost:3000';
4
5
 
6
+ // Mirrors the create-app mock inbound webhook adapter, which verifies an HMAC-SHA256
7
+ // signature over the raw request body in the x-mock-webhook-signature header. The running
8
+ // app resolves its secret from MOCK_INBOUND_WEBHOOK_SECRET (exported by the CI workflow so
9
+ // it works even when `yarn start` forces NODE_ENV=production); we read the SAME env var here
10
+ // so the test signs with the same key. The dev fallback is only a local-default convenience.
11
+ const MOCK_INBOUND_WEBHOOK_SECRET =
12
+ process.env.MOCK_INBOUND_WEBHOOK_SECRET?.trim() || 'open-mercato-mock-dev-inbound-webhook-secret';
13
+
14
+ function signMockInboundWebhook(rawBody: string): string {
15
+ return createHmac('sha256', MOCK_INBOUND_WEBHOOK_SECRET).update(rawBody, 'utf-8').digest('hex');
16
+ }
17
+
5
18
  test.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {
6
19
  test('should accept valid inbound webhooks, mark duplicates, and reject invalid endpoints or signatures', async ({ request }) => {
7
20
  const messageId = `msg-${Date.now()}`;
@@ -13,17 +26,19 @@ test.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {
13
26
  externalId: `ext-${Date.now()}`,
14
27
  },
15
28
  };
29
+ const rawBody = JSON.stringify(payload);
30
+ const validSignature = signMockInboundWebhook(rawBody);
16
31
 
17
32
  const firstResponse = await request.fetch(`${BASE_URL}/api/webhooks/inbound/mock_inbound`, {
18
33
  method: 'POST',
19
34
  headers: {
20
35
  'content-type': 'application/json',
21
- 'x-mock-webhook-signature': 'valid',
36
+ 'x-mock-webhook-signature': validSignature,
22
37
  'webhook-id': messageId,
23
38
  'webhook-timestamp': replayTimestamp,
24
39
  'webhook-signature': 'v1,mock',
25
40
  },
26
- data: JSON.stringify(payload),
41
+ data: rawBody,
27
42
  });
28
43
  expect(firstResponse.status()).toBe(200);
29
44
  await expect(firstResponse.json()).resolves.toEqual({ ok: true });
@@ -32,12 +47,12 @@ test.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {
32
47
  method: 'POST',
33
48
  headers: {
34
49
  'content-type': 'application/json',
35
- 'x-mock-webhook-signature': 'valid',
50
+ 'x-mock-webhook-signature': validSignature,
36
51
  'webhook-id': messageId,
37
52
  'webhook-timestamp': replayTimestamp,
38
53
  'webhook-signature': 'v1,mock',
39
54
  },
40
- data: JSON.stringify(payload),
55
+ data: rawBody,
41
56
  });
42
57
  expect(duplicateResponse.status()).toBe(200);
43
58
  await expect(duplicateResponse.json()).resolves.toEqual({ ok: true, duplicate: true });
@@ -46,11 +61,11 @@ test.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {
46
61
  method: 'POST',
47
62
  headers: {
48
63
  'content-type': 'application/json',
49
- 'x-mock-webhook-signature': 'valid',
64
+ 'x-mock-webhook-signature': validSignature,
50
65
  'webhook-timestamp': replayTimestamp,
51
66
  'webhook-signature': 'v1,mock',
52
67
  },
53
- data: JSON.stringify(payload),
68
+ data: rawBody,
54
69
  });
55
70
  expect(replayWithoutMessageIdResponse.status()).toBe(200);
56
71
  await expect(replayWithoutMessageIdResponse.json()).resolves.toEqual({ ok: true });
@@ -59,11 +74,11 @@ test.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {
59
74
  method: 'POST',
60
75
  headers: {
61
76
  'content-type': 'application/json',
62
- 'x-mock-webhook-signature': 'valid',
77
+ 'x-mock-webhook-signature': validSignature,
63
78
  'webhook-timestamp': replayTimestamp,
64
79
  'webhook-signature': 'v1,mock',
65
80
  },
66
- data: JSON.stringify(payload),
81
+ data: rawBody,
67
82
  });
68
83
  expect(replayWithoutMessageIdDuplicateResponse.status()).toBe(200);
69
84
  await expect(replayWithoutMessageIdDuplicateResponse.json()).resolves.toEqual({ ok: true, duplicate: true });
@@ -74,7 +89,7 @@ test.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {
74
89
  'content-type': 'application/json',
75
90
  'x-mock-webhook-signature': 'invalid',
76
91
  },
77
- data: JSON.stringify(payload),
92
+ data: rawBody,
78
93
  });
79
94
  expect(invalidSignatureResponse.status()).toBe(400);
80
95
  await expect(invalidSignatureResponse.json()).resolves.toEqual({ error: 'Verification failed' });
@@ -83,9 +98,9 @@ test.describe('TC-WEBHOOK-003: Inbound webhook receiver', () => {
83
98
  method: 'POST',
84
99
  headers: {
85
100
  'content-type': 'application/json',
86
- 'x-mock-webhook-signature': 'valid',
101
+ 'x-mock-webhook-signature': validSignature,
87
102
  },
88
- data: JSON.stringify(payload),
103
+ data: rawBody,
89
104
  });
90
105
  expect(unknownEndpointResponse.status()).toBe(404);
91
106
  await expect(unknownEndpointResponse.json()).resolves.toEqual({ error: 'Webhook endpoint not found' });
@@ -5,6 +5,7 @@ import {
5
5
  cleanupWebhooksUser,
6
6
  createWebhookFixture,
7
7
  deleteWebhookIfExists,
8
+ mockInboundAuthHeaders,
8
9
  provisionWebhooksUser,
9
10
  type ProvisionedWebhooksUser,
10
11
  } from './helpers/fixtures';
@@ -50,7 +51,7 @@ test.describe('TC-WEBHOOK-005: RBAC permission gates by feature flag', () => {
50
51
  name: `Webhook RBAC ${Date.now()}`,
51
52
  url: 'https://example.com/webhook-rbac',
52
53
  subscribedEvents: ['catalog.product.created'],
53
- customHeaders: { 'x-mock-webhook-signature': 'valid' },
54
+ customHeaders: mockInboundAuthHeaders(),
54
55
  });
55
56
  webhookId = created.id;
56
57
 
@@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test';
2
2
  import { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';
3
3
  import {
4
4
  buildMockInboundUrl,
5
+ mockInboundAuthHeaders,
5
6
  createWebhookFixture,
6
7
  deleteWebhookIfExists,
7
8
  listWebhookDeliveries,
@@ -27,7 +28,7 @@ test.describe('TC-WEBHOOK-006: Webhook deactivation blocks delivery', () => {
27
28
  name: `Webhook Deactivation ${Date.now()}`,
28
29
  url: buildMockInboundUrl(),
29
30
  subscribedEvents: ['catalog.product.created'],
30
- customHeaders: { 'x-mock-webhook-signature': 'valid' },
31
+ customHeaders: mockInboundAuthHeaders(),
31
32
  });
32
33
  webhookId = created.id;
33
34
 
@@ -2,6 +2,8 @@ import { expect, test } from '@playwright/test';
2
2
  import { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';
3
3
  import {
4
4
  buildMockInboundUrl,
5
+ mockInboundAuthHeaders,
6
+ mockInboundInvalidAuthHeaders,
5
7
  createWebhookFixture,
6
8
  deleteWebhookIfExists,
7
9
  listWebhookDeliveries,
@@ -34,7 +36,7 @@ test.describe('TC-WEBHOOK-008: Delivery list filtering by status and event type'
34
36
  name: `Webhook Filter ${Date.now()}`,
35
37
  url: buildMockInboundUrl(),
36
38
  subscribedEvents: [EVENT_CREATED, EVENT_UPDATED],
37
- customHeaders: { 'x-mock-webhook-signature': 'valid' },
39
+ customHeaders: mockInboundAuthHeaders(),
38
40
  });
39
41
  webhookId = created.id;
40
42
 
@@ -46,7 +48,7 @@ test.describe('TC-WEBHOOK-008: Delivery list filtering by status and event type'
46
48
  // Flip the mock signature to invalid so the next attempt fails terminally (expired).
47
49
  const invalidate = await apiRequest(request, 'PUT', `/api/webhooks/${created.id}`, {
48
50
  token,
49
- data: { customHeaders: { 'x-mock-webhook-signature': 'invalid' } },
51
+ data: { customHeaders: mockInboundInvalidAuthHeaders() },
50
52
  });
51
53
  expect(invalidate.status()).toBe(200);
52
54
 
@@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test';
2
2
  import { getAuthToken } from '@open-mercato/core/helpers/integration/api';
3
3
  import {
4
4
  buildMockInboundUrl,
5
+ mockInboundAuthHeaders,
5
6
  createWebhookFixture,
6
7
  deleteWebhookIfExists,
7
8
  getDeliveryDetail,
@@ -28,7 +29,7 @@ test.describe('TC-WEBHOOK-010: Delivery detail full payload and response body',
28
29
  name: `Webhook Detail ${Date.now()}`,
29
30
  url: buildMockInboundUrl(),
30
31
  subscribedEvents: ['catalog.product.created'],
31
- customHeaders: { 'x-mock-webhook-signature': 'valid' },
32
+ customHeaders: mockInboundAuthHeaders(),
32
33
  });
33
34
  webhookId = created.id;
34
35
 
@@ -103,6 +103,24 @@ export function buildMockInboundUrl(endpointId = 'mock_inbound'): string {
103
103
  return `${BASE_URL}/api/webhooks/inbound/${endpointId}`;
104
104
  }
105
105
 
106
+ // The mock inbound adapter (issue #2707) authenticates either an HMAC-SHA256 signature
107
+ // over the raw body (x-mock-webhook-signature) or a static shared-secret token
108
+ // (x-mock-webhook-token). Outbound delivery tests must use the static token because the
109
+ // delivery body is generated server-side and cannot be pre-signed into customHeaders.
110
+ // The secret mirrors the app process env (CI exports MOCK_INBOUND_WEBHOOK_SECRET to both).
111
+ export const MOCK_INBOUND_TOKEN_HEADER = 'x-mock-webhook-token';
112
+
113
+ export const MOCK_INBOUND_WEBHOOK_SECRET =
114
+ process.env.MOCK_INBOUND_WEBHOOK_SECRET?.trim() || 'open-mercato-mock-dev-inbound-webhook-secret';
115
+
116
+ export function mockInboundAuthHeaders(): Record<string, string> {
117
+ return { [MOCK_INBOUND_TOKEN_HEADER]: MOCK_INBOUND_WEBHOOK_SECRET };
118
+ }
119
+
120
+ export function mockInboundInvalidAuthHeaders(): Record<string, string> {
121
+ return { [MOCK_INBOUND_TOKEN_HEADER]: 'not-the-mock-inbound-secret' };
122
+ }
123
+
106
124
  export async function createWebhookFixture(
107
125
  request: APIRequestContext,
108
126
  token: string,