@friggframework/core 2.0.0--canary.397.1b51778.0 → 2.0.0--canary.397.155fecd.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.
- package/README.md +933 -50
- package/core/create-handler.js +1 -0
- package/handlers/routers/auth.js +1 -15
- package/integrations/integration-router.js +11 -11
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +89 -0
- package/integrations/tests/use-cases/create-integration.test.js +124 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +143 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +143 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +169 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +169 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +134 -0
- package/integrations/use-cases/create-integration.js +19 -4
- package/integrations/use-cases/delete-integration-for-user.js +19 -0
- package/integrations/use-cases/get-integration-for-user.js +22 -6
- package/integrations/use-cases/get-integration-instance.js +14 -3
- package/integrations/use-cases/get-integrations-for-user.js +17 -3
- package/integrations/use-cases/get-possible-integrations.js +14 -0
- package/integrations/use-cases/update-integration-messages.js +17 -6
- package/integrations/use-cases/update-integration-status.js +16 -0
- package/integrations/use-cases/update-integration.js +17 -6
- package/modules/module-repository.js +2 -21
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +19 -0
- package/package.json +5 -5
- package/integrations/test/integration-base.test.js +0 -144
package/core/create-handler.js
CHANGED
|
@@ -34,6 +34,7 @@ const createHandler = (optionByName = {}) => {
|
|
|
34
34
|
// Helps mongoose reuse the connection. Lowers response times.
|
|
35
35
|
context.callbackWaitsForEmptyEventLoop = false;
|
|
36
36
|
|
|
37
|
+
// todo: this should not be necessary anymore
|
|
37
38
|
if (shouldUseDatabase) {
|
|
38
39
|
await connectToDatabase();
|
|
39
40
|
}
|
package/handlers/routers/auth.js
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
const { createIntegrationRouter } = require('@friggframework/core');
|
|
2
2
|
const { createAppHandler } = require('./../app-handler-helpers');
|
|
3
|
-
const {
|
|
4
|
-
loadAppDefinition,
|
|
5
|
-
} = require('../app-definition-loader');
|
|
6
|
-
const { UserRepository } = require('../../user/user-repository');
|
|
7
|
-
const { GetUserFromBearerToken } = require('../../user/use-cases/get-user-from-bearer-token');
|
|
8
3
|
|
|
9
|
-
const
|
|
10
|
-
const userRepository = new UserRepository({ userConfig });
|
|
11
|
-
const getUserFromBearerToken = new GetUserFromBearerToken({
|
|
12
|
-
userRepository,
|
|
13
|
-
userConfig,
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const router = createIntegrationRouter({
|
|
17
|
-
getUserFromBearerToken,
|
|
18
|
-
});
|
|
4
|
+
const router = createIntegrationRouter();
|
|
19
5
|
|
|
20
6
|
router.route('/redirect/:appId').get((req, res) => {
|
|
21
7
|
res.redirect(
|
|
@@ -22,19 +22,20 @@ const { GetModule } = require('../modules/use-cases/get-module');
|
|
|
22
22
|
const { GetEntityOptionsById } = require('../modules/use-cases/get-entity-options-by-id');
|
|
23
23
|
const { RefreshEntityOptions } = require('../modules/use-cases/refresh-entity-options');
|
|
24
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');
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* @param {Object} params - Configuration parameters for the router
|
|
29
|
-
* @param {express.Router} [params.router] - Optional Express router instance, creates new one if not provided
|
|
30
|
-
* @param {import('../user/use-cases/get-user-from-bearer-token').GetUserFromBearerToken} params.getUserFromBearerToken - Use case for retrieving a user from a bearer token
|
|
31
|
-
* @returns {express.Router} Configured Express router with integration and entity routes
|
|
32
|
-
*/
|
|
33
|
-
function createIntegrationRouter(params) {
|
|
34
|
-
const { integrations: integrationClasses } = loadAppDefinition();
|
|
28
|
+
function createIntegrationRouter() {
|
|
29
|
+
const { integrations: integrationClasses, userConfig } = loadAppDefinition();
|
|
35
30
|
const moduleRepository = new ModuleRepository();
|
|
36
31
|
const integrationRepository = new IntegrationRepository();
|
|
37
32
|
const credentialRepository = new CredentialRepository();
|
|
33
|
+
const userRepository = new UserRepository({ userConfig });
|
|
34
|
+
|
|
35
|
+
const getUserFromBearerToken = new GetUserFromBearerToken({
|
|
36
|
+
userRepository,
|
|
37
|
+
userConfig,
|
|
38
|
+
});
|
|
38
39
|
|
|
39
40
|
const moduleFactory = new ModuleFactory({
|
|
40
41
|
moduleRepository,
|
|
@@ -111,8 +112,7 @@ function createIntegrationRouter(params) {
|
|
|
111
112
|
integrationClasses,
|
|
112
113
|
});
|
|
113
114
|
|
|
114
|
-
const router =
|
|
115
|
-
const getUserFromBearerToken = get(params, 'getUserFromBearerToken');
|
|
115
|
+
const router = express();
|
|
116
116
|
|
|
117
117
|
setIntegrationRoutes(router, getUserFromBearerToken, {
|
|
118
118
|
createIntegration,
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const { DeleteIntegrationForUser } = require('../../use-cases/delete-integration-for-user');
|
|
2
|
+
const { TestIntegrationRepository } = require('../doubles/test-integration-repository');
|
|
3
|
+
const { DummyIntegration } = require('../doubles/dummy-integration-class');
|
|
4
|
+
|
|
5
|
+
describe('DeleteIntegrationForUser Use-Case', () => {
|
|
6
|
+
let integrationRepository;
|
|
7
|
+
let useCase;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
integrationRepository = new TestIntegrationRepository();
|
|
11
|
+
useCase = new DeleteIntegrationForUser({
|
|
12
|
+
integrationRepository,
|
|
13
|
+
integrationClasses: [DummyIntegration],
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('happy path', () => {
|
|
18
|
+
it('deletes integration successfully', async () => {
|
|
19
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
20
|
+
|
|
21
|
+
await useCase.execute(record.id, 'user-1');
|
|
22
|
+
|
|
23
|
+
const found = await integrationRepository.findIntegrationById(record.id);
|
|
24
|
+
expect(found).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('tracks delete operation', async () => {
|
|
28
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
29
|
+
integrationRepository.clearHistory();
|
|
30
|
+
|
|
31
|
+
await useCase.execute(record.id, 'user-1');
|
|
32
|
+
|
|
33
|
+
const history = integrationRepository.getOperationHistory();
|
|
34
|
+
const deleteOperation = history.find(op => op.operation === 'delete');
|
|
35
|
+
expect(deleteOperation).toEqual({
|
|
36
|
+
operation: 'delete',
|
|
37
|
+
id: record.id,
|
|
38
|
+
existed: true,
|
|
39
|
+
success: true
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('deletes integration with multiple entities', async () => {
|
|
44
|
+
const record = await integrationRepository.createIntegration(['e1', 'e2', 'e3'], 'user-1', { type: 'dummy' });
|
|
45
|
+
|
|
46
|
+
await useCase.execute(record.id, 'user-1');
|
|
47
|
+
|
|
48
|
+
const found = await integrationRepository.findIntegrationById(record.id);
|
|
49
|
+
expect(found).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('error cases', () => {
|
|
54
|
+
it('throws error when integration not found', async () => {
|
|
55
|
+
const nonExistentId = 'non-existent-id';
|
|
56
|
+
|
|
57
|
+
await expect(useCase.execute(nonExistentId, 'user-1'))
|
|
58
|
+
.rejects
|
|
59
|
+
.toThrow(`Integration with id of ${nonExistentId} does not exist`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws error when user does not own integration', async () => {
|
|
63
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
64
|
+
|
|
65
|
+
await expect(useCase.execute(record.id, 'different-user'))
|
|
66
|
+
.rejects
|
|
67
|
+
.toThrow(`Integration ${record.id} does not belong to User different-user`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('throws error when integration class not found', async () => {
|
|
71
|
+
const useCaseWithoutClasses = new DeleteIntegrationForUser({
|
|
72
|
+
integrationRepository,
|
|
73
|
+
integrationClasses: [],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
77
|
+
|
|
78
|
+
await expect(useCaseWithoutClasses.execute(record.id, 'user-1'))
|
|
79
|
+
.rejects
|
|
80
|
+
.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('tracks failed delete operation for non-existent integration', async () => {
|
|
84
|
+
const nonExistentId = 'non-existent-id';
|
|
85
|
+
integrationRepository.clearHistory();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await useCase.execute(nonExistentId, 'user-1');
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const history = integrationRepository.getOperationHistory();
|
|
91
|
+
const findOperation = history.find(op => op.operation === 'findById');
|
|
92
|
+
expect(findOperation).toEqual({
|
|
93
|
+
operation: 'findById',
|
|
94
|
+
id: nonExistentId,
|
|
95
|
+
found: false
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('edge cases', () => {
|
|
102
|
+
it('handles deletion of already deleted integration', async () => {
|
|
103
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
104
|
+
|
|
105
|
+
await useCase.execute(record.id, 'user-1');
|
|
106
|
+
|
|
107
|
+
await expect(useCase.execute(record.id, 'user-1'))
|
|
108
|
+
.rejects
|
|
109
|
+
.toThrow(`Integration with id of ${record.id} does not exist`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('handles integration with complex config during deletion', async () => {
|
|
113
|
+
const complexConfig = {
|
|
114
|
+
type: 'dummy',
|
|
115
|
+
settings: { nested: { deep: 'value' } },
|
|
116
|
+
credentials: { encrypted: true }
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', complexConfig);
|
|
120
|
+
|
|
121
|
+
await useCase.execute(record.id, 'user-1');
|
|
122
|
+
|
|
123
|
+
const found = await integrationRepository.findIntegrationById(record.id);
|
|
124
|
+
expect(found).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('handles null userId gracefully', async () => {
|
|
128
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
129
|
+
|
|
130
|
+
await expect(useCase.execute(record.id, null))
|
|
131
|
+
.rejects
|
|
132
|
+
.toThrow(`Integration ${record.id} does not belong to User null`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('handles undefined userId gracefully', async () => {
|
|
136
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
137
|
+
|
|
138
|
+
await expect(useCase.execute(record.id, undefined))
|
|
139
|
+
.rejects
|
|
140
|
+
.toThrow(`Integration ${record.id} does not belong to User undefined`);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -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
|
+
});
|