@onlineapps/conn-orch-orchestrator 1.0.63 → 1.0.65
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.
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* Tests individual methods in isolation with mocked dependencies
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
jest.mock('@onlineapps/mq-client-core', () => ({
|
|
9
|
+
monitoring: {
|
|
10
|
+
publishToMonitoringWorkflow: jest.fn(async () => true)
|
|
11
|
+
}
|
|
12
|
+
}));
|
|
13
|
+
|
|
8
14
|
const WorkflowOrchestrator = require('../../src/WorkflowOrchestrator');
|
|
9
15
|
|
|
10
16
|
describe('WorkflowOrchestrator - Unit Tests @unit', () => {
|
|
@@ -33,19 +39,21 @@ describe('WorkflowOrchestrator - Unit Tests @unit', () => {
|
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
mockApiMapper = {
|
|
36
|
-
callOperation: jest.fn()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
callOperation: jest.fn(async (operationId, input, ctx) => ({
|
|
43
|
+
operationId,
|
|
44
|
+
input,
|
|
45
|
+
ctx
|
|
46
|
+
}))
|
|
40
47
|
};
|
|
41
48
|
|
|
42
49
|
mockCookbook = {
|
|
43
50
|
validateCookbook: jest.fn(),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
48
|
-
|
|
51
|
+
templateHelpers: {
|
|
52
|
+
normalizeString: jest.fn((value) => String(value || '').replace(/\s+/g, ' ').trim()),
|
|
53
|
+
webalizeString: jest.fn((value) => String(value || '').toLowerCase().replace(/\s+/g, '-')),
|
|
54
|
+
string2file: jest.fn(async (value) => ({ _descriptor: true, type: 'file', storage_ref: `minio://mock/${String(value).length}` })),
|
|
55
|
+
file2string: jest.fn(async (descriptor) => (descriptor && descriptor.storage_ref ? `from:${descriptor.storage_ref}` : String(descriptor)))
|
|
56
|
+
}
|
|
49
57
|
};
|
|
50
58
|
|
|
51
59
|
mockLogger = {
|
|
@@ -95,159 +103,152 @@ describe('WorkflowOrchestrator - Unit Tests @unit', () => {
|
|
|
95
103
|
});
|
|
96
104
|
|
|
97
105
|
describe('processWorkflowMessage', () => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
it('should process a valid workflow message', async () => {
|
|
113
|
-
const result = await orchestrator.processWorkflowMessage(testMessage, 'test-service');
|
|
114
|
-
|
|
115
|
-
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
116
|
-
expect.stringContaining('Processing workflow message'),
|
|
117
|
-
expect.any(Object)
|
|
118
|
-
);
|
|
119
|
-
expect(result).toBeDefined();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should use cache when available', async () => {
|
|
123
|
-
mockCache.get.mockResolvedValueOnce({ cached: 'data' });
|
|
124
|
-
|
|
125
|
-
await orchestrator.processWorkflowMessage(testMessage, 'test-service');
|
|
126
|
-
|
|
127
|
-
expect(mockCache.get).toHaveBeenCalled();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should handle errors appropriately', async () => {
|
|
131
|
-
mockApiMapper.callOperation.mockRejectedValueOnce(new Error('API Error'));
|
|
132
|
-
|
|
133
|
-
await expect(
|
|
134
|
-
orchestrator.processWorkflowMessage(testMessage, 'test-service')
|
|
135
|
-
).rejects.toThrow('API Error');
|
|
136
|
-
|
|
137
|
-
expect(mockLogger.error).toHaveBeenCalled();
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('validateMessage', () => {
|
|
142
|
-
it('should validate a correct message', () => {
|
|
143
|
-
const message = {
|
|
144
|
-
workflow_id: 'wf-123',
|
|
145
|
-
current_step: {
|
|
146
|
-
id: 'step1',
|
|
147
|
-
service: 'test',
|
|
148
|
-
operation: 'op1'
|
|
149
|
-
}
|
|
106
|
+
it('executes task step for the current service and calls ApiMapper', async () => {
|
|
107
|
+
const cookbook = {
|
|
108
|
+
version: '2.1.0',
|
|
109
|
+
api_input: { text: 'hello' },
|
|
110
|
+
steps: [
|
|
111
|
+
{
|
|
112
|
+
step_id: 's1',
|
|
113
|
+
type: 'task',
|
|
114
|
+
service: 'hello-service',
|
|
115
|
+
operation: 'test-echo',
|
|
116
|
+
input: { text: '{{api_input.text}}' }
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
delivery: { handler: 'none' }
|
|
150
120
|
};
|
|
151
121
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
it('should throw for invalid message structure', () => {
|
|
156
|
-
const invalidMessage = {
|
|
157
|
-
workflow_id: 'wf-123'
|
|
158
|
-
// missing current_step
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
expect(() => orchestrator.validateMessage(invalidMessage)).toThrow();
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('executeStep', () => {
|
|
166
|
-
const step = {
|
|
167
|
-
id: 'step1',
|
|
168
|
-
service: 'test-service',
|
|
169
|
-
operation: 'testOp',
|
|
170
|
-
input: { data: 'test' }
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
it('should execute a step successfully', async () => {
|
|
174
|
-
const result = await orchestrator.executeStep(step, {});
|
|
122
|
+
const message = { workflow_id: 'wf-123', cookbook, current_step: 's1' };
|
|
123
|
+
const result = await orchestrator.processWorkflowMessage(message, 'hello-service');
|
|
175
124
|
|
|
176
125
|
expect(mockApiMapper.callOperation).toHaveBeenCalledWith(
|
|
177
|
-
'
|
|
178
|
-
{
|
|
179
|
-
{}
|
|
126
|
+
'test-echo',
|
|
127
|
+
{ text: 'hello' },
|
|
128
|
+
expect.objectContaining({ workflow_id: 'wf-123', step_id: 's1' })
|
|
180
129
|
);
|
|
181
|
-
expect(result).
|
|
182
|
-
status: 200,
|
|
183
|
-
data: { result: 'success' }
|
|
184
|
-
});
|
|
130
|
+
expect(result.success).toBe(true);
|
|
185
131
|
});
|
|
186
132
|
|
|
187
|
-
it('
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
133
|
+
it('routes task step to correct service queue when service does not match', async () => {
|
|
134
|
+
const cookbook = {
|
|
135
|
+
version: '2.1.0',
|
|
136
|
+
api_input: { text: 'hello' },
|
|
137
|
+
steps: [
|
|
138
|
+
{
|
|
139
|
+
step_id: 's1',
|
|
140
|
+
type: 'task',
|
|
141
|
+
service: 'other-service',
|
|
142
|
+
operation: 'test-echo',
|
|
143
|
+
input: { text: '{{api_input.text}}' }
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
delivery: { handler: 'none' }
|
|
147
|
+
};
|
|
192
148
|
|
|
193
|
-
const
|
|
149
|
+
const message = { workflow_id: 'wf-123', cookbook, current_step: 's1' };
|
|
150
|
+
const res = await orchestrator.processWorkflowMessage(message, 'hello-service');
|
|
194
151
|
|
|
195
|
-
expect(
|
|
152
|
+
expect(res).toEqual({ skipped: true, reason: 'wrong_service' });
|
|
153
|
+
expect(mockMqClient.publish).toHaveBeenCalledWith('other-service.workflow', message);
|
|
154
|
+
expect(mockApiMapper.callOperation).not.toHaveBeenCalled();
|
|
196
155
|
});
|
|
197
|
-
});
|
|
198
156
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
157
|
+
it('routes pinned control-flow step to pinned service when current service does not match', async () => {
|
|
158
|
+
const cookbook = {
|
|
159
|
+
version: '2.1.0',
|
|
160
|
+
api_input: { items: ['A', 'B'] },
|
|
161
|
+
steps: [
|
|
162
|
+
{
|
|
163
|
+
step_id: 'loop',
|
|
164
|
+
type: 'foreach',
|
|
165
|
+
service: 'hello-service', // pin
|
|
166
|
+
iterator: '{{api_input.items}}',
|
|
167
|
+
body: [
|
|
168
|
+
{
|
|
169
|
+
step_id: 'inner',
|
|
170
|
+
type: 'task',
|
|
171
|
+
service: 'hello-service',
|
|
172
|
+
operation: 'good-day',
|
|
173
|
+
input: { name: '{{current}}' }
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
delivery: { handler: 'none' }
|
|
204
179
|
};
|
|
205
180
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
expect(mockMqClient.publish).toHaveBeenCalledWith(
|
|
209
|
-
expect.objectContaining({
|
|
210
|
-
workflow_id: 'wf-123',
|
|
211
|
-
current_step: nextStep
|
|
212
|
-
}),
|
|
213
|
-
expect.objectContaining({
|
|
214
|
-
queue: 'next-service.workflow'
|
|
215
|
-
})
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should handle routing errors', async () => {
|
|
220
|
-
mockMqClient.publish.mockRejectedValueOnce(new Error('MQ Error'));
|
|
181
|
+
const message = { workflow_id: 'wf-123', cookbook, current_step: 'loop' };
|
|
182
|
+
const res = await orchestrator.processWorkflowMessage(message, 'pdfgen-service');
|
|
221
183
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
).
|
|
184
|
+
expect(res).toEqual({ skipped: true, reason: 'wrong_service_control_flow' });
|
|
185
|
+
expect(mockMqClient.publish).toHaveBeenCalledWith('hello-service.workflow', message);
|
|
186
|
+
expect(mockApiMapper.callOperation).not.toHaveBeenCalled();
|
|
225
187
|
});
|
|
226
|
-
});
|
|
227
188
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
189
|
+
it('executes foreach control-flow step when pinned to this service', async () => {
|
|
190
|
+
const cookbook = {
|
|
191
|
+
version: '2.1.0',
|
|
192
|
+
api_input: { items: ['A', 'B'] },
|
|
193
|
+
steps: [
|
|
194
|
+
{
|
|
195
|
+
step_id: 'loop',
|
|
196
|
+
type: 'foreach',
|
|
197
|
+
service: 'hello-service', // pin to ensure deterministic executor
|
|
198
|
+
iterator: '{{api_input.items}}',
|
|
199
|
+
body: [
|
|
200
|
+
{
|
|
201
|
+
step_id: 'inner',
|
|
202
|
+
type: 'task',
|
|
203
|
+
service: 'hello-service',
|
|
204
|
+
operation: 'good-day',
|
|
205
|
+
input: { name: '{{current}}' }
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
],
|
|
210
|
+
delivery: { handler: 'none' }
|
|
233
211
|
};
|
|
234
|
-
const result = { data: 'test' };
|
|
235
|
-
|
|
236
|
-
await orchestrator.cacheResult(step, result);
|
|
237
212
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
213
|
+
const message = { workflow_id: 'wf-123', cookbook, current_step: 'loop' };
|
|
214
|
+
const res = await orchestrator.processWorkflowMessage(message, 'hello-service');
|
|
215
|
+
|
|
216
|
+
expect(res.success).toBe(true);
|
|
217
|
+
expect(res.result).toEqual(expect.objectContaining({ type: 'foreach', count: 2 }));
|
|
218
|
+
expect(mockApiMapper.callOperation).toHaveBeenCalledTimes(2);
|
|
219
|
+
expect(mockApiMapper.callOperation).toHaveBeenNthCalledWith(
|
|
220
|
+
1,
|
|
221
|
+
'good-day',
|
|
222
|
+
{ name: 'A' },
|
|
223
|
+
expect.objectContaining({ workflow_id: 'wf-123', step_id: 'inner' })
|
|
242
224
|
);
|
|
243
225
|
});
|
|
244
226
|
|
|
245
|
-
it('
|
|
246
|
-
|
|
227
|
+
it('resolves helper call in templates', async () => {
|
|
228
|
+
const cookbook = {
|
|
229
|
+
version: '2.1.0',
|
|
230
|
+
api_input: { text: ' Hello World ' },
|
|
231
|
+
steps: [
|
|
232
|
+
{
|
|
233
|
+
step_id: 's1',
|
|
234
|
+
type: 'task',
|
|
235
|
+
service: 'hello-service',
|
|
236
|
+
operation: 'test-echo',
|
|
237
|
+
input: { text: '{{normalizeString(api_input.text)}}' }
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
delivery: { handler: 'none' }
|
|
241
|
+
};
|
|
247
242
|
|
|
248
|
-
|
|
243
|
+
const message = { workflow_id: 'wf-123', cookbook, current_step: 's1' };
|
|
244
|
+
await orchestrator.processWorkflowMessage(message, 'hello-service');
|
|
249
245
|
|
|
250
|
-
expect(
|
|
246
|
+
expect(mockCookbook.templateHelpers.normalizeString).toHaveBeenCalled();
|
|
247
|
+
expect(mockApiMapper.callOperation).toHaveBeenCalledWith(
|
|
248
|
+
'test-echo',
|
|
249
|
+
{ text: 'Hello World' },
|
|
250
|
+
expect.objectContaining({ workflow_id: 'wf-123', step_id: 's1' })
|
|
251
|
+
);
|
|
251
252
|
});
|
|
252
253
|
});
|
|
253
254
|
});
|