@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().mockResolvedValue({
37
- status: 200,
38
- data: { result: 'success' }
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
- CookbookExecutor: jest.fn().mockImplementation(() => ({
45
- execute: jest.fn().mockResolvedValue({
46
- results: { step1: { data: 'test' } }
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
- const testMessage = {
99
- workflow_id: 'wf-123',
100
- current_step: {
101
- id: 'step1',
102
- service: 'test-service',
103
- operation: 'testOp',
104
- input: { data: 'test' }
105
- },
106
- context: {
107
- variables: {},
108
- results: {}
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
- expect(() => orchestrator.validateMessage(message)).not.toThrow();
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
- 'testOp',
178
- { data: 'test' },
179
- {}
126
+ 'test-echo',
127
+ { text: 'hello' },
128
+ expect.objectContaining({ workflow_id: 'wf-123', step_id: 's1' })
180
129
  );
181
- expect(result).toEqual({
182
- status: 200,
183
- data: { result: 'success' }
184
- });
130
+ expect(result.success).toBe(true);
185
131
  });
186
132
 
187
- it('should retry on transient errors', async () => {
188
- mockErrorHandler.isRetryableError.mockReturnValue(true);
189
- mockApiMapper.callOperation
190
- .mockRejectedValueOnce(new Error('Transient error'))
191
- .mockResolvedValueOnce({ status: 200, data: 'success' });
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 result = await orchestrator.executeStep(step, {});
149
+ const message = { workflow_id: 'wf-123', cookbook, current_step: 's1' };
150
+ const res = await orchestrator.processWorkflowMessage(message, 'hello-service');
194
151
 
195
- expect(mockErrorHandler.executeWithRetry).toHaveBeenCalled();
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
- describe('routeToNextStep', () => {
200
- it('should route to next service via MQ', async () => {
201
- const nextStep = {
202
- service: 'next-service',
203
- operation: 'nextOp'
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
- await orchestrator.routeToNextStep('wf-123', nextStep, {});
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
- await expect(
223
- orchestrator.routeToNextStep('wf-123', {}, {})
224
- ).rejects.toThrow('MQ Error');
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
- describe('caching', () => {
229
- it('should cache successful results when cache is available', async () => {
230
- const step = {
231
- id: 'step1',
232
- cache: { ttl: 300 }
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
- expect(mockCache.set).toHaveBeenCalledWith(
239
- expect.any(String),
240
- result,
241
- 300
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('should skip caching when cache is not configured', async () => {
246
- orchestrator.cache = null;
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
- await orchestrator.cacheResult({}, {});
243
+ const message = { workflow_id: 'wf-123', cookbook, current_step: 's1' };
244
+ await orchestrator.processWorkflowMessage(message, 'hello-service');
249
245
 
250
- expect(mockCache.set).not.toHaveBeenCalled();
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
  });