@friggframework/core 2.0.0-next.44 → 2.0.0-next.45

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.
@@ -0,0 +1,356 @@
1
+ jest.mock('../database/config', () => ({
2
+ DB_TYPE: 'mongodb',
3
+ getDatabaseType: jest.fn(() => 'mongodb'),
4
+ PRISMA_LOG_LEVEL: 'error,warn',
5
+ PRISMA_QUERY_LOGGING: false,
6
+ }));
7
+
8
+ const { IntegrationBase } = require('../integrations/integration-base');
9
+ const { IntegrationEventDispatcher } = require('./integration-event-dispatcher');
10
+ const { QueuerUtil } = require('../queues');
11
+
12
+ // Mock AWS SQS
13
+ jest.mock('aws-sdk', () => {
14
+ const mockSQS = {
15
+ sendMessage: jest.fn((params, callback) => {
16
+ callback(null, { MessageId: 'mock-message-id-123' });
17
+ }),
18
+ };
19
+ return {
20
+ SQS: jest.fn(() => mockSQS),
21
+ config: { update: jest.fn() },
22
+ };
23
+ });
24
+
25
+ class WebhookTestIntegration extends IntegrationBase {
26
+ static Definition = {
27
+ name: 'webhook-test',
28
+ version: '1.0.0',
29
+ modules: {},
30
+ webhooks: true,
31
+ };
32
+
33
+ constructor(params) {
34
+ super(params);
35
+ this.webhookData = null;
36
+ }
37
+
38
+ // Override for custom signature verification
39
+ async onWebhookReceived({ req, res }) {
40
+ const signature = req.headers['x-custom-signature'];
41
+
42
+ if (signature && signature !== 'valid-signature-123') {
43
+ return res.status(401).json({ error: 'Invalid signature' });
44
+ }
45
+
46
+ await this.queueWebhook({
47
+ integrationId: req.params.integrationId,
48
+ body: req.body,
49
+ headers: req.headers,
50
+ query: req.query,
51
+ });
52
+
53
+ res.status(200).json({ received: true, verified: !!signature });
54
+ }
55
+
56
+ // Override for webhook processing
57
+ async onWebhook({ data }) {
58
+ this.webhookData = data;
59
+ return { processed: true, webhookData: data };
60
+ }
61
+ }
62
+
63
+ describe('Webhook Flow Integration Test', () => {
64
+ describe('End-to-End Webhook Flow', () => {
65
+ beforeEach(() => {
66
+ jest.clearAllMocks();
67
+ process.env.WEBHOOK_TEST_QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue';
68
+ });
69
+
70
+ it('should complete full webhook flow: HTTP → Queue → Worker', async () => {
71
+ // Step 1: Simulate HTTP webhook received
72
+ const integration = new WebhookTestIntegration();
73
+ const dispatcher = new IntegrationEventDispatcher(integration);
74
+
75
+ const req = {
76
+ body: { event: 'item.created', itemId: '12345' },
77
+ params: { integrationId: 'int-789' },
78
+ headers: { 'content-type': 'application/json' },
79
+ query: {},
80
+ };
81
+ const res = {
82
+ status: jest.fn().mockReturnThis(),
83
+ json: jest.fn(),
84
+ };
85
+
86
+ // Execute WEBHOOK_RECEIVED
87
+ await dispatcher.dispatchHttp({
88
+ event: 'WEBHOOK_RECEIVED',
89
+ req,
90
+ res,
91
+ next: jest.fn(),
92
+ });
93
+
94
+ // Verify HTTP response
95
+ expect(res.status).toHaveBeenCalledWith(200);
96
+ expect(res.json).toHaveBeenCalledWith({ received: true, verified: false });
97
+
98
+ // Verify message was queued
99
+ const AWS = require('aws-sdk');
100
+ const mockSQS = new AWS.SQS();
101
+ expect(mockSQS.sendMessage).toHaveBeenCalled();
102
+
103
+ const queueCall = mockSQS.sendMessage.mock.calls[0][0];
104
+ expect(queueCall.QueueUrl).toBe(process.env.WEBHOOK_TEST_QUEUE_URL);
105
+
106
+ const queuedMessage = JSON.parse(queueCall.MessageBody);
107
+ expect(queuedMessage.event).toBe('ON_WEBHOOK');
108
+ expect(queuedMessage.data.integrationId).toBe('int-789');
109
+ expect(queuedMessage.data.body).toEqual({ event: 'item.created', itemId: '12345' });
110
+
111
+ // Step 2: Simulate worker processing from queue
112
+ const workerIntegration = new WebhookTestIntegration();
113
+ const workerDispatcher = new IntegrationEventDispatcher(workerIntegration);
114
+
115
+ const result = await workerDispatcher.dispatchJob({
116
+ event: 'ON_WEBHOOK',
117
+ data: queuedMessage.data,
118
+ context: {},
119
+ });
120
+
121
+ // Verify processing result
122
+ expect(result.processed).toBe(true);
123
+ expect(result.webhookData).toEqual(queuedMessage.data);
124
+ expect(workerIntegration.webhookData).not.toBeNull();
125
+ });
126
+
127
+ it('should support custom signature verification', async () => {
128
+ const integration = new WebhookTestIntegration();
129
+ const dispatcher = new IntegrationEventDispatcher(integration);
130
+
131
+ const reqInvalid = {
132
+ body: { event: 'test' },
133
+ params: {},
134
+ headers: { 'x-custom-signature': 'invalid-sig' },
135
+ query: {},
136
+ };
137
+ const resInvalid = {
138
+ status: jest.fn().mockReturnThis(),
139
+ json: jest.fn(),
140
+ };
141
+
142
+ // Test invalid signature
143
+ await dispatcher.dispatchHttp({
144
+ event: 'WEBHOOK_RECEIVED',
145
+ req: reqInvalid,
146
+ res: resInvalid,
147
+ next: jest.fn(),
148
+ });
149
+
150
+ expect(resInvalid.status).toHaveBeenCalledWith(401);
151
+ expect(resInvalid.json).toHaveBeenCalledWith({ error: 'Invalid signature' });
152
+
153
+ // Test valid signature
154
+ const reqValid = {
155
+ body: { event: 'test' },
156
+ params: {},
157
+ headers: { 'x-custom-signature': 'valid-signature-123' },
158
+ query: {},
159
+ };
160
+ const resValid = {
161
+ status: jest.fn().mockReturnThis(),
162
+ json: jest.fn(),
163
+ };
164
+
165
+ await dispatcher.dispatchHttp({
166
+ event: 'WEBHOOK_RECEIVED',
167
+ req: reqValid,
168
+ res: resValid,
169
+ next: jest.fn(),
170
+ });
171
+
172
+ expect(resValid.status).toHaveBeenCalledWith(200);
173
+ expect(resValid.json).toHaveBeenCalledWith({ received: true, verified: true });
174
+ });
175
+
176
+ it('should handle webhooks without integration ID', async () => {
177
+ const integration = new WebhookTestIntegration();
178
+ const dispatcher = new IntegrationEventDispatcher(integration);
179
+
180
+ const req = {
181
+ body: { event: 'system.event' },
182
+ params: {}, // No integrationId
183
+ headers: {},
184
+ query: {},
185
+ };
186
+ const res = {
187
+ status: jest.fn().mockReturnThis(),
188
+ json: jest.fn(),
189
+ };
190
+
191
+ await dispatcher.dispatchHttp({
192
+ event: 'WEBHOOK_RECEIVED',
193
+ req,
194
+ res,
195
+ next: jest.fn(),
196
+ });
197
+
198
+ // Should queue with integrationId: null
199
+ const AWS = require('aws-sdk');
200
+ const mockSQS = new AWS.SQS();
201
+ const queuedMessage = JSON.parse(mockSQS.sendMessage.mock.calls[0][0].MessageBody);
202
+
203
+ expect(queuedMessage.data.integrationId).toBeNull();
204
+ });
205
+
206
+ it('should preserve webhook headers and query params', async () => {
207
+ const integration = new WebhookTestIntegration();
208
+ const dispatcher = new IntegrationEventDispatcher(integration);
209
+
210
+ const req = {
211
+ body: { event: 'test' },
212
+ params: { integrationId: 'int-456' },
213
+ headers: {
214
+ 'x-webhook-id': 'webhook-123',
215
+ 'x-custom-header': 'value',
216
+ },
217
+ query: { timestamp: '2025-10-15', version: '2' },
218
+ };
219
+ const res = {
220
+ status: jest.fn().mockReturnThis(),
221
+ json: jest.fn(),
222
+ };
223
+
224
+ await dispatcher.dispatchHttp({
225
+ event: 'WEBHOOK_RECEIVED',
226
+ req,
227
+ res,
228
+ next: jest.fn(),
229
+ });
230
+
231
+ const AWS = require('aws-sdk');
232
+ const mockSQS = new AWS.SQS();
233
+ const queuedMessage = JSON.parse(mockSQS.sendMessage.mock.calls[0][0].MessageBody);
234
+
235
+ expect(queuedMessage.data.headers).toEqual(req.headers);
236
+ expect(queuedMessage.data.query).toEqual(req.query);
237
+ });
238
+ });
239
+
240
+ describe('Default Webhook Handlers', () => {
241
+ it('should use default WEBHOOK_RECEIVED handler if not overridden', async () => {
242
+ // Integration without custom handler
243
+ class DefaultWebhookIntegration extends IntegrationBase {
244
+ static Definition = {
245
+ name: 'default-webhook',
246
+ version: '1.0.0',
247
+ modules: {},
248
+ webhooks: true,
249
+ };
250
+ }
251
+
252
+ process.env.DEFAULT_WEBHOOK_QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789/default-queue';
253
+
254
+ const integration = new DefaultWebhookIntegration();
255
+ const dispatcher = new IntegrationEventDispatcher(integration);
256
+
257
+ const req = {
258
+ body: { test: 'data' },
259
+ params: {},
260
+ headers: {},
261
+ query: {},
262
+ };
263
+ const res = {
264
+ status: jest.fn().mockReturnThis(),
265
+ json: jest.fn(),
266
+ };
267
+
268
+ await dispatcher.dispatchHttp({
269
+ event: 'WEBHOOK_RECEIVED',
270
+ req,
271
+ res,
272
+ next: jest.fn(),
273
+ });
274
+
275
+ // Should use default handler
276
+ expect(res.status).toHaveBeenCalledWith(200);
277
+ expect(res.json).toHaveBeenCalledWith({ received: true });
278
+ });
279
+
280
+ it('should use default ON_WEBHOOK handler if not overridden', async () => {
281
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
282
+
283
+ class DefaultWebhookIntegration extends IntegrationBase {
284
+ static Definition = {
285
+ name: 'default-webhook-worker',
286
+ version: '1.0.0',
287
+ modules: {},
288
+ webhooks: true,
289
+ };
290
+ }
291
+
292
+ const integration = new DefaultWebhookIntegration();
293
+ const dispatcher = new IntegrationEventDispatcher(integration);
294
+
295
+ const webhookData = { body: { test: 'data' } };
296
+
297
+ await dispatcher.dispatchJob({
298
+ event: 'ON_WEBHOOK',
299
+ data: webhookData,
300
+ context: {},
301
+ });
302
+
303
+ // Default handler logs the data
304
+ expect(consoleSpy).toHaveBeenCalledWith('Webhook received:', webhookData);
305
+
306
+ consoleSpy.mockRestore();
307
+ });
308
+ });
309
+
310
+ describe('Error Handling', () => {
311
+ it('should handle queueing errors gracefully', async () => {
312
+ const AWS = require('aws-sdk');
313
+ const mockSQS = new AWS.SQS();
314
+ mockSQS.sendMessage.mockImplementation((params, callback) => {
315
+ callback(new Error('Queue is full'), null);
316
+ });
317
+
318
+ process.env.WEBHOOK_TEST_QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue';
319
+
320
+ const integration = new WebhookTestIntegration();
321
+ const dispatcher = new IntegrationEventDispatcher(integration);
322
+
323
+ const req = {
324
+ body: { event: 'test' },
325
+ params: {},
326
+ headers: {},
327
+ query: {},
328
+ };
329
+ const res = {
330
+ status: jest.fn().mockReturnThis(),
331
+ json: jest.fn(),
332
+ };
333
+
334
+ // Should throw error when queueing fails
335
+ await expect(
336
+ dispatcher.dispatchHttp({
337
+ event: 'WEBHOOK_RECEIVED',
338
+ req,
339
+ res,
340
+ next: jest.fn(),
341
+ })
342
+ ).rejects.toThrow('Queue is full');
343
+ });
344
+
345
+ it('should throw error if queue URL not configured', async () => {
346
+ delete process.env.WEBHOOK_TEST_QUEUE_URL;
347
+
348
+ const integration = new WebhookTestIntegration();
349
+
350
+ await expect(
351
+ integration.queueWebhook({ test: 'data' })
352
+ ).rejects.toThrow('Queue URL not found for WEBHOOK_TEST_QUEUE_URL');
353
+ });
354
+ });
355
+ });
356
+
@@ -0,0 +1,184 @@
1
+ jest.mock('../../database/config', () => ({
2
+ DB_TYPE: 'mongodb',
3
+ getDatabaseType: jest.fn(() => 'mongodb'),
4
+ PRISMA_LOG_LEVEL: 'error,warn',
5
+ PRISMA_QUERY_LOGGING: false,
6
+ }));
7
+
8
+ const { createQueueWorker } = require('../backend-utils');
9
+ const { IntegrationBase } = require('../../integrations/integration-base');
10
+ const { IntegrationEventDispatcher } = require('../integration-event-dispatcher');
11
+
12
+ class TestWebhookIntegration extends IntegrationBase {
13
+ static Definition = {
14
+ name: 'test-webhook',
15
+ version: '1.0.0',
16
+ modules: {},
17
+ webhooks: true,
18
+ };
19
+
20
+ constructor(params) {
21
+ super(params);
22
+ this.onWebhookCalled = false;
23
+ this.receivedData = null;
24
+ }
25
+
26
+ async onWebhook({ data }) {
27
+ this.onWebhookCalled = true;
28
+ this.receivedData = data;
29
+ return { processed: true, data };
30
+ }
31
+ }
32
+
33
+ describe('Webhook Queue Worker', () => {
34
+ describe('ON_WEBHOOK event processing', () => {
35
+ it('should process ON_WEBHOOK event without integration ID (unhydrated)', async () => {
36
+ const QueueWorker = createQueueWorker(TestWebhookIntegration);
37
+ const worker = new QueueWorker();
38
+
39
+ const params = {
40
+ event: 'ON_WEBHOOK',
41
+ data: {
42
+ body: { webhookEvent: 'created', entityId: '123' },
43
+ headers: { 'content-type': 'application/json' },
44
+ },
45
+ };
46
+
47
+ const sqsEvent = {
48
+ Records: [{ body: JSON.stringify(params) }],
49
+ };
50
+
51
+ // Should work with unhydrated instance without throwing
52
+ await expect(worker.run(sqsEvent, {})).resolves.not.toThrow();
53
+ });
54
+
55
+ it('should call ON_WEBHOOK handler with webhook data', async () => {
56
+ const QueueWorker = createQueueWorker(TestWebhookIntegration);
57
+ const worker = new QueueWorker();
58
+
59
+ const webhookData = {
60
+ body: { webhookEvent: 'created', entityId: '123' },
61
+ headers: { 'content-type': 'application/json' },
62
+ query: {},
63
+ };
64
+
65
+ const params = {
66
+ event: 'ON_WEBHOOK',
67
+ data: webhookData,
68
+ };
69
+
70
+ const sqsEvent = {
71
+ Records: [{ body: JSON.stringify(params) }],
72
+ };
73
+
74
+ await worker.run(sqsEvent, {});
75
+
76
+ // The handler should have been called
77
+ });
78
+
79
+ it('should handle multiple webhook messages in batch', async () => {
80
+ const QueueWorker = createQueueWorker(TestWebhookIntegration);
81
+ const worker = new QueueWorker();
82
+
83
+ const message1 = {
84
+ event: 'ON_WEBHOOK',
85
+ data: { body: { event: '1' } },
86
+ };
87
+ const message2 = {
88
+ event: 'ON_WEBHOOK',
89
+ data: { body: { event: '2' } },
90
+ };
91
+
92
+ const sqsEvent = {
93
+ Records: [
94
+ { body: JSON.stringify(message1) },
95
+ { body: JSON.stringify(message2) },
96
+ ],
97
+ };
98
+
99
+ await worker.run(sqsEvent, {});
100
+
101
+ // Should process both messages without error
102
+ });
103
+ });
104
+
105
+ describe('Error Handling', () => {
106
+ it('should throw error if ON_WEBHOOK handler fails', async () => {
107
+ const FailingIntegration = class extends TestWebhookIntegration {
108
+ async onWebhook({ data }) {
109
+ throw new Error('Processing failed');
110
+ }
111
+ };
112
+
113
+ const FailingWorker = createQueueWorker(FailingIntegration);
114
+ const failingWorker = new FailingWorker();
115
+
116
+ const params = {
117
+ event: 'ON_WEBHOOK',
118
+ data: { body: { invalid: 'data' } },
119
+ };
120
+
121
+ const sqsEvent = {
122
+ Records: [{ body: JSON.stringify(params) }],
123
+ };
124
+
125
+ await expect(failingWorker.run(sqsEvent, {})).rejects.toThrow('Processing failed');
126
+ });
127
+
128
+ it('should log errors with integration context', async () => {
129
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
130
+
131
+ const FailingIntegration = class extends TestWebhookIntegration {
132
+ async onWebhook({ data }) {
133
+ throw new Error('Test error');
134
+ }
135
+ };
136
+
137
+ const FailingWorker = createQueueWorker(FailingIntegration);
138
+ const failingWorker = new FailingWorker();
139
+
140
+ const params = {
141
+ event: 'ON_WEBHOOK',
142
+ data: { body: {} },
143
+ };
144
+
145
+ const sqsEvent = {
146
+ Records: [{ body: JSON.stringify(params) }],
147
+ };
148
+
149
+ await expect(failingWorker.run(sqsEvent, {})).rejects.toThrow();
150
+ expect(consoleSpy).toHaveBeenCalledWith(
151
+ expect.stringContaining('Error in ON_WEBHOOK for test-webhook'),
152
+ expect.any(Error)
153
+ );
154
+
155
+ consoleSpy.mockRestore();
156
+ });
157
+ });
158
+
159
+ describe('Integration Hydration for webhooks with integrationId', () => {
160
+ it('should attempt to load integration when integrationId present', async () => {
161
+ // This test verifies the logic path - full integration test
162
+ // will verify actual DB loading with mocked repositories
163
+ const QueueWorker = createQueueWorker(TestWebhookIntegration);
164
+ const worker = new QueueWorker();
165
+
166
+ const params = {
167
+ event: 'ON_WEBHOOK',
168
+ data: {
169
+ integrationId: 'integration-456',
170
+ body: { webhookEvent: 'updated' },
171
+ },
172
+ };
173
+
174
+ const sqsEvent = {
175
+ Records: [{ body: JSON.stringify(params) }],
176
+ };
177
+
178
+ // This will fail trying to load the integration from DB
179
+ // but it proves the code path is attempted
180
+ await expect(worker.run(sqsEvent, {})).rejects.toThrow();
181
+ });
182
+ });
183
+ });
184
+
package/index.js CHANGED
@@ -43,6 +43,18 @@ const {
43
43
  const {
44
44
  IntegrationMappingRepository,
45
45
  } = require('./integrations/repositories/integration-mapping-repository');
46
+ const {
47
+ CreateProcess,
48
+ } = require('./integrations/use-cases/create-process');
49
+ const {
50
+ UpdateProcessState,
51
+ } = require('./integrations/use-cases/update-process-state');
52
+ const {
53
+ UpdateProcessMetrics,
54
+ } = require('./integrations/use-cases/update-process-metrics');
55
+ const {
56
+ GetProcess,
57
+ } = require('./integrations/use-cases/get-process');
46
58
  const { Cryptor } = require('./encrypt');
47
59
  const {
48
60
  BaseError,
@@ -130,6 +142,10 @@ module.exports = {
130
142
  createIntegrationRouter,
131
143
  getModulesDefinitionFromIntegrationClasses,
132
144
  LoadIntegrationContextUseCase,
145
+ CreateProcess,
146
+ UpdateProcessState,
147
+ UpdateProcessMetrics,
148
+ GetProcess,
133
149
 
134
150
  // application - Command factories for integration developers
135
151
  application,