@onlineapps/conn-orch-orchestrator 1.0.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.
@@ -0,0 +1,253 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Unit tests for WorkflowOrchestrator
5
+ * Tests individual methods in isolation with mocked dependencies
6
+ */
7
+
8
+ const WorkflowOrchestrator = require('../../src/WorkflowOrchestrator');
9
+
10
+ describe('WorkflowOrchestrator - Unit Tests', () => {
11
+ let orchestrator;
12
+ let mockMqClient;
13
+ let mockRegistryClient;
14
+ let mockApiMapper;
15
+ let mockCookbook;
16
+ let mockLogger;
17
+ let mockCache;
18
+ let mockErrorHandler;
19
+
20
+ beforeEach(() => {
21
+ // Setup mocked dependencies
22
+ mockMqClient = {
23
+ publish: jest.fn().mockResolvedValue(true),
24
+ subscribe: jest.fn().mockResolvedValue(true)
25
+ };
26
+
27
+ mockRegistryClient = {
28
+ getService: jest.fn().mockResolvedValue({
29
+ name: 'test-service',
30
+ url: 'http://localhost:3000',
31
+ openapi: {}
32
+ })
33
+ };
34
+
35
+ mockApiMapper = {
36
+ callOperation: jest.fn().mockResolvedValue({
37
+ status: 200,
38
+ data: { result: 'success' }
39
+ })
40
+ };
41
+
42
+ mockCookbook = {
43
+ validateCookbook: jest.fn(),
44
+ CookbookExecutor: jest.fn().mockImplementation(() => ({
45
+ execute: jest.fn().mockResolvedValue({
46
+ results: { step1: { data: 'test' } }
47
+ })
48
+ }))
49
+ };
50
+
51
+ mockLogger = {
52
+ info: jest.fn(),
53
+ error: jest.fn(),
54
+ warn: jest.fn(),
55
+ debug: jest.fn()
56
+ };
57
+
58
+ mockCache = {
59
+ get: jest.fn().mockResolvedValue(null),
60
+ set: jest.fn().mockResolvedValue('OK'),
61
+ del: jest.fn().mockResolvedValue(1)
62
+ };
63
+
64
+ mockErrorHandler = {
65
+ executeWithRetry: jest.fn((fn) => fn()),
66
+ handleError: jest.fn(),
67
+ isRetryableError: jest.fn().mockReturnValue(false)
68
+ };
69
+
70
+ orchestrator = new WorkflowOrchestrator({
71
+ mqClient: mockMqClient,
72
+ registryClient: mockRegistryClient,
73
+ apiMapper: mockApiMapper,
74
+ cookbook: mockCookbook,
75
+ logger: mockLogger,
76
+ cache: mockCache,
77
+ errorHandler: mockErrorHandler
78
+ });
79
+ });
80
+
81
+ describe('constructor', () => {
82
+ it('should initialize with all required dependencies', () => {
83
+ expect(orchestrator.mqClient).toBe(mockMqClient);
84
+ expect(orchestrator.registryClient).toBe(mockRegistryClient);
85
+ expect(orchestrator.apiMapper).toBe(mockApiMapper);
86
+ expect(orchestrator.cookbook).toBe(mockCookbook);
87
+ expect(orchestrator.logger).toBe(mockLogger);
88
+ expect(orchestrator.cache).toBe(mockCache);
89
+ expect(orchestrator.errorHandler).toBe(mockErrorHandler);
90
+ });
91
+
92
+ it('should throw if required dependencies are missing', () => {
93
+ expect(() => new WorkflowOrchestrator({})).toThrow();
94
+ });
95
+ });
96
+
97
+ 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
+ }
150
+ };
151
+
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, {});
175
+
176
+ expect(mockApiMapper.callOperation).toHaveBeenCalledWith(
177
+ 'testOp',
178
+ { data: 'test' },
179
+ {}
180
+ );
181
+ expect(result).toEqual({
182
+ status: 200,
183
+ data: { result: 'success' }
184
+ });
185
+ });
186
+
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' });
192
+
193
+ const result = await orchestrator.executeStep(step, {});
194
+
195
+ expect(mockErrorHandler.executeWithRetry).toHaveBeenCalled();
196
+ });
197
+ });
198
+
199
+ describe('routeToNextStep', () => {
200
+ it('should route to next service via MQ', async () => {
201
+ const nextStep = {
202
+ service: 'next-service',
203
+ operation: 'nextOp'
204
+ };
205
+
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'));
221
+
222
+ await expect(
223
+ orchestrator.routeToNextStep('wf-123', {}, {})
224
+ ).rejects.toThrow('MQ Error');
225
+ });
226
+ });
227
+
228
+ describe('caching', () => {
229
+ it('should cache successful results when cache is available', async () => {
230
+ const step = {
231
+ id: 'step1',
232
+ cache: { ttl: 300 }
233
+ };
234
+ const result = { data: 'test' };
235
+
236
+ await orchestrator.cacheResult(step, result);
237
+
238
+ expect(mockCache.set).toHaveBeenCalledWith(
239
+ expect.any(String),
240
+ result,
241
+ 300
242
+ );
243
+ });
244
+
245
+ it('should skip caching when cache is not configured', async () => {
246
+ orchestrator.cache = null;
247
+
248
+ await orchestrator.cacheResult({}, {});
249
+
250
+ expect(mockCache.set).not.toHaveBeenCalled();
251
+ });
252
+ });
253
+ });