@friggframework/core 2.0.0--canary.396.6862738.0 → 2.0.0--canary.397.c07f148.0

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 (69) hide show
  1. package/README.md +931 -50
  2. package/core/create-handler.js +1 -0
  3. package/credential/credential-repository.js +48 -1
  4. package/credential/use-cases/update-authentication-status.js +15 -0
  5. package/handlers/backend-utils.js +34 -31
  6. package/handlers/routers/auth.js +1 -15
  7. package/index.js +1 -5
  8. package/integrations/integration-base.js +133 -40
  9. package/integrations/integration-repository.js +39 -3
  10. package/integrations/integration-router.js +109 -85
  11. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  12. package/integrations/tests/doubles/test-integration-repository.js +89 -0
  13. package/integrations/tests/use-cases/create-integration.test.js +124 -0
  14. package/integrations/tests/use-cases/delete-integration-for-user.test.js +143 -0
  15. package/integrations/tests/use-cases/get-integration-for-user.test.js +143 -0
  16. package/integrations/tests/use-cases/get-integration-instance.test.js +169 -0
  17. package/integrations/tests/use-cases/get-integrations-for-user.test.js +169 -0
  18. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  19. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  20. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  21. package/integrations/tests/use-cases/update-integration.test.js +134 -0
  22. package/integrations/use-cases/create-integration.js +25 -12
  23. package/integrations/use-cases/delete-integration-for-user.js +21 -2
  24. package/integrations/use-cases/get-integration-for-user.js +28 -13
  25. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  26. package/integrations/use-cases/get-integration-instance.js +20 -11
  27. package/integrations/use-cases/get-integrations-for-user.js +22 -10
  28. package/integrations/use-cases/get-possible-integrations.js +27 -0
  29. package/integrations/use-cases/update-integration-messages.js +31 -0
  30. package/integrations/use-cases/update-integration-status.js +28 -0
  31. package/integrations/use-cases/update-integration.js +23 -13
  32. package/integrations/utils/map-integration-dto.js +0 -1
  33. package/{module-plugin → modules}/entity.js +1 -0
  34. package/{module-plugin → modules}/index.js +0 -4
  35. package/{module-plugin/module-service.js → modules/module-factory.js} +9 -5
  36. package/modules/module-repository.js +107 -0
  37. package/modules/module.js +218 -0
  38. package/modules/tests/doubles/test-module-factory.js +16 -0
  39. package/modules/tests/doubles/test-module-repository.js +19 -0
  40. package/{module-plugin → modules}/use-cases/get-entities-for-user.js +1 -1
  41. package/modules/use-cases/get-entity-options-by-id.js +58 -0
  42. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  43. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  44. package/modules/use-cases/get-module.js +56 -0
  45. package/modules/use-cases/process-authorization-callback.js +108 -0
  46. package/modules/use-cases/refresh-entity-options.js +58 -0
  47. package/modules/use-cases/test-module-auth.js +54 -0
  48. package/{module-plugin → modules}/utils/map-module-dto.js +1 -1
  49. package/package.json +5 -5
  50. package/syncs/sync.js +0 -1
  51. package/types/module-plugin/index.d.ts +0 -35
  52. package/types/syncs/index.d.ts +0 -2
  53. package/integrations/integration.js +0 -233
  54. package/integrations/test/integration-base.test.js +0 -144
  55. package/module-plugin/manager.js +0 -169
  56. package/module-plugin/module-factory.js +0 -42
  57. package/module-plugin/module-repository.js +0 -70
  58. package/module-plugin/module.js +0 -329
  59. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  60. /package/{module-plugin → modules}/credential.js +0 -0
  61. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  62. /package/{module-plugin → modules}/requester/basic.js +0 -0
  63. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  64. /package/{module-plugin → modules}/requester/requester.js +0 -0
  65. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  66. /package/{module-plugin → modules}/test/auther.test.js +0 -0
  67. /package/{module-plugin → modules}/test/mock-api/api.js +0 -0
  68. /package/{module-plugin → modules}/test/mock-api/definition.js +0 -0
  69. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -8,28 +8,37 @@ const { GetIntegrationsForUser } = require('./use-cases/get-integrations-for-use
8
8
  const { CredentialRepository } = require('../credential/credential-repository');
9
9
  const { GetCredentialForUser } = require('../credential/use-cases/get-credential-for-user');
10
10
  const { CreateIntegration } = require('./use-cases/create-integration');
11
- const { ModuleService } = require('../module-plugin/module-service');
12
- const { ModuleRepository } = require('../module-plugin/module-repository');
13
- const { GetEntitiesForUser } = require('../module-plugin/use-cases/get-entities-for-user');
11
+ const { ModuleFactory } = require('../modules/module-factory');
12
+ const { ModuleRepository } = require('../modules/module-repository');
13
+ const { GetEntitiesForUser } = require('../modules/use-cases/get-entities-for-user');
14
14
  const { loadAppDefinition } = require('../handlers/app-definition-loader');
15
15
  const { GetIntegrationInstance } = require('./use-cases/get-integration-instance');
16
16
  const { UpdateIntegration } = require('./use-cases/update-integration');
17
17
  const { getModulesDefinitionFromIntegrationClasses } = require('./utils/map-integration-dto');
18
-
19
- /**
20
- * Creates an Express router with integration and entity routes configured
21
- * @param {Object} params - Configuration parameters for the router
22
- * @param {express.Router} [params.router] - Optional Express router instance, creates new one if not provided
23
- * @param {import('../user/use-cases/get-user-from-bearer-token').GetUserFromBearerToken} params.getUserFromBearerToken - Use case for retrieving a user from a bearer token
24
- * @returns {express.Router} Configured Express router with integration and entity routes
25
- */
26
- function createIntegrationRouter(params) {
27
- const { integrations: integrationClasses } = loadAppDefinition();
18
+ const { GetModuleInstanceFromType } = require('../modules/use-cases/get-module-instance-from-type');
19
+ const { GetEntityOptionsByType } = require('../modules/use-cases/get-entity-options-by-type');
20
+ const { TestModuleAuth } = require('../modules/use-cases/test-module-auth');
21
+ const { GetModule } = require('../modules/use-cases/get-module');
22
+ const { GetEntityOptionsById } = require('../modules/use-cases/get-entity-options-by-id');
23
+ const { RefreshEntityOptions } = require('../modules/use-cases/refresh-entity-options');
24
+ const { GetPossibleIntegrations } = require('./use-cases/get-possible-integrations');
25
+ const { UserRepository } = require('../user/user-repository');
26
+ const { GetUserFromBearerToken } = require('../user/use-cases/get-user-from-bearer-token');
27
+ const { ProcessAuthorizationCallback } = require('../modules/use-cases/process-authorization-callback');
28
+
29
+ function createIntegrationRouter() {
30
+ const { integrations: integrationClasses, userConfig } = loadAppDefinition();
28
31
  const moduleRepository = new ModuleRepository();
29
32
  const integrationRepository = new IntegrationRepository();
30
33
  const credentialRepository = new CredentialRepository();
34
+ const userRepository = new UserRepository({ userConfig });
35
+
36
+ const getUserFromBearerToken = new GetUserFromBearerToken({
37
+ userRepository,
38
+ userConfig,
39
+ });
31
40
 
32
- const moduleService = new ModuleService({
41
+ const moduleFactory = new ModuleFactory({
33
42
  moduleRepository,
34
43
  moduleDefinitions: getModulesDefinitionFromIntegrationClasses(integrationClasses),
35
44
  });
@@ -41,16 +50,18 @@ function createIntegrationRouter(params) {
41
50
  const getIntegrationsForUser = new GetIntegrationsForUser({
42
51
  integrationRepository,
43
52
  integrationClasses,
44
- moduleService,
53
+ moduleFactory,
45
54
  moduleRepository,
46
55
  });
56
+
47
57
  const getCredentialForUser = new GetCredentialForUser({
48
58
  credentialRepository,
49
59
  });
60
+
50
61
  const createIntegration = new CreateIntegration({
51
62
  integrationRepository,
52
63
  integrationClasses,
53
- moduleService,
64
+ moduleFactory,
54
65
  });
55
66
 
56
67
  const getEntitiesForUser = new GetEntitiesForUser({
@@ -61,18 +72,54 @@ function createIntegrationRouter(params) {
61
72
  const getIntegrationInstance = new GetIntegrationInstance({
62
73
  integrationRepository,
63
74
  integrationClasses,
64
- moduleService,
75
+ moduleFactory,
65
76
  });
66
77
 
67
78
  const updateIntegration = new UpdateIntegration({
68
79
  integrationRepository,
69
80
  integrationClasses,
70
- moduleService,
81
+ moduleFactory,
82
+ });
83
+
84
+ const getModuleInstanceFromType = new GetModuleInstanceFromType({
85
+ moduleDefinitions: getModulesDefinitionFromIntegrationClasses(integrationClasses),
86
+ });
87
+
88
+ const getEntityOptionsByType = new GetEntityOptionsByType({
89
+ moduleDefinitions: getModulesDefinitionFromIntegrationClasses(integrationClasses),
90
+ });
91
+
92
+ const testModuleAuth = new TestModuleAuth({
93
+ moduleRepository,
94
+ moduleDefinitions: getModulesDefinitionFromIntegrationClasses(integrationClasses),
95
+ });
96
+
97
+ const getModule = new GetModule({
98
+ moduleRepository,
99
+ moduleDefinitions: getModulesDefinitionFromIntegrationClasses(integrationClasses),
100
+ });
101
+
102
+ const getEntityOptionsById = new GetEntityOptionsById({
103
+ moduleRepository,
104
+ moduleDefinitions: getModulesDefinitionFromIntegrationClasses(integrationClasses),
105
+ });
106
+
107
+ const refreshEntityOptions = new RefreshEntityOptions({
108
+ moduleRepository,
109
+ moduleDefinitions: getModulesDefinitionFromIntegrationClasses(integrationClasses),
71
110
  });
72
111
 
73
- const router = get(params, 'router', express());
74
- const factory = get(params, 'factory');
75
- const getUserFromBearerToken = get(params, 'getUserFromBearerToken');
112
+ const getPossibleIntegrations = new GetPossibleIntegrations({
113
+ integrationClasses,
114
+ });
115
+
116
+ const processAuthorizationCallback = new ProcessAuthorizationCallback({
117
+ moduleRepository,
118
+ credentialRepository,
119
+ moduleDefinitions: getModulesDefinitionFromIntegrationClasses(integrationClasses),
120
+ });
121
+
122
+ const router = express();
76
123
 
77
124
  setIntegrationRoutes(router, getUserFromBearerToken, {
78
125
  createIntegration,
@@ -81,9 +128,17 @@ function createIntegrationRouter(params) {
81
128
  getEntitiesForUser,
82
129
  getIntegrationInstance,
83
130
  updateIntegration,
131
+ getPossibleIntegrations,
84
132
  });
85
- setEntityRoutes(router, factory, getUserFromBearerToken, {
133
+ setEntityRoutes(router, getUserFromBearerToken, {
86
134
  getCredentialForUser,
135
+ getModuleInstanceFromType,
136
+ getEntityOptionsByType,
137
+ testModuleAuth,
138
+ getModule,
139
+ getEntityOptionsById,
140
+ refreshEntityOptions,
141
+ processAuthorizationCallback,
87
142
  });
88
143
  return router;
89
144
  }
@@ -124,6 +179,7 @@ function setIntegrationRoutes(router, getUserFromBearerToken, useCases) {
124
179
  getEntitiesForUser,
125
180
  getIntegrationInstance,
126
181
  updateIntegration,
182
+ getPossibleIntegrations,
127
183
  } = useCases;
128
184
  router.route('/api/integrations').get(
129
185
  catchAsyncError(async (req, res) => {
@@ -134,9 +190,7 @@ function setIntegrationRoutes(router, getUserFromBearerToken, useCases) {
134
190
  const integrations = await getIntegrationsForUser.execute(userId);
135
191
  const results = {
136
192
  entities: {
137
- options: integrations.map((integration) =>
138
- integration.options
139
- ),
193
+ options: await getPossibleIntegrations.execute(),
140
194
  authorized: await getEntitiesForUser.execute(userId),
141
195
  },
142
196
  integrations: integrations,
@@ -303,7 +357,7 @@ function setIntegrationRoutes(router, getUserFromBearerToken, useCases) {
303
357
  }
304
358
 
305
359
  const params = checkRequiredParams(req.params, ['integrationId']);
306
- const integration = await getIntegrationForUser.execute(params.integrationId, user.getId());
360
+ const integration = await getIntegrationInstance.execute(params.integrationId, user.getId());
307
361
 
308
362
  // We could perhaps augment router with dynamic options? Haven't decided yet, but here may be the place
309
363
 
@@ -348,25 +402,19 @@ function setIntegrationRoutes(router, getUserFromBearerToken, useCases) {
348
402
  /**
349
403
  * Sets up entity-related routes for the integration router
350
404
  * @param {Object} router - Express router instance
351
- * @param {Object} factory - Factory object containing moduleFactory
352
405
  * @param {import('../user/use-cases/get-user-from-bearer-token').GetUserFromBearerToken} getUserFromBearerToken - Use case for retrieving a user from a bearer token
353
406
  */
354
- function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
355
- const { moduleFactory } = factory;
356
- const { getCredentialForUser } = useCases;
357
- const getModuleInstance = async (userId, entityType) => {
358
- if (!moduleFactory.checkIsValidType(entityType)) {
359
- throw Boom.badRequest(
360
- `Error: Invalid entity type of ${entityType}, options are ${moduleFactory.moduleTypes.join(
361
- ', '
362
- )}`
363
- );
364
- }
365
- return await moduleFactory.getInstanceFromTypeName(
366
- entityType,
367
- userId
368
- );
369
- };
407
+ function setEntityRoutes(router, getUserFromBearerToken, useCases) {
408
+ const {
409
+ getCredentialForUser,
410
+ getModuleInstanceFromType,
411
+ getEntityOptionsByType,
412
+ testModuleAuth,
413
+ getModule,
414
+ getEntityOptionsById,
415
+ refreshEntityOptions,
416
+ processAuthorizationCallback,
417
+ } = useCases;
370
418
 
371
419
  router.route('/api/authorize').get(
372
420
  catchAsyncError(async (req, res) => {
@@ -375,16 +423,15 @@ function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
375
423
  );
376
424
  const userId = user.getId();
377
425
  const params = checkRequiredParams(req.query, ['entityType']);
378
- const module = await getModuleInstance(userId, params.entityType);
379
- const areRequirementsValid =
380
- module.validateAuthorizationRequirements();
426
+ const module = await getModuleInstanceFromType.execute(userId, params.entityType);
427
+ const areRequirementsValid = module.validateAuthorizationRequirements();
381
428
  if (!areRequirementsValid) {
382
429
  throw new Error(
383
430
  `Error: Entity of type ${params.entityType} requires a valid url`
384
431
  );
385
432
  }
386
433
 
387
- res.json(await module.getAuthorizationRequirements());
434
+ res.json(module.getAuthorizationRequirements());
388
435
  })
389
436
  );
390
437
 
@@ -398,14 +445,10 @@ function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
398
445
  'entityType',
399
446
  'data',
400
447
  ]);
401
- const module = await getModuleInstance(userId, params.entityType);
402
448
 
403
- res.json(
404
- await module.processAuthorizationCallback({
405
- userId,
406
- data: params.data,
407
- })
408
- );
449
+ const entityDetails = await processAuthorizationCallback.execute(userId, params.entityType, params.data);
450
+
451
+ res.json(entityDetails);
409
452
  })
410
453
  );
411
454
 
@@ -431,7 +474,7 @@ function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
431
474
  throw Boom.badRequest('Invalid credential ID');
432
475
  }
433
476
 
434
- const module = await getModuleInstance(userId, params.entityType);
477
+ const module = await getModuleInstanceFromType.execute(userId, params.entityType);
435
478
  const entityDetails = await module.getEntityDetails(
436
479
  module.api,
437
480
  null,
@@ -460,9 +503,9 @@ function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
460
503
  }
461
504
 
462
505
  const params = checkRequiredParams(req.query, ['entityType']);
463
- const module = await getModuleInstance(userId, params.entityType);
506
+ const entityOptions = await getEntityOptionsByType.execute(userId, params.entityType);
464
507
 
465
- res.json(await module.getEntityOptions());
508
+ res.json(entityOptions);
466
509
  })
467
510
  );
468
511
 
@@ -473,17 +516,11 @@ function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
473
516
  );
474
517
  const userId = user.getId();
475
518
  const params = checkRequiredParams(req.params, ['entityId']);
476
- const module = await moduleFactory.getModuleInstanceFromEntityId(
519
+ const testAuthResponse = await testModuleAuth.execute(
477
520
  params.entityId,
478
521
  userId
479
522
  );
480
523
 
481
- if (!module) {
482
- throw Boom.notFound();
483
- }
484
-
485
- const testAuthResponse = await module.testAuth();
486
-
487
524
  if (!testAuthResponse) {
488
525
  res.status(400);
489
526
  res.json({
@@ -508,16 +545,12 @@ function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
508
545
  );
509
546
  const userId = user.getId();
510
547
  const params = checkRequiredParams(req.params, ['entityId']);
511
- const module = await moduleFactory.getModuleInstanceFromEntityId(
548
+ const module = await getModule.execute(
512
549
  params.entityId,
513
550
  userId
514
551
  );
515
552
 
516
- if (!module) {
517
- throw Boom.notFound();
518
- }
519
-
520
- res.json(module.entity);
553
+ res.json(module);
521
554
  })
522
555
  );
523
556
 
@@ -530,16 +563,10 @@ function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
530
563
  const params = checkRequiredParams(req.params, [
531
564
  'entityId',
532
565
  ]);
533
- const module = await moduleFactory.getModuleInstanceFromEntityId(
534
- params.entityId,
535
- userId
536
- );
537
566
 
538
- if (!module) {
539
- throw Boom.notFound();
540
- }
567
+ const entityOptions = await getEntityOptionsById.execute(params.entityId, userId);
541
568
 
542
- res.json(await module.getEntityOptions());
569
+ res.json(entityOptions);
543
570
  })
544
571
  );
545
572
 
@@ -552,16 +579,13 @@ function setEntityRoutes(router, factory, getUserFromBearerToken, useCases) {
552
579
  const params = checkRequiredParams(req.params, [
553
580
  'entityId',
554
581
  ]);
555
- const module = await moduleFactory.getModuleInstanceFromEntityId(
582
+ const updatedOptions = await refreshEntityOptions.execute(
556
583
  params.entityId,
557
- userId
584
+ userId,
585
+ req.body
558
586
  );
559
587
 
560
- if (!module) {
561
- throw Boom.notFound();
562
- }
563
-
564
- res.json(await module.refreshEntityOptions(req.body));
588
+ res.json(updatedOptions);
565
589
  })
566
590
  );
567
591
  }
@@ -0,0 +1,90 @@
1
+ const { IntegrationBase } = require('../../integration-base');
2
+
3
+ class DummyModule {
4
+ static definition = {
5
+ getName: () => 'dummy'
6
+ };
7
+ }
8
+
9
+ class DummyIntegration extends IntegrationBase {
10
+ static Definition = {
11
+ name: 'dummy',
12
+ version: '1.0.0',
13
+ modules: {
14
+ dummy: DummyModule
15
+ },
16
+ display: {
17
+ label: 'Dummy Integration',
18
+ description: 'A dummy integration for testing',
19
+ detailsUrl: 'https://example.com',
20
+ icon: 'dummy-icon'
21
+ }
22
+ };
23
+
24
+ static getOptionDetails() {
25
+ return {
26
+ name: this.Definition.name,
27
+ version: this.Definition.version,
28
+ display: this.Definition.display
29
+ };
30
+ }
31
+
32
+ constructor(params) {
33
+ super(params);
34
+ this.sendSpy = jest.fn();
35
+ this.eventCallHistory = [];
36
+ this.events = {};
37
+
38
+ this.integrationRepository = {
39
+ updateIntegrationById: jest.fn().mockResolvedValue({}),
40
+ findIntegrationById: jest.fn().mockResolvedValue({}),
41
+ };
42
+
43
+ this.updateIntegrationStatus = {
44
+ execute: jest.fn().mockResolvedValue({})
45
+ };
46
+
47
+ this.updateIntegrationMessages = {
48
+ execute: jest.fn().mockResolvedValue({})
49
+ };
50
+
51
+ this.registerEventHandlers();
52
+ }
53
+
54
+ async loadDynamicUserActions() {
55
+ return {};
56
+ }
57
+
58
+ async registerEventHandlers() {
59
+ super.registerEventHandlers();
60
+ return;
61
+ }
62
+
63
+ async send(event, data) {
64
+ this.sendSpy(event, data);
65
+ this.eventCallHistory.push({ event, data, timestamp: Date.now() });
66
+ return super.send(event, data);
67
+ }
68
+
69
+ async initialize() {
70
+ return;
71
+ }
72
+
73
+ async onCreate({ integrationId }) {
74
+ return;
75
+ }
76
+
77
+ async onUpdate(params) {
78
+ return;
79
+ }
80
+
81
+ async onDelete(params) {
82
+ return;
83
+ }
84
+
85
+ getConfig() {
86
+ return this.config || {};
87
+ }
88
+ }
89
+
90
+ module.exports = { DummyIntegration };
@@ -0,0 +1,89 @@
1
+ const { v4: uuid } = require('uuid');
2
+
3
+ class TestIntegrationRepository {
4
+ constructor() {
5
+ this.store = new Map();
6
+ this.operationHistory = [];
7
+ }
8
+
9
+ async createIntegration(entities, userId, config) {
10
+ const id = uuid();
11
+ const record = {
12
+ id,
13
+ _id: id,
14
+ entitiesIds: entities,
15
+ userId: userId,
16
+ config,
17
+ version: '0.0.0',
18
+ status: 'NEW',
19
+ messages: {},
20
+ };
21
+ this.store.set(id, record);
22
+ this.operationHistory.push({ operation: 'create', id, userId, config });
23
+ return record;
24
+ }
25
+
26
+ async findIntegrationById(id) {
27
+ const rec = this.store.get(id);
28
+ this.operationHistory.push({ operation: 'findById', id, found: !!rec });
29
+ if (!rec) return null;
30
+ return rec;
31
+ }
32
+
33
+ async findIntegrationsByUserId(userId) {
34
+ const results = Array.from(this.store.values()).filter(r => r.userId === userId);
35
+ this.operationHistory.push({ operation: 'findByUserId', userId, count: results.length });
36
+ return results;
37
+ }
38
+
39
+ async updateIntegrationMessages(id, type, title, body, timestamp) {
40
+ const rec = this.store.get(id);
41
+ if (!rec) {
42
+ this.operationHistory.push({ operation: 'updateMessages', id, success: false });
43
+ return false;
44
+ }
45
+ if (!rec.messages[type]) rec.messages[type] = [];
46
+ rec.messages[type].push({ title, message: body, timestamp });
47
+ this.operationHistory.push({ operation: 'updateMessages', id, type, success: true });
48
+ return true;
49
+ }
50
+
51
+ async updateIntegrationConfig(id, config) {
52
+ const rec = this.store.get(id);
53
+ if (!rec) {
54
+ this.operationHistory.push({ operation: 'updateConfig', id, success: false });
55
+ return false;
56
+ }
57
+ rec.config = config;
58
+ this.operationHistory.push({ operation: 'updateConfig', id, success: true });
59
+ return true;
60
+ }
61
+
62
+ async deleteIntegrationById(id) {
63
+ const existed = this.store.has(id);
64
+ const result = this.store.delete(id);
65
+ this.operationHistory.push({ operation: 'delete', id, existed, success: result });
66
+ return result;
67
+ }
68
+
69
+ async updateIntegrationStatus(id, status) {
70
+ const rec = this.store.get(id);
71
+ if (rec) {
72
+ rec.status = status;
73
+ this.operationHistory.push({ operation: 'updateStatus', id, status, success: true });
74
+ } else {
75
+ this.operationHistory.push({ operation: 'updateStatus', id, status, success: false });
76
+ }
77
+ return !!rec;
78
+ }
79
+
80
+ getOperationHistory() {
81
+ return [...this.operationHistory];
82
+ }
83
+
84
+ clearHistory() {
85
+ this.operationHistory = [];
86
+ }
87
+ }
88
+
89
+ module.exports = { TestIntegrationRepository };
@@ -0,0 +1,124 @@
1
+ const { CreateIntegration } = require('../../use-cases/create-integration');
2
+ const { TestIntegrationRepository } = require('../doubles/test-integration-repository');
3
+ const { TestModuleFactory } = require('../../../modules/tests/doubles/test-module-factory');
4
+ const { DummyIntegration } = require('../doubles/dummy-integration-class');
5
+
6
+ describe('CreateIntegration Use-Case', () => {
7
+ let integrationRepository;
8
+ let moduleFactory;
9
+ let useCase;
10
+
11
+ beforeEach(() => {
12
+ integrationRepository = new TestIntegrationRepository();
13
+ moduleFactory = new TestModuleFactory();
14
+ useCase = new CreateIntegration({
15
+ integrationRepository,
16
+ integrationClasses: [DummyIntegration],
17
+ moduleFactory,
18
+ });
19
+ });
20
+
21
+ describe('happy path', () => {
22
+ it('creates an integration and returns DTO', async () => {
23
+ const entities = ['entity-1'];
24
+ const userId = 'user-1';
25
+ const config = { type: 'dummy', foo: 'bar' };
26
+
27
+ const dto = await useCase.execute(entities, userId, config);
28
+
29
+ expect(dto.id).toBeDefined();
30
+ expect(dto.config).toEqual(config);
31
+ expect(dto.userId).toBe(userId);
32
+ expect(dto.entities).toEqual(entities);
33
+ expect(dto.status).toBe('NEW');
34
+ });
35
+
36
+ it('triggers ON_CREATE event with correct payload', async () => {
37
+ const entities = ['entity-1'];
38
+ const userId = 'user-1';
39
+ const config = { type: 'dummy', foo: 'bar' };
40
+
41
+ const dto = await useCase.execute(entities, userId, config);
42
+
43
+ const record = await integrationRepository.findIntegrationById(dto.id);
44
+ expect(record).toBeTruthy();
45
+
46
+ const history = integrationRepository.getOperationHistory();
47
+ const createOperation = history.find(op => op.operation === 'create');
48
+ expect(createOperation).toEqual({
49
+ operation: 'create',
50
+ id: dto.id,
51
+ userId,
52
+ config
53
+ });
54
+ });
55
+
56
+ it('loads modules for each entity', async () => {
57
+ const entities = ['entity-1', 'entity-2'];
58
+ const userId = 'user-1';
59
+ const config = { type: 'dummy' };
60
+
61
+ const dto = await useCase.execute(entities, userId, config);
62
+
63
+ expect(dto.entities).toEqual(entities);
64
+ });
65
+ });
66
+
67
+ describe('error cases', () => {
68
+ it('throws error when integration class is not found', async () => {
69
+ const entities = ['entity-1'];
70
+ const userId = 'user-1';
71
+ const config = { type: 'unknown-type' };
72
+
73
+ await expect(useCase.execute(entities, userId, config))
74
+ .rejects
75
+ .toThrow('No integration class found for type: unknown-type');
76
+ });
77
+
78
+ it('throws error when no integration classes provided', async () => {
79
+ const useCaseWithoutClasses = new CreateIntegration({
80
+ integrationRepository,
81
+ integrationClasses: [],
82
+ moduleFactory,
83
+ });
84
+
85
+ const entities = ['entity-1'];
86
+ const userId = 'user-1';
87
+ const config = { type: 'dummy' };
88
+
89
+ await expect(useCaseWithoutClasses.execute(entities, userId, config))
90
+ .rejects
91
+ .toThrow('No integration class found for type: dummy');
92
+ });
93
+ });
94
+
95
+ describe('edge cases', () => {
96
+ it('handles empty entities array', async () => {
97
+ const entities = [];
98
+ const userId = 'user-1';
99
+ const config = { type: 'dummy' };
100
+
101
+ const dto = await useCase.execute(entities, userId, config);
102
+
103
+ expect(dto.entities).toEqual([]);
104
+ expect(dto.id).toBeDefined();
105
+ });
106
+
107
+ it('handles complex config objects', async () => {
108
+ const entities = ['entity-1'];
109
+ const userId = 'user-1';
110
+ const config = {
111
+ type: 'dummy',
112
+ nested: {
113
+ value: 123,
114
+ array: [1, 2, 3],
115
+ bool: true
116
+ }
117
+ };
118
+
119
+ const dto = await useCase.execute(entities, userId, config);
120
+
121
+ expect(dto.config).toEqual(config);
122
+ });
123
+ });
124
+ });