@friggframework/core 2.0.0-next.45 → 2.0.0-next.46
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 +28 -0
- package/application/commands/integration-commands.js +19 -0
- package/core/Worker.js +8 -21
- package/credential/repositories/credential-repository-mongo.js +14 -8
- package/credential/repositories/credential-repository-postgres.js +14 -8
- package/credential/repositories/credential-repository.js +3 -8
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +11 -2
- package/database/models/WebsocketConnection.js +11 -10
- package/database/prisma.js +63 -3
- package/database/repositories/health-check-repository-mongodb.js +3 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +3 -2
- package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
- package/database/use-cases/get-migration-status-use-case.js +93 -0
- package/database/use-cases/run-database-migration-use-case.js +137 -0
- package/database/use-cases/trigger-database-migration-use-case.js +157 -0
- package/database/utils/mongodb-collection-utils.js +91 -0
- package/database/utils/mongodb-schema-init.js +106 -0
- package/database/utils/prisma-runner.js +400 -0
- package/database/utils/prisma-schema-parser.js +182 -0
- package/encrypt/Cryptor.js +14 -16
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +4 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +4 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +334 -0
- package/generated/prisma-mongodb/index-browser.js +316 -0
- package/generated/prisma-mongodb/index.d.ts +22897 -0
- package/generated/prisma-mongodb/index.js +359 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
- package/generated/prisma-mongodb/runtime/binary.js +289 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
- package/generated/prisma-mongodb/runtime/edge.js +34 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
- package/generated/prisma-mongodb/runtime/react-native.js +83 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-mongodb/schema.prisma +362 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +341 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +4 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +4 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +356 -0
- package/generated/prisma-postgresql/index-browser.js +338 -0
- package/generated/prisma-postgresql/index.d.ts +25071 -0
- package/generated/prisma-postgresql/index.js +381 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
- package/generated/prisma-postgresql/runtime/binary.js +289 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
- package/generated/prisma-postgresql/runtime/edge.js +34 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
- package/generated/prisma-postgresql/runtime/react-native.js +83 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-postgresql/schema.prisma +345 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +363 -0
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/routers/auth.js +1 -1
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +256 -0
- package/handlers/routers/health.js +41 -6
- package/handlers/routers/integration-webhook-routers.js +2 -2
- package/handlers/use-cases/check-integrations-health-use-case.js +22 -10
- package/handlers/workers/db-migration.js +352 -0
- package/index.js +12 -0
- package/integrations/integration-router.js +60 -70
- package/integrations/repositories/integration-repository-interface.js +12 -0
- package/integrations/repositories/integration-repository-mongo.js +32 -0
- package/integrations/repositories/integration-repository-postgres.js +33 -0
- package/integrations/repositories/process-repository-postgres.js +2 -2
- package/integrations/tests/doubles/test-integration-repository.js +2 -2
- package/logs/logger.js +0 -4
- package/modules/entity.js +0 -1
- package/modules/repositories/module-repository-mongo.js +3 -12
- package/modules/repositories/module-repository-postgres.js +0 -11
- package/modules/repositories/module-repository.js +1 -12
- package/modules/use-cases/get-entity-options-by-id.js +1 -1
- package/modules/use-cases/get-module.js +1 -2
- package/modules/use-cases/refresh-entity-options.js +1 -1
- package/modules/use-cases/test-module-auth.js +1 -1
- package/package.json +82 -66
- package/prisma-mongodb/schema.prisma +21 -21
- package/prisma-postgresql/schema.prisma +15 -15
- package/queues/queuer-util.js +24 -21
- package/types/core/index.d.ts +2 -2
- package/types/module-plugin/index.d.ts +0 -2
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
- package/user/user.js +16 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +11 -10
- package/websocket/repositories/websocket-connection-repository-postgres.js +11 -10
- package/websocket/repositories/websocket-connection-repository.js +11 -10
- package/application/commands/integration-commands.test.js +0 -123
- package/database/encryption/encryption-integration.test.js +0 -553
- package/database/encryption/encryption-schema-registry.test.js +0 -392
- package/database/encryption/field-encryption-service.test.js +0 -525
- package/database/encryption/mongo-decryption-fix-verification.test.js +0 -348
- package/database/encryption/postgres-decryption-fix-verification.test.js +0 -371
- package/database/encryption/postgres-relation-decryption.test.js +0 -245
- package/database/encryption/prisma-encryption-extension.test.js +0 -439
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/handlers/auth-flow.integration.test.js +0 -147
- package/handlers/integration-event-dispatcher.test.js +0 -209
- package/handlers/routers/health.test.js +0 -210
- package/handlers/routers/integration-webhook-routers.test.js +0 -126
- package/handlers/webhook-flow.integration.test.js +0 -356
- package/handlers/workers/integration-defined-workers.test.js +0 -184
- package/integrations/tests/use-cases/create-integration.test.js +0 -131
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -150
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +0 -92
- package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -150
- package/integrations/tests/use-cases/get-integration-instance.test.js +0 -176
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -176
- package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
- package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
- package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
- package/integrations/tests/use-cases/update-integration.test.js +0 -141
- package/integrations/use-cases/create-process.test.js +0 -178
- package/integrations/use-cases/get-process.test.js +0 -190
- package/integrations/use-cases/load-integration-context-full.test.js +0 -329
- package/integrations/use-cases/load-integration-context.test.js +0 -114
- package/integrations/use-cases/update-process-metrics.test.js +0 -308
- package/integrations/use-cases/update-process-state.test.js +0 -256
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/modules/module-hydration.test.js +0 -205
- package/modules/requester/requester.test.js +0 -28
- package/user/tests/use-cases/create-individual-user.test.js +0 -24
- package/user/tests/use-cases/create-organization-user.test.js +0 -28
- package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
- package/user/tests/use-cases/login-user.test.js +0 -220
- package/user/tests/user-password-encryption-isolation.test.js +0 -237
- package/user/tests/user-password-hashing.test.js +0 -235
|
@@ -1,147 +0,0 @@
|
|
|
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 { IntegrationEventDispatcher } = require('./integration-event-dispatcher');
|
|
9
|
-
const { IntegrationBase } = require('../integrations/integration-base');
|
|
10
|
-
|
|
11
|
-
class SimulatedAsanaIntegration extends IntegrationBase {
|
|
12
|
-
static Definition = {
|
|
13
|
-
name: 'asana',
|
|
14
|
-
version: '1.0.0',
|
|
15
|
-
modules: {},
|
|
16
|
-
routes: [
|
|
17
|
-
{ path: '/auth', method: 'GET', event: 'AUTH_REQUEST' },
|
|
18
|
-
{ path: '/auth/redirect/:provider', method: 'GET', event: 'AUTH_REDIRECT' },
|
|
19
|
-
{ path: '/form', method: 'GET', event: 'LOAD_FORM' },
|
|
20
|
-
],
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
constructor(params = {}) {
|
|
24
|
-
super(params);
|
|
25
|
-
this.events = {
|
|
26
|
-
AUTH_REQUEST: { handler: this.authRequest.bind(this) },
|
|
27
|
-
AUTH_REDIRECT: { handler: this.authRedirect.bind(this) },
|
|
28
|
-
LOAD_FORM: { handler: this.loadForm.bind(this) },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async authRequest() {
|
|
33
|
-
return {
|
|
34
|
-
success: true,
|
|
35
|
-
action: 'redirect',
|
|
36
|
-
hydrated: this.isHydrated,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async authRedirect({ req }) {
|
|
41
|
-
const { code } = req.query || {};
|
|
42
|
-
return {
|
|
43
|
-
success: true,
|
|
44
|
-
action: 'tokens_received',
|
|
45
|
-
receivedCode: code,
|
|
46
|
-
hydrated: this.isHydrated,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async loadForm() {
|
|
51
|
-
if (!this.isHydrated && SimulatedAsanaIntegration.testRecord) {
|
|
52
|
-
this.setIntegrationRecord({
|
|
53
|
-
record: SimulatedAsanaIntegration.testRecord.record,
|
|
54
|
-
modules: SimulatedAsanaIntegration.testRecord.modules,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
this.assertHydrated('Integration not found - must authenticate first');
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
success: true,
|
|
62
|
-
form: {
|
|
63
|
-
fields: ['field1', 'field2'],
|
|
64
|
-
},
|
|
65
|
-
integrationId: this.id,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe('IntegrationEventDispatcher auth flow', () => {
|
|
71
|
-
const createDispatcher = () =>
|
|
72
|
-
new IntegrationEventDispatcher(new SimulatedAsanaIntegration());
|
|
73
|
-
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
SimulatedAsanaIntegration.testRecord = null;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('handles auth request without hydration', async () => {
|
|
79
|
-
const dispatcher = createDispatcher();
|
|
80
|
-
const result = await dispatcher.dispatchHttp({
|
|
81
|
-
event: 'AUTH_REQUEST',
|
|
82
|
-
req: { params: { provider: 'asana' }, query: {} },
|
|
83
|
-
res: {},
|
|
84
|
-
next: jest.fn(),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
expect(result).toEqual({ success: true, action: 'redirect', hydrated: false });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('handles auth redirect without hydration', async () => {
|
|
91
|
-
const dispatcher = createDispatcher();
|
|
92
|
-
const result = await dispatcher.dispatchHttp({
|
|
93
|
-
event: 'AUTH_REDIRECT',
|
|
94
|
-
req: { params: { provider: 'asana' }, query: { code: 'abc123' } },
|
|
95
|
-
res: {},
|
|
96
|
-
next: jest.fn(),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
expect(result).toEqual({
|
|
100
|
-
success: true,
|
|
101
|
-
action: 'tokens_received',
|
|
102
|
-
receivedCode: 'abc123',
|
|
103
|
-
hydrated: false,
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('throws for protected routes when no record is loaded', async () => {
|
|
108
|
-
const dispatcher = createDispatcher();
|
|
109
|
-
await expect(
|
|
110
|
-
dispatcher.dispatchHttp({
|
|
111
|
-
event: 'LOAD_FORM',
|
|
112
|
-
req: { query: {} },
|
|
113
|
-
res: {},
|
|
114
|
-
next: jest.fn(),
|
|
115
|
-
})
|
|
116
|
-
).rejects.toThrow('Integration not found - must authenticate first');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('allows handlers to hydrate explicitly before continuing', async () => {
|
|
120
|
-
SimulatedAsanaIntegration.testRecord = {
|
|
121
|
-
record: {
|
|
122
|
-
id: 'integration-123',
|
|
123
|
-
userId: 'user-456',
|
|
124
|
-
config: { type: 'asana' },
|
|
125
|
-
status: 'ENABLED',
|
|
126
|
-
version: '1.0.0',
|
|
127
|
-
messages: { errors: [], warnings: [] },
|
|
128
|
-
entities: [],
|
|
129
|
-
},
|
|
130
|
-
modules: [],
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const dispatcher = createDispatcher();
|
|
134
|
-
const result = await dispatcher.dispatchHttp({
|
|
135
|
-
event: 'LOAD_FORM',
|
|
136
|
-
req: { query: {} },
|
|
137
|
-
res: {},
|
|
138
|
-
next: jest.fn(),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
expect(result).toEqual({
|
|
142
|
-
success: true,
|
|
143
|
-
form: { fields: ['field1', 'field2'] },
|
|
144
|
-
integrationId: 'integration-123',
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
});
|
|
@@ -1,209 +0,0 @@
|
|
|
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 { IntegrationEventDispatcher } = require('./integration-event-dispatcher');
|
|
9
|
-
const { IntegrationBase } = require('../integrations/integration-base');
|
|
10
|
-
|
|
11
|
-
class TestIntegration extends IntegrationBase {
|
|
12
|
-
static Definition = {
|
|
13
|
-
name: 'test-integration',
|
|
14
|
-
version: '1.0.0',
|
|
15
|
-
modules: {},
|
|
16
|
-
routes: [
|
|
17
|
-
{ path: '/auth', method: 'GET', event: 'AUTH_REQUEST' },
|
|
18
|
-
{ path: '/data', method: 'GET', event: 'LOAD_DATA' },
|
|
19
|
-
{ path: '/job', method: 'POST', event: 'TEST_EVENT' },
|
|
20
|
-
{ path: '/dynamic', method: 'GET', event: 'DYNAMIC_EVENT' },
|
|
21
|
-
],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
constructor(params) {
|
|
25
|
-
super(params);
|
|
26
|
-
this.events = {
|
|
27
|
-
AUTH_REQUEST: { handler: this.authRequest.bind(this) },
|
|
28
|
-
LOAD_DATA: { handler: this.loadData.bind(this) },
|
|
29
|
-
TEST_EVENT: { handler: this.testHandler.bind(this) },
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async authRequest() {
|
|
34
|
-
TestIntegration.latestInstance = this;
|
|
35
|
-
return {
|
|
36
|
-
success: true,
|
|
37
|
-
hydrated: this.isHydrated,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async loadData() {
|
|
42
|
-
this.assertHydrated('loadData requires hydration');
|
|
43
|
-
return { success: true };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async testHandler({ data }) {
|
|
47
|
-
TestIntegration.latestInstance = this;
|
|
48
|
-
return { received: data };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async initialize() {
|
|
52
|
-
this.events = {
|
|
53
|
-
...this.events,
|
|
54
|
-
DYNAMIC_EVENT: { handler: this.dynamicHandler.bind(this) },
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async dynamicHandler() {
|
|
59
|
-
TestIntegration.latestInstance = this;
|
|
60
|
-
return { dynamic: true };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe('IntegrationEventDispatcher', () => {
|
|
65
|
-
const createDispatcher = () =>
|
|
66
|
-
new IntegrationEventDispatcher(new TestIntegration());
|
|
67
|
-
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
TestIntegration.latestInstance = null;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('dispatchHttp', () => {
|
|
73
|
-
it('creates a stateless integration instance for HTTP events', async () => {
|
|
74
|
-
const dispatcher = createDispatcher();
|
|
75
|
-
const result = await dispatcher.dispatchHttp({
|
|
76
|
-
event: 'AUTH_REQUEST',
|
|
77
|
-
req: {},
|
|
78
|
-
res: {},
|
|
79
|
-
next: jest.fn(),
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
expect(result).toEqual({ success: true, hydrated: false });
|
|
83
|
-
expect(TestIntegration.latestInstance).toBeInstanceOf(TestIntegration);
|
|
84
|
-
expect(TestIntegration.latestInstance.isHydrated).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('calls initialize to register dynamic events', async () => {
|
|
88
|
-
const dispatcher = createDispatcher();
|
|
89
|
-
await dispatcher.integrationInstance.initialize();
|
|
90
|
-
const result = await dispatcher.dispatchHttp({
|
|
91
|
-
event: 'DYNAMIC_EVENT',
|
|
92
|
-
req: {},
|
|
93
|
-
res: {},
|
|
94
|
-
next: jest.fn(),
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
expect(result).toEqual({ dynamic: true });
|
|
98
|
-
expect(TestIntegration.latestInstance).toBeInstanceOf(TestIntegration);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('throws when requesting an unknown event', async () => {
|
|
102
|
-
const dispatcher = createDispatcher();
|
|
103
|
-
await expect(
|
|
104
|
-
dispatcher.dispatchHttp({
|
|
105
|
-
event: 'UNKNOWN',
|
|
106
|
-
req: {},
|
|
107
|
-
res: {},
|
|
108
|
-
next: jest.fn(),
|
|
109
|
-
})
|
|
110
|
-
).rejects.toThrow('Event UNKNOWN not registered for test-integration');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('does not hydrate automatically for handlers that require data', async () => {
|
|
114
|
-
const dispatcher = createDispatcher();
|
|
115
|
-
await expect(
|
|
116
|
-
dispatcher.dispatchHttp({
|
|
117
|
-
event: 'LOAD_DATA',
|
|
118
|
-
req: {},
|
|
119
|
-
res: {},
|
|
120
|
-
next: jest.fn(),
|
|
121
|
-
})
|
|
122
|
-
).rejects.toThrow('loadData requires hydration');
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe('dispatchJob', () => {
|
|
127
|
-
it('creates a stateless integration instance for job events', async () => {
|
|
128
|
-
const payload = { foo: 'bar' };
|
|
129
|
-
const dispatcher = createDispatcher();
|
|
130
|
-
const result = await dispatcher.dispatchJob({
|
|
131
|
-
event: 'TEST_EVENT',
|
|
132
|
-
data: payload,
|
|
133
|
-
context: {},
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
expect(result).toEqual({ received: payload });
|
|
137
|
-
expect(TestIntegration.latestInstance).toBeInstanceOf(TestIntegration);
|
|
138
|
-
expect(TestIntegration.latestInstance.isHydrated).toBe(false);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('Webhook Events', () => {
|
|
143
|
-
it('should dispatch WEBHOOK_RECEIVED without hydration', async () => {
|
|
144
|
-
const integration = new TestIntegration();
|
|
145
|
-
integration.events.WEBHOOK_RECEIVED = {
|
|
146
|
-
handler: jest.fn().mockResolvedValue({ received: true })
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const dispatcher = new IntegrationEventDispatcher(integration);
|
|
150
|
-
const req = { body: { test: 'data' }, params: {} };
|
|
151
|
-
const res = {};
|
|
152
|
-
|
|
153
|
-
await dispatcher.dispatchHttp({
|
|
154
|
-
event: 'WEBHOOK_RECEIVED',
|
|
155
|
-
req,
|
|
156
|
-
res,
|
|
157
|
-
next: jest.fn()
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
expect(integration.events.WEBHOOK_RECEIVED.handler).toHaveBeenCalledWith({
|
|
161
|
-
req,
|
|
162
|
-
res,
|
|
163
|
-
next: expect.any(Function)
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should dispatch ON_WEBHOOK with job context', async () => {
|
|
168
|
-
const integration = new TestIntegration({ id: '123', userId: 'user1' });
|
|
169
|
-
integration.events.ON_WEBHOOK = {
|
|
170
|
-
handler: jest.fn().mockResolvedValue({ processed: true })
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const dispatcher = new IntegrationEventDispatcher(integration);
|
|
174
|
-
const data = { integrationId: '123', body: { event: 'test' } };
|
|
175
|
-
|
|
176
|
-
await dispatcher.dispatchJob({
|
|
177
|
-
event: 'ON_WEBHOOK',
|
|
178
|
-
data,
|
|
179
|
-
context: {}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
expect(integration.events.ON_WEBHOOK.handler).toHaveBeenCalledWith({
|
|
183
|
-
data,
|
|
184
|
-
context: {}
|
|
185
|
-
});
|
|
186
|
-
expect(integration.isHydrated).toBe(true);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('should use default WEBHOOK_RECEIVED handler if not overridden', async () => {
|
|
190
|
-
const integration = new TestIntegration();
|
|
191
|
-
const dispatcher = new IntegrationEventDispatcher(integration);
|
|
192
|
-
|
|
193
|
-
const req = { body: { test: 'data' }, params: {}, headers: {}, query: {} };
|
|
194
|
-
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
195
|
-
|
|
196
|
-
// Mock queueWebhook
|
|
197
|
-
integration.queueWebhook = jest.fn().mockResolvedValue('message-id');
|
|
198
|
-
|
|
199
|
-
const handler = dispatcher.findEventHandler(integration, 'WEBHOOK_RECEIVED');
|
|
200
|
-
expect(handler).toBeDefined();
|
|
201
|
-
|
|
202
|
-
await handler.call(integration, { req, res });
|
|
203
|
-
|
|
204
|
-
expect(integration.queueWebhook).toHaveBeenCalled();
|
|
205
|
-
expect(res.status).toHaveBeenCalledWith(200);
|
|
206
|
-
expect(res.json).toHaveBeenCalledWith({ received: true });
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
});
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
process.env.HEALTH_API_KEY = 'test-api-key';
|
|
2
|
-
|
|
3
|
-
jest.mock('../../database/config', () => ({
|
|
4
|
-
DB_TYPE: 'mongodb',
|
|
5
|
-
getDatabaseType: jest.fn(() => 'mongodb'),
|
|
6
|
-
PRISMA_LOG_LEVEL: 'error,warn',
|
|
7
|
-
PRISMA_QUERY_LOGGING: false,
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
jest.mock('mongoose', () => ({
|
|
11
|
-
set: jest.fn(),
|
|
12
|
-
connection: {
|
|
13
|
-
readyState: 1,
|
|
14
|
-
db: {
|
|
15
|
-
admin: () => ({
|
|
16
|
-
ping: jest.fn().mockResolvedValue(true)
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
jest.mock('./../backend-utils', () => ({
|
|
23
|
-
moduleFactory: {
|
|
24
|
-
moduleTypes: ['test-module', 'another-module']
|
|
25
|
-
},
|
|
26
|
-
integrationFactory: {
|
|
27
|
-
integrationTypes: ['test-integration', 'another-integration']
|
|
28
|
-
}
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
jest.mock('./../app-handler-helpers', () => ({
|
|
32
|
-
createAppHandler: jest.fn((name, router) => ({ name, router }))
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
const { router } = require('./health');
|
|
36
|
-
const mongoose = require('mongoose');
|
|
37
|
-
|
|
38
|
-
const mockRequest = (path, headers = {}) => ({
|
|
39
|
-
path,
|
|
40
|
-
headers
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const mockResponse = () => {
|
|
44
|
-
const res = {};
|
|
45
|
-
res.status = jest.fn().mockReturnValue(res);
|
|
46
|
-
res.json = jest.fn().mockReturnValue(res);
|
|
47
|
-
return res;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
describe('Health Check Endpoints', () => {
|
|
51
|
-
beforeEach(() => {
|
|
52
|
-
mongoose.connection.readyState = 1;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('Middleware - validateApiKey', () => {
|
|
56
|
-
it('should allow access to /health without authentication', async () => {
|
|
57
|
-
expect(true).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('GET /health', () => {
|
|
62
|
-
it('should return basic health status', async () => {
|
|
63
|
-
const req = mockRequest('/health');
|
|
64
|
-
const res = mockResponse();
|
|
65
|
-
|
|
66
|
-
const routeHandler = router.stack.find(layer =>
|
|
67
|
-
layer.route && layer.route.path === '/health'
|
|
68
|
-
).route.stack[0].handle;
|
|
69
|
-
|
|
70
|
-
await routeHandler(req, res);
|
|
71
|
-
|
|
72
|
-
expect(res.status).toHaveBeenCalledWith(200);
|
|
73
|
-
expect(res.json).toHaveBeenCalledWith({
|
|
74
|
-
status: 'ok',
|
|
75
|
-
timestamp: expect.any(String),
|
|
76
|
-
service: 'frigg-core-api'
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('GET /health/detailed', () => {
|
|
82
|
-
it('should return detailed health status when healthy', async () => {
|
|
83
|
-
const req = mockRequest('/health/detailed', { 'x-api-key': 'test-api-key' });
|
|
84
|
-
const res = mockResponse();
|
|
85
|
-
|
|
86
|
-
const originalPromiseAll = Promise.all;
|
|
87
|
-
Promise.all = jest.fn().mockResolvedValue([
|
|
88
|
-
{ name: 'github', status: 'healthy', reachable: true, statusCode: 200, responseTime: 100 },
|
|
89
|
-
{ name: 'npm', status: 'healthy', reachable: true, statusCode: 200, responseTime: 150 }
|
|
90
|
-
]);
|
|
91
|
-
|
|
92
|
-
const routeHandler = router.stack.find(layer =>
|
|
93
|
-
layer.route && layer.route.path === '/health/detailed'
|
|
94
|
-
).route.stack[0].handle;
|
|
95
|
-
|
|
96
|
-
await routeHandler(req, res);
|
|
97
|
-
|
|
98
|
-
Promise.all = originalPromiseAll;
|
|
99
|
-
|
|
100
|
-
expect(res.status).toHaveBeenCalledWith(200);
|
|
101
|
-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
102
|
-
status: 'healthy',
|
|
103
|
-
service: 'frigg-core-api',
|
|
104
|
-
timestamp: expect.any(String),
|
|
105
|
-
checks: expect.objectContaining({
|
|
106
|
-
database: expect.objectContaining({
|
|
107
|
-
status: 'healthy',
|
|
108
|
-
state: 'connected'
|
|
109
|
-
}),
|
|
110
|
-
integrations: expect.objectContaining({
|
|
111
|
-
status: 'healthy'
|
|
112
|
-
})
|
|
113
|
-
}),
|
|
114
|
-
responseTime: expect.any(Number)
|
|
115
|
-
}));
|
|
116
|
-
|
|
117
|
-
const response = res.json.mock.calls[0][0];
|
|
118
|
-
expect(response).not.toHaveProperty('version');
|
|
119
|
-
expect(response).not.toHaveProperty('uptime');
|
|
120
|
-
expect(response.checks).not.toHaveProperty('memory');
|
|
121
|
-
expect(response.checks.database).not.toHaveProperty('type');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should return 503 when database is disconnected', async () => {
|
|
125
|
-
mongoose.connection.readyState = 0;
|
|
126
|
-
|
|
127
|
-
const req = mockRequest('/health/detailed', { 'x-api-key': 'test-api-key' });
|
|
128
|
-
const res = mockResponse();
|
|
129
|
-
|
|
130
|
-
const originalPromiseAll = Promise.all;
|
|
131
|
-
Promise.all = jest.fn().mockResolvedValue([
|
|
132
|
-
{ name: 'github', status: 'healthy', reachable: true, statusCode: 200, responseTime: 100 },
|
|
133
|
-
{ name: 'npm', status: 'healthy', reachable: true, statusCode: 200, responseTime: 150 }
|
|
134
|
-
]);
|
|
135
|
-
|
|
136
|
-
const routeHandler = router.stack.find(layer =>
|
|
137
|
-
layer.route && layer.route.path === '/health/detailed'
|
|
138
|
-
).route.stack[0].handle;
|
|
139
|
-
|
|
140
|
-
await routeHandler(req, res);
|
|
141
|
-
|
|
142
|
-
Promise.all = originalPromiseAll;
|
|
143
|
-
|
|
144
|
-
expect(res.status).toHaveBeenCalledWith(503);
|
|
145
|
-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
146
|
-
status: 'unhealthy'
|
|
147
|
-
}));
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('GET /health/live', () => {
|
|
152
|
-
it('should return alive status', async () => {
|
|
153
|
-
const req = mockRequest('/health/live', { 'x-api-key': 'test-api-key' });
|
|
154
|
-
const res = mockResponse();
|
|
155
|
-
|
|
156
|
-
const routeHandler = router.stack.find(layer =>
|
|
157
|
-
layer.route && layer.route.path === '/health/live'
|
|
158
|
-
).route.stack[0].handle;
|
|
159
|
-
|
|
160
|
-
routeHandler(req, res);
|
|
161
|
-
|
|
162
|
-
expect(res.status).toHaveBeenCalledWith(200);
|
|
163
|
-
expect(res.json).toHaveBeenCalledWith({
|
|
164
|
-
status: 'alive',
|
|
165
|
-
timestamp: expect.any(String)
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('GET /health/ready', () => {
|
|
171
|
-
it('should return ready when all checks pass', async () => {
|
|
172
|
-
const req = mockRequest('/health/ready', { 'x-api-key': 'test-api-key' });
|
|
173
|
-
const res = mockResponse();
|
|
174
|
-
|
|
175
|
-
const routeHandler = router.stack.find(layer =>
|
|
176
|
-
layer.route && layer.route.path === '/health/ready'
|
|
177
|
-
).route.stack[0].handle;
|
|
178
|
-
|
|
179
|
-
await routeHandler(req, res);
|
|
180
|
-
|
|
181
|
-
expect(res.status).toHaveBeenCalledWith(200);
|
|
182
|
-
expect(res.json).toHaveBeenCalledWith({
|
|
183
|
-
ready: true,
|
|
184
|
-
timestamp: expect.any(String),
|
|
185
|
-
checks: {
|
|
186
|
-
database: true,
|
|
187
|
-
modules: true
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should return 503 when database is not connected', async () => {
|
|
193
|
-
mongoose.connection.readyState = 0;
|
|
194
|
-
|
|
195
|
-
const req = mockRequest('/health/ready', { 'x-api-key': 'test-api-key' });
|
|
196
|
-
const res = mockResponse();
|
|
197
|
-
|
|
198
|
-
const routeHandler = router.stack.find(layer =>
|
|
199
|
-
layer.route && layer.route.path === '/health/ready'
|
|
200
|
-
).route.stack[0].handle;
|
|
201
|
-
|
|
202
|
-
await routeHandler(req, res);
|
|
203
|
-
|
|
204
|
-
expect(res.status).toHaveBeenCalledWith(503);
|
|
205
|
-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
206
|
-
ready: false
|
|
207
|
-
}));
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
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
|
-
jest.mock('../app-definition-loader', () => {
|
|
9
|
-
const { IntegrationBase } = require('../../integrations/integration-base');
|
|
10
|
-
|
|
11
|
-
class WebhookEnabledIntegration extends IntegrationBase {
|
|
12
|
-
static Definition = {
|
|
13
|
-
name: 'webhook-enabled',
|
|
14
|
-
version: '1.0.0',
|
|
15
|
-
modules: {},
|
|
16
|
-
webhooks: true,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
constructor(params) {
|
|
20
|
-
super(params);
|
|
21
|
-
this.queueWebhook = jest.fn().mockResolvedValue('message-id');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
class AdvancedWebhookIntegration extends IntegrationBase {
|
|
26
|
-
static Definition = {
|
|
27
|
-
name: 'advanced-webhook',
|
|
28
|
-
version: '1.0.0',
|
|
29
|
-
modules: {},
|
|
30
|
-
webhooks: {
|
|
31
|
-
enabled: true,
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
constructor(params) {
|
|
36
|
-
super(params);
|
|
37
|
-
this.events = {
|
|
38
|
-
WEBHOOK_RECEIVED: {
|
|
39
|
-
handler: async ({ req, res }) => {
|
|
40
|
-
// Custom signature verification
|
|
41
|
-
const signature = req.headers['x-webhook-signature'];
|
|
42
|
-
if (signature !== 'valid-signature') {
|
|
43
|
-
return res.status(401).json({ error: 'Invalid signature' });
|
|
44
|
-
}
|
|
45
|
-
await this.queueWebhook({ body: req.body });
|
|
46
|
-
res.status(200).json({ verified: true });
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
this.queueWebhook = jest.fn().mockResolvedValue('message-id');
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
class NoWebhookIntegration extends IntegrationBase {
|
|
55
|
-
static Definition = {
|
|
56
|
-
name: 'no-webhook',
|
|
57
|
-
version: '1.0.0',
|
|
58
|
-
modules: {},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
loadAppDefinition: () => ({
|
|
64
|
-
integrations: [
|
|
65
|
-
WebhookEnabledIntegration,
|
|
66
|
-
AdvancedWebhookIntegration,
|
|
67
|
-
NoWebhookIntegration,
|
|
68
|
-
],
|
|
69
|
-
}),
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('Integration Webhook Routers', () => {
|
|
74
|
-
let handlers;
|
|
75
|
-
|
|
76
|
-
beforeEach(() => {
|
|
77
|
-
// Clear module cache to get fresh handlers
|
|
78
|
-
jest.resetModules();
|
|
79
|
-
jest.clearAllMocks();
|
|
80
|
-
|
|
81
|
-
// Re-require after mocking
|
|
82
|
-
handlers = require('./integration-webhook-routers').handlers;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('Handler Creation', () => {
|
|
86
|
-
it('should create webhook handlers for integrations with webhooks: true', () => {
|
|
87
|
-
expect(handlers['webhook-enabledWebhook']).toBeDefined();
|
|
88
|
-
expect(handlers['webhook-enabledWebhook'].handler).toBeDefined();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should create webhook handlers for integrations with webhooks.enabled: true', () => {
|
|
92
|
-
expect(handlers['advanced-webhookWebhook']).toBeDefined();
|
|
93
|
-
expect(handlers['advanced-webhookWebhook'].handler).toBeDefined();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should not create webhook handlers for integrations without webhooks', () => {
|
|
97
|
-
expect(handlers['no-webhookWebhook']).toBeUndefined();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should configure handlers to not use database connection', () => {
|
|
101
|
-
// Handlers are created with createAppHandler(..., false)
|
|
102
|
-
// This means shouldUseDatabase = false
|
|
103
|
-
// Actual behavior is tested in integration tests
|
|
104
|
-
expect(handlers['webhook-enabledWebhook']).toBeDefined();
|
|
105
|
-
expect(handlers['advanced-webhookWebhook']).toBeDefined();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('Webhook Configuration', () => {
|
|
110
|
-
it('should support boolean webhook configuration', () => {
|
|
111
|
-
// webhooks: true should enable webhook handling
|
|
112
|
-
expect(handlers['webhook-enabledWebhook']).toBeDefined();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should support object webhook configuration', () => {
|
|
116
|
-
// webhooks: { enabled: true } should enable webhook handling
|
|
117
|
-
expect(handlers['advanced-webhookWebhook']).toBeDefined();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should skip integrations with webhooks disabled', () => {
|
|
121
|
-
// webhooks: false or missing should not create handlers
|
|
122
|
-
expect(handlers['no-webhookWebhook']).toBeUndefined();
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|