@friggframework/core 2.0.0--canary.398.53eac55.0 → 2.0.0--canary.397.878fefa.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 (97) hide show
  1. package/README.md +931 -50
  2. package/core/create-handler.js +1 -0
  3. package/credential/credential-repository.js +42 -0
  4. package/credential/use-cases/get-credential-for-user.js +21 -0
  5. package/credential/use-cases/update-authentication-status.js +15 -0
  6. package/database/models/WebsocketConnection.js +0 -5
  7. package/handlers/app-definition-loader.js +38 -0
  8. package/handlers/app-handler-helpers.js +0 -3
  9. package/handlers/backend-utils.js +35 -34
  10. package/handlers/routers/auth.js +3 -14
  11. package/handlers/routers/integration-defined-routers.js +8 -5
  12. package/handlers/routers/user.js +25 -5
  13. package/handlers/workers/integration-defined-workers.js +6 -3
  14. package/index.js +1 -16
  15. package/integrations/index.js +0 -5
  16. package/integrations/integration-base.js +42 -44
  17. package/integrations/integration-repository.js +67 -0
  18. package/integrations/integration-router.js +301 -178
  19. package/integrations/integration.js +233 -0
  20. package/integrations/options.js +1 -1
  21. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  22. package/integrations/tests/doubles/test-integration-repository.js +89 -0
  23. package/integrations/tests/use-cases/create-integration.test.js +124 -0
  24. package/integrations/tests/use-cases/delete-integration-for-user.test.js +143 -0
  25. package/integrations/tests/use-cases/get-integration-for-user.test.js +143 -0
  26. package/integrations/tests/use-cases/get-integration-instance.test.js +169 -0
  27. package/integrations/tests/use-cases/get-integrations-for-user.test.js +169 -0
  28. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  29. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  30. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  31. package/integrations/tests/use-cases/update-integration.test.js +134 -0
  32. package/integrations/use-cases/create-integration.js +72 -0
  33. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  34. package/integrations/use-cases/get-integration-for-user.js +79 -0
  35. package/integrations/use-cases/get-integration-instance.js +84 -0
  36. package/integrations/use-cases/get-integrations-for-user.js +77 -0
  37. package/integrations/use-cases/get-possible-integrations.js +27 -0
  38. package/integrations/use-cases/index.js +11 -0
  39. package/integrations/use-cases/update-integration-messages.js +31 -0
  40. package/integrations/use-cases/update-integration-status.js +28 -0
  41. package/integrations/use-cases/update-integration.js +92 -0
  42. package/integrations/utils/map-integration-dto.js +36 -0
  43. package/{module-plugin → modules}/index.js +0 -8
  44. package/modules/module-factory.js +54 -0
  45. package/modules/module-repository.js +107 -0
  46. package/modules/module.js +221 -0
  47. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  48. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  49. package/modules/tests/doubles/test-module-factory.js +16 -0
  50. package/modules/tests/doubles/test-module-repository.js +19 -0
  51. package/modules/use-cases/get-entities-for-user.js +32 -0
  52. package/modules/use-cases/get-entity-options-by-id.js +58 -0
  53. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  54. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  55. package/modules/use-cases/get-module.js +56 -0
  56. package/modules/use-cases/process-authorization-callback.js +114 -0
  57. package/modules/use-cases/refresh-entity-options.js +58 -0
  58. package/modules/use-cases/test-module-auth.js +54 -0
  59. package/modules/utils/map-module-dto.js +18 -0
  60. package/package.json +5 -5
  61. package/syncs/sync.js +0 -1
  62. package/types/integrations/index.d.ts +2 -6
  63. package/types/module-plugin/index.d.ts +4 -56
  64. package/types/syncs/index.d.ts +0 -2
  65. package/user/tests/doubles/test-user-repository.js +72 -0
  66. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  67. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  68. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  69. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  70. package/user/tests/use-cases/login-user.test.js +140 -0
  71. package/user/use-cases/create-individual-user.js +61 -0
  72. package/user/use-cases/create-organization-user.js +47 -0
  73. package/user/use-cases/create-token-for-user-id.js +30 -0
  74. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  75. package/user/use-cases/login-user.js +122 -0
  76. package/user/user-repository.js +62 -0
  77. package/user/user.js +77 -0
  78. package/handlers/routers/middleware/loadUser.js +0 -15
  79. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  80. package/integrations/create-frigg-backend.js +0 -31
  81. package/integrations/integration-factory.js +0 -251
  82. package/integrations/integration-user.js +0 -144
  83. package/integrations/test/integration-base.test.js +0 -144
  84. package/module-plugin/auther.js +0 -393
  85. package/module-plugin/entity-manager.js +0 -70
  86. package/module-plugin/manager.js +0 -169
  87. package/module-plugin/module-factory.js +0 -61
  88. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  89. /package/{module-plugin → modules}/credential.js +0 -0
  90. /package/{module-plugin → modules}/entity.js +0 -0
  91. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  92. /package/{module-plugin → modules}/requester/basic.js +0 -0
  93. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  94. /package/{module-plugin → modules}/requester/requester.js +0 -0
  95. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  96. /package/{module-plugin → modules}/test/auther.test.js +0 -0
  97. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,143 @@
1
+ const { GetIntegrationForUser } = require('../../use-cases/get-integration-for-user');
2
+ const { TestIntegrationRepository } = require('../doubles/test-integration-repository');
3
+ const { TestModuleFactory } = require('../../../modules/tests/doubles/test-module-factory');
4
+ const { TestModuleRepository } = require('../../../modules/tests/doubles/test-module-repository');
5
+ const { DummyIntegration } = require('../doubles/dummy-integration-class');
6
+
7
+ describe('GetIntegrationForUser Use-Case', () => {
8
+ let integrationRepository;
9
+ let moduleRepository;
10
+ let moduleFactory;
11
+ let useCase;
12
+
13
+ beforeEach(() => {
14
+ integrationRepository = new TestIntegrationRepository();
15
+ moduleRepository = new TestModuleRepository();
16
+ moduleFactory = new TestModuleFactory();
17
+ useCase = new GetIntegrationForUser({
18
+ integrationRepository,
19
+ integrationClasses: [DummyIntegration],
20
+ moduleFactory,
21
+ moduleRepository,
22
+ });
23
+ });
24
+
25
+ describe('happy path', () => {
26
+ it('returns integration dto', async () => {
27
+ const entity = { id: 'entity-1', _id: 'entity-1' };
28
+ moduleRepository.addEntity(entity);
29
+
30
+ const record = await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
31
+
32
+ const dto = await useCase.execute(record.id, 'user-1');
33
+ expect(dto.id).toBe(record.id);
34
+ expect(dto.userId).toBe('user-1');
35
+ expect(dto.config.type).toBe('dummy');
36
+ });
37
+
38
+ it('returns integration with multiple entities', async () => {
39
+ const entity1 = { id: 'entity-1', _id: 'entity-1' };
40
+ const entity2 = { id: 'entity-2', _id: 'entity-2' };
41
+ moduleRepository.addEntity(entity1);
42
+ moduleRepository.addEntity(entity2);
43
+
44
+ const record = await integrationRepository.createIntegration([entity1.id, entity2.id], 'user-1', { type: 'dummy' });
45
+
46
+ const dto = await useCase.execute(record.id, 'user-1');
47
+ expect(dto.entities).toEqual([entity1, entity2]);
48
+ });
49
+
50
+ it('returns integration with complex config', async () => {
51
+ const entity = { id: 'entity-1', _id: 'entity-1' };
52
+ moduleRepository.addEntity(entity);
53
+
54
+ const complexConfig = {
55
+ type: 'dummy',
56
+ settings: { api: { timeout: 5000 }, debug: true },
57
+ features: ['webhooks', 'sync']
58
+ };
59
+
60
+ const record = await integrationRepository.createIntegration([entity.id], 'user-1', complexConfig);
61
+
62
+ const dto = await useCase.execute(record.id, 'user-1');
63
+ expect(dto.config).toEqual(complexConfig);
64
+ });
65
+ });
66
+
67
+ describe('error cases', () => {
68
+ it('throws error when integration not found', async () => {
69
+ const nonExistentId = 'non-existent-id';
70
+
71
+ await expect(useCase.execute(nonExistentId, 'user-1'))
72
+ .rejects
73
+ .toThrow();
74
+ });
75
+
76
+ it('throws error when user does not own integration', async () => {
77
+ const entity = { id: 'entity-1', _id: 'entity-1' };
78
+ moduleRepository.addEntity(entity);
79
+
80
+ const record = await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
81
+
82
+ await expect(useCase.execute(record.id, 'different-user'))
83
+ .rejects
84
+ .toThrow();
85
+ });
86
+
87
+ it('throws error when integration class not found', async () => {
88
+ const useCaseWithoutClasses = new GetIntegrationForUser({
89
+ integrationRepository,
90
+ integrationClasses: [],
91
+ moduleFactory,
92
+ moduleRepository,
93
+ });
94
+
95
+ const entity = { id: 'entity-1', _id: 'entity-1' };
96
+ moduleRepository.addEntity(entity);
97
+
98
+ const record = await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
99
+
100
+ await expect(useCaseWithoutClasses.execute(record.id, 'user-1'))
101
+ .rejects
102
+ .toThrow();
103
+ });
104
+
105
+ it('handles missing entities gracefully', async () => {
106
+ const record = await integrationRepository.createIntegration(['missing-entity'], 'user-1', { type: 'dummy' });
107
+
108
+ await expect(useCase.execute(record.id, 'user-1'))
109
+ .rejects
110
+ .toThrow();
111
+ });
112
+ });
113
+
114
+ describe('edge cases', () => {
115
+ it('handles userId as string vs number comparison', async () => {
116
+ const entity = { id: 'entity-1', _id: 'entity-1' };
117
+ moduleRepository.addEntity(entity);
118
+
119
+ const record = await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
120
+
121
+ const dto1 = await useCase.execute(record.id, 'user-1');
122
+ const dto2 = await useCase.execute(record.id, 'user-1');
123
+
124
+ expect(dto1.userId).toBe(dto2.userId);
125
+ });
126
+
127
+ it('returns all integration properties', async () => {
128
+ const entity = { id: 'entity-1', _id: 'entity-1' };
129
+ moduleRepository.addEntity(entity);
130
+
131
+ const record = await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
132
+
133
+ record.status = 'ACTIVE';
134
+ record.version = '1.0.0';
135
+ record.messages = { info: [{ title: 'Test', message: 'Message' }] };
136
+
137
+ const dto = await useCase.execute(record.id, 'user-1');
138
+ expect(dto.status).toBe('ACTIVE');
139
+ expect(dto.version).toBe('1.0.0');
140
+ expect(dto.messages).toEqual({ info: [{ title: 'Test', message: 'Message' }] });
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,169 @@
1
+ const { GetIntegrationInstance } = require('../../use-cases/get-integration-instance');
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('GetIntegrationInstance 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 GetIntegrationInstance({
15
+ integrationRepository,
16
+ integrationClasses: [DummyIntegration],
17
+ moduleFactory,
18
+ });
19
+ });
20
+
21
+ describe('happy path', () => {
22
+ it('returns hydrated integration instance', async () => {
23
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'dummy' });
24
+
25
+ const instance = await useCase.execute(record.id, 'user-1');
26
+
27
+ expect(instance.id).toBe(record.id);
28
+ expect(instance.getConfig().type).toBe('dummy');
29
+ expect(instance.entities).toEqual(record.entitiesIds);
30
+ expect(instance.userId).toBe('user-1');
31
+ });
32
+
33
+ it('returns instance with multiple modules', async () => {
34
+ const record = await integrationRepository.createIntegration(['entity-1', 'entity-2'], 'user-1', { type: 'dummy' });
35
+
36
+ const instance = await useCase.execute(record.id, 'user-1');
37
+
38
+ expect(instance.entities).toEqual(['entity-1', 'entity-2']);
39
+ expect(Object.keys(instance.modules)).toHaveLength(1);
40
+ expect(instance.modules['stubModule']).toBeDefined();
41
+ });
42
+
43
+ it('initializes integration instance properly', async () => {
44
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'dummy' });
45
+
46
+ const instance = await useCase.execute(record.id, 'user-1');
47
+
48
+ expect(typeof instance.send).toBe('function');
49
+ expect(typeof instance.getConfig).toBe('function');
50
+ expect(typeof instance.initialize).toBe('function');
51
+ });
52
+
53
+ it('preserves all integration properties', async () => {
54
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'dummy', custom: 'value' });
55
+
56
+ record.status = 'ACTIVE';
57
+ record.version = '2.0.0';
58
+ record.messages = { logs: [{ title: 'Test', message: 'Log entry' }] };
59
+
60
+ const instance = await useCase.execute(record.id, 'user-1');
61
+
62
+ expect(instance.status).toBe('ACTIVE');
63
+ expect(instance.version).toBe('2.0.0');
64
+ expect(instance.messages).toEqual({ logs: [{ title: 'Test', message: 'Log entry' }] });
65
+ expect(instance.getConfig().custom).toBe('value');
66
+ });
67
+ });
68
+
69
+ describe('error cases', () => {
70
+ it('throws error when integration not found', async () => {
71
+ const nonExistentId = 'non-existent-id';
72
+
73
+ await expect(useCase.execute(nonExistentId, 'user-1'))
74
+ .rejects
75
+ .toThrow(`No integration found by the ID of ${nonExistentId}`);
76
+ });
77
+
78
+ it('throws error when user does not own integration', async () => {
79
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'dummy' });
80
+
81
+ await expect(useCase.execute(record.id, 'different-user'))
82
+ .rejects
83
+ .toThrow(`Integration ${record.id} does not belong to User different-user`);
84
+ });
85
+
86
+ it('throws error when integration class not found', async () => {
87
+ const useCaseWithoutClasses = new GetIntegrationInstance({
88
+ integrationRepository,
89
+ integrationClasses: [],
90
+ moduleFactory,
91
+ });
92
+
93
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'dummy' });
94
+
95
+ await expect(useCaseWithoutClasses.execute(record.id, 'user-1'))
96
+ .rejects
97
+ .toThrow('No integration class found for type: dummy');
98
+ });
99
+
100
+ it('throws error when integration has unknown type', async () => {
101
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'unknown-type' });
102
+
103
+ await expect(useCase.execute(record.id, 'user-1'))
104
+ .rejects
105
+ .toThrow('No integration class found for type: unknown-type');
106
+ });
107
+ });
108
+
109
+ describe('edge cases', () => {
110
+ it('handles integration with no entities', async () => {
111
+ const record = await integrationRepository.createIntegration([], 'user-1', { type: 'dummy' });
112
+
113
+ const instance = await useCase.execute(record.id, 'user-1');
114
+
115
+ expect(instance.entities).toEqual([]);
116
+ expect(Object.keys(instance.modules)).toHaveLength(0);
117
+ });
118
+
119
+ it('handles integration with null config values', async () => {
120
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'dummy', nullValue: null });
121
+
122
+ const instance = await useCase.execute(record.id, 'user-1');
123
+
124
+ expect(instance.getConfig().nullValue).toBeNull();
125
+ });
126
+
127
+ it('handles userId comparison edge cases', async () => {
128
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'dummy' });
129
+
130
+ const instance1 = await useCase.execute(record.id, 'user-1');
131
+ const instance2 = await useCase.execute(record.id, 'user-1');
132
+
133
+ expect(instance1.userId).toBe(instance2.userId);
134
+ });
135
+
136
+ it('returns fresh instance on each call', async () => {
137
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', { type: 'dummy' });
138
+
139
+ const instance1 = await useCase.execute(record.id, 'user-1');
140
+ const instance2 = await useCase.execute(record.id, 'user-1');
141
+
142
+ expect(instance1).not.toBe(instance2);
143
+ expect(instance1.id).toBe(instance2.id);
144
+ });
145
+
146
+ it('handles complex nested config structures', async () => {
147
+ const complexConfig = {
148
+ type: 'dummy',
149
+ settings: {
150
+ api: {
151
+ timeout: 5000,
152
+ retries: 3,
153
+ endpoints: ['users', 'orders']
154
+ },
155
+ features: {
156
+ webhooks: true,
157
+ sync: { interval: 300 }
158
+ }
159
+ }
160
+ };
161
+
162
+ const record = await integrationRepository.createIntegration(['entity-1'], 'user-1', complexConfig);
163
+
164
+ const instance = await useCase.execute(record.id, 'user-1');
165
+
166
+ expect(instance.getConfig()).toEqual(complexConfig);
167
+ });
168
+ });
169
+ });
@@ -0,0 +1,169 @@
1
+ const { GetIntegrationsForUser } = require('../../use-cases/get-integrations-for-user');
2
+ const { TestIntegrationRepository } = require('../doubles/test-integration-repository');
3
+ const { TestModuleFactory } = require('../../../modules/tests/doubles/test-module-factory');
4
+ const { TestModuleRepository } = require('../../../modules/tests/doubles/test-module-repository');
5
+ const { DummyIntegration } = require('../doubles/dummy-integration-class');
6
+
7
+ describe('GetIntegrationsForUser Use-Case', () => {
8
+ let integrationRepository;
9
+ let moduleRepository;
10
+ let moduleFactory;
11
+ let useCase;
12
+
13
+ beforeEach(() => {
14
+ integrationRepository = new TestIntegrationRepository();
15
+ moduleRepository = new TestModuleRepository();
16
+ moduleFactory = new TestModuleFactory();
17
+ useCase = new GetIntegrationsForUser({
18
+ integrationRepository,
19
+ integrationClasses: [DummyIntegration],
20
+ moduleFactory,
21
+ moduleRepository,
22
+ });
23
+ });
24
+
25
+ describe('happy path', () => {
26
+ it('returns integrations dto list for single user', async () => {
27
+ const entity = { id: 'entity-1' };
28
+ moduleRepository.addEntity(entity);
29
+
30
+ await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
31
+
32
+ const list = await useCase.execute('user-1');
33
+ expect(list.length).toBe(1);
34
+ expect(list[0].config.type).toBe('dummy');
35
+ expect(list[0].userId).toBe('user-1');
36
+ });
37
+
38
+ it('returns multiple integrations for same user', async () => {
39
+ const entity1 = { id: 'entity-1' };
40
+ const entity2 = { id: 'entity-2' };
41
+ moduleRepository.addEntity(entity1);
42
+ moduleRepository.addEntity(entity2);
43
+
44
+ await integrationRepository.createIntegration([entity1.id], 'user-1', { type: 'dummy', name: 'first' });
45
+ await integrationRepository.createIntegration([entity2.id], 'user-1', { type: 'dummy', name: 'second' });
46
+
47
+ const list = await useCase.execute('user-1');
48
+ expect(list.length).toBe(2);
49
+ expect(list[0].config.name).toBe('first');
50
+ expect(list[1].config.name).toBe('second');
51
+ });
52
+
53
+ it('filters integrations by user correctly', async () => {
54
+ const entity1 = { id: 'entity-1' };
55
+ const entity2 = { id: 'entity-2' };
56
+ moduleRepository.addEntity(entity1);
57
+ moduleRepository.addEntity(entity2);
58
+
59
+ await integrationRepository.createIntegration([entity1.id], 'user-1', { type: 'dummy', owner: 'user1' });
60
+ await integrationRepository.createIntegration([entity2.id], 'user-2', { type: 'dummy', owner: 'user2' });
61
+
62
+ const user1List = await useCase.execute('user-1');
63
+ const user2List = await useCase.execute('user-2');
64
+
65
+ expect(user1List.length).toBe(1);
66
+ expect(user2List.length).toBe(1);
67
+ expect(user1List[0].config.owner).toBe('user1');
68
+ expect(user2List[0].config.owner).toBe('user2');
69
+ });
70
+
71
+ it('returns empty array when user has no integrations', async () => {
72
+ const entity = { id: 'entity-1' };
73
+ moduleRepository.addEntity(entity);
74
+
75
+ await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
76
+
77
+ const list = await useCase.execute('user-2');
78
+ expect(list).toEqual([]);
79
+ });
80
+
81
+ it('tracks repository operations', async () => {
82
+ const entity = { id: 'entity-1' };
83
+ moduleRepository.addEntity(entity);
84
+ await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
85
+ integrationRepository.clearHistory();
86
+
87
+ await useCase.execute('user-1');
88
+
89
+ const history = integrationRepository.getOperationHistory();
90
+ const findOperation = history.find(op => op.operation === 'findByUserId');
91
+ expect(findOperation).toEqual({
92
+ operation: 'findByUserId',
93
+ userId: 'user-1',
94
+ count: 1
95
+ });
96
+ });
97
+ });
98
+
99
+ describe('error cases', () => {
100
+ it('throws error when integration class not found', async () => {
101
+ const useCaseWithoutClasses = new GetIntegrationsForUser({
102
+ integrationRepository,
103
+ integrationClasses: [],
104
+ moduleFactory,
105
+ moduleRepository,
106
+ });
107
+
108
+ const entity = { id: 'entity-1' };
109
+ moduleRepository.addEntity(entity);
110
+ await integrationRepository.createIntegration([entity.id], 'user-1', { type: 'dummy' });
111
+
112
+ await expect(useCaseWithoutClasses.execute('user-1'))
113
+ .rejects
114
+ .toThrow();
115
+ });
116
+
117
+ it('handles missing entities gracefully', async () => {
118
+ await integrationRepository.createIntegration(['missing-entity'], 'user-1', { type: 'dummy' });
119
+
120
+ await expect(useCase.execute('user-1'))
121
+ .rejects
122
+ .toThrow();
123
+ });
124
+ });
125
+
126
+ describe('edge cases', () => {
127
+ it('handles user with null/undefined userId', async () => {
128
+ const list1 = await useCase.execute(null);
129
+ const list2 = await useCase.execute(undefined);
130
+
131
+ expect(list1).toEqual([]);
132
+ expect(list2).toEqual([]);
133
+ });
134
+
135
+ it('handles integrations with complex configs', async () => {
136
+ const entity = { id: 'entity-1' };
137
+ moduleRepository.addEntity(entity);
138
+
139
+ const complexConfig = {
140
+ type: 'dummy',
141
+ settings: {
142
+ nested: { deep: 'value' },
143
+ array: [1, 2, 3],
144
+ boolean: true,
145
+ nullValue: null
146
+ }
147
+ };
148
+
149
+ await integrationRepository.createIntegration([entity.id], 'user-1', complexConfig);
150
+
151
+ const list = await useCase.execute('user-1');
152
+ expect(list[0].config).toEqual(complexConfig);
153
+ });
154
+
155
+ it('handles integrations with multiple entities', async () => {
156
+ const entity1 = { id: 'entity-1' };
157
+ const entity2 = { id: 'entity-2' };
158
+ const entity3 = { id: 'entity-3' };
159
+ moduleRepository.addEntity(entity1);
160
+ moduleRepository.addEntity(entity2);
161
+ moduleRepository.addEntity(entity3);
162
+
163
+ await integrationRepository.createIntegration([entity1.id, entity2.id, entity3.id], 'user-1', { type: 'dummy' });
164
+
165
+ const list = await useCase.execute('user-1');
166
+ expect(list[0].entities).toEqual([entity1, entity2, entity3]);
167
+ });
168
+ });
169
+ });
@@ -0,0 +1,188 @@
1
+ const { GetPossibleIntegrations } = require('../../use-cases/get-possible-integrations');
2
+ const { DummyIntegration } = require('../doubles/dummy-integration-class');
3
+
4
+ describe('GetPossibleIntegrations Use-Case', () => {
5
+ describe('happy path', () => {
6
+ it('returns option details array for single integration', async () => {
7
+ const useCase = new GetPossibleIntegrations({ integrationClasses: [DummyIntegration] });
8
+ const result = await useCase.execute();
9
+
10
+ expect(Array.isArray(result)).toBe(true);
11
+ expect(result.length).toBe(1);
12
+ expect(result[0].display).toBeDefined();
13
+ expect(result[0].display.label).toBe('Dummy Integration');
14
+ expect(result[0].display.description).toBe('A dummy integration for testing');
15
+ expect(result[0].name).toBe('dummy');
16
+ expect(result[0].version).toBe('1.0.0');
17
+ });
18
+
19
+ it('returns multiple integration options', async () => {
20
+ class AnotherDummyIntegration {
21
+ static Definition = {
22
+ name: 'another-dummy',
23
+ version: '2.0.0',
24
+ modules: { dummy: {} },
25
+ display: {
26
+ label: 'Another Dummy',
27
+ description: 'Another test integration',
28
+ detailsUrl: 'https://another.example.com',
29
+ icon: 'another-icon'
30
+ }
31
+ };
32
+
33
+ static getOptionDetails() {
34
+ return {
35
+ name: this.Definition.name,
36
+ version: this.Definition.version,
37
+ display: this.Definition.display
38
+ };
39
+ }
40
+ }
41
+
42
+ const useCase = new GetPossibleIntegrations({
43
+ integrationClasses: [DummyIntegration, AnotherDummyIntegration]
44
+ });
45
+ const result = await useCase.execute();
46
+
47
+ expect(result.length).toBe(2);
48
+ expect(result[0].name).toBe('dummy');
49
+ expect(result[1].name).toBe('another-dummy');
50
+ });
51
+
52
+ it('includes all required display properties', async () => {
53
+ const useCase = new GetPossibleIntegrations({ integrationClasses: [DummyIntegration] });
54
+ const result = await useCase.execute();
55
+
56
+ const integration = result[0];
57
+ expect(integration.display.label).toBeDefined();
58
+ expect(integration.display.description).toBeDefined();
59
+ expect(integration.display.detailsUrl).toBeDefined();
60
+ expect(integration.display.icon).toBeDefined();
61
+ });
62
+ });
63
+
64
+ describe('error cases', () => {
65
+ it('returns empty array when no integration classes provided', async () => {
66
+ const useCase = new GetPossibleIntegrations({ integrationClasses: [] });
67
+ const result = await useCase.execute();
68
+
69
+ expect(Array.isArray(result)).toBe(true);
70
+ expect(result.length).toBe(0);
71
+ });
72
+
73
+ it('handles integration class without getOptionDetails method', async () => {
74
+ class InvalidIntegration {
75
+ static Definition = { name: 'invalid' };
76
+ }
77
+
78
+ const useCase = new GetPossibleIntegrations({ integrationClasses: [InvalidIntegration] });
79
+
80
+ await expect(useCase.execute()).rejects.toThrow();
81
+ });
82
+
83
+ it('handles integration class with incomplete Definition', async () => {
84
+ class IncompleteIntegration {
85
+ static Definition = {
86
+ name: 'incomplete',
87
+ modules: { dummy: {} }
88
+ };
89
+
90
+ static getOptionDetails() {
91
+ return {
92
+ name: this.Definition.name,
93
+ version: this.Definition.version,
94
+ display: this.Definition.display
95
+ };
96
+ }
97
+ }
98
+
99
+ const useCase = new GetPossibleIntegrations({ integrationClasses: [IncompleteIntegration] });
100
+ const result = await useCase.execute();
101
+
102
+ expect(result.length).toBe(1);
103
+ expect(result[0].name).toBe('incomplete');
104
+ expect(result[0].display).toBeUndefined();
105
+ });
106
+ });
107
+
108
+ describe('edge cases', () => {
109
+ it('handles null integrationClasses parameter', async () => {
110
+ const useCase = new GetPossibleIntegrations({ integrationClasses: null });
111
+
112
+ await expect(useCase.execute()).rejects.toThrow();
113
+ });
114
+
115
+ it('handles undefined integrationClasses parameter', async () => {
116
+ const useCase = new GetPossibleIntegrations({ integrationClasses: undefined });
117
+
118
+ await expect(useCase.execute()).rejects.toThrow();
119
+ });
120
+
121
+ it('filters out null/undefined integration classes', async () => {
122
+ const useCase = new GetPossibleIntegrations({
123
+ integrationClasses: [DummyIntegration, null, undefined].filter(Boolean)
124
+ });
125
+ const result = await useCase.execute();
126
+
127
+ expect(result.length).toBe(1);
128
+ expect(result[0].name).toBe('dummy');
129
+ });
130
+
131
+ it('handles integration with complex display properties', async () => {
132
+ class ComplexIntegration {
133
+ static Definition = {
134
+ name: 'complex',
135
+ version: '3.0.0',
136
+ modules: { dummy: {} },
137
+ display: {
138
+ label: 'Complex Integration with Special Characters! 🚀',
139
+ description: 'A very long description that includes\nnewlines and\ttabs and special characters like émojis 🎉',
140
+ detailsUrl: 'https://complex.example.com/with/path?param=value&other=123',
141
+ icon: 'data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=',
142
+ category: 'Test & Development',
143
+ tags: ['testing', 'development', 'complex']
144
+ }
145
+ };
146
+
147
+ static getOptionDetails() {
148
+ return {
149
+ name: this.Definition.name,
150
+ version: this.Definition.version,
151
+ display: this.Definition.display
152
+ };
153
+ }
154
+ }
155
+
156
+ const useCase = new GetPossibleIntegrations({ integrationClasses: [ComplexIntegration] });
157
+ const result = await useCase.execute();
158
+
159
+ expect(result[0].display.label).toContain('🚀');
160
+ expect(result[0].display.description).toContain('🎉');
161
+ expect(result[0].display.detailsUrl).toContain('?param=value');
162
+ });
163
+
164
+ it('preserves integration class order', async () => {
165
+ class FirstIntegration {
166
+ static Definition = { name: 'first', version: '1.0.0', modules: { dummy: {} }, display: { label: 'First' } };
167
+ static getOptionDetails() { return { name: this.Definition.name, version: this.Definition.version, display: this.Definition.display }; }
168
+ }
169
+ class SecondIntegration {
170
+ static Definition = { name: 'second', version: '1.0.0', modules: { dummy: {} }, display: { label: 'Second' } };
171
+ static getOptionDetails() { return { name: this.Definition.name, version: this.Definition.version, display: this.Definition.display }; }
172
+ }
173
+ class ThirdIntegration {
174
+ static Definition = { name: 'third', version: '1.0.0', modules: { dummy: {} }, display: { label: 'Third' } };
175
+ static getOptionDetails() { return { name: this.Definition.name, version: this.Definition.version, display: this.Definition.display }; }
176
+ }
177
+
178
+ const useCase = new GetPossibleIntegrations({
179
+ integrationClasses: [FirstIntegration, SecondIntegration, ThirdIntegration]
180
+ });
181
+ const result = await useCase.execute();
182
+
183
+ expect(result[0].name).toBe('first');
184
+ expect(result[1].name).toBe('second');
185
+ expect(result[2].name).toBe('third');
186
+ });
187
+ });
188
+ });