@onlineapps/service-wrapper 2.0.5 → 2.0.7
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/package.json +7 -6
- package/src/ServiceWrapper.js +35 -6
- package/test/e2e/full-flow.test.js +293 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-wrapper",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,17 +24,18 @@
|
|
|
24
24
|
"author": "OA Drive Team",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"@onlineapps/conn-base-cache": "^1.0.0",
|
|
27
28
|
"@onlineapps/conn-base-logger": "^1.0.0",
|
|
29
|
+
"@onlineapps/conn-infra-error-handler": "^1.0.0",
|
|
28
30
|
"@onlineapps/conn-infra-mq": "^1.1.0",
|
|
29
|
-
"@onlineapps/conn-orch-
|
|
31
|
+
"@onlineapps/conn-orch-api-mapper": "^1.0.0",
|
|
30
32
|
"@onlineapps/conn-orch-cookbook": "^2.0.0",
|
|
31
33
|
"@onlineapps/conn-orch-orchestrator": "^1.0.1",
|
|
32
|
-
"@onlineapps/conn-orch-
|
|
33
|
-
"@onlineapps/conn-base-cache": "^1.0.0",
|
|
34
|
-
"@onlineapps/conn-infra-error-handler": "^1.0.0"
|
|
34
|
+
"@onlineapps/conn-orch-registry": "^1.1.4"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"
|
|
37
|
+
"express": "^5.1.0",
|
|
38
|
+
"jest": "^29.7.0",
|
|
38
39
|
"jsdoc": "^4.0.2",
|
|
39
40
|
"jsdoc-to-markdown": "^8.0.0"
|
|
40
41
|
},
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -69,7 +69,7 @@ class ServiceWrapper {
|
|
|
69
69
|
this.config = options.config || {};
|
|
70
70
|
|
|
71
71
|
// Initialize connectors
|
|
72
|
-
this.logger = LoggerConnector
|
|
72
|
+
this.logger = new LoggerConnector({ serviceName: this.serviceName });
|
|
73
73
|
this.mqClient = null;
|
|
74
74
|
this.registryClient = null;
|
|
75
75
|
this.orchestrator = null;
|
|
@@ -297,8 +297,38 @@ class ServiceWrapper {
|
|
|
297
297
|
|
|
298
298
|
// Subscribe to workflow messages
|
|
299
299
|
await this.mqClient.consume(
|
|
300
|
-
|
|
300
|
+
queueName,
|
|
301
|
+
async (rawMessage) => {
|
|
301
302
|
try {
|
|
303
|
+
// Parse message content if it's a buffer (AMQP message)
|
|
304
|
+
let message;
|
|
305
|
+
if (rawMessage && rawMessage.content) {
|
|
306
|
+
// This is an AMQP message with content buffer
|
|
307
|
+
const messageContent = rawMessage.content.toString();
|
|
308
|
+
try {
|
|
309
|
+
message = JSON.parse(messageContent);
|
|
310
|
+
} catch (parseError) {
|
|
311
|
+
this.logger.error('Failed to parse message content', {
|
|
312
|
+
error: parseError.message,
|
|
313
|
+
content: messageContent
|
|
314
|
+
});
|
|
315
|
+
throw parseError;
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
// Already parsed or direct message
|
|
319
|
+
message = rawMessage;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// DEBUG: Log the actual message structure
|
|
323
|
+
this.logger.info('DEBUG: Received message structure', {
|
|
324
|
+
workflow_id: message.workflow_id,
|
|
325
|
+
has_cookbook: !!message.cookbook,
|
|
326
|
+
cookbook_name: message.cookbook?.name,
|
|
327
|
+
cookbook_steps: message.cookbook?.steps?.length,
|
|
328
|
+
first_step: message.cookbook?.steps?.[0],
|
|
329
|
+
current_step: message.current_step
|
|
330
|
+
});
|
|
331
|
+
|
|
302
332
|
// Delegate ALL processing to orchestrator
|
|
303
333
|
const result = await this.orchestrator.processWorkflowMessage(
|
|
304
334
|
message,
|
|
@@ -313,13 +343,12 @@ class ServiceWrapper {
|
|
|
313
343
|
|
|
314
344
|
} catch (error) {
|
|
315
345
|
this.logger.error('Message processing failed', {
|
|
316
|
-
workflow_id: message
|
|
317
|
-
step: message
|
|
346
|
+
workflow_id: message?.workflow_id || 'unknown',
|
|
347
|
+
step: message?.current_step || 'unknown',
|
|
318
348
|
error: error.message
|
|
319
349
|
});
|
|
320
350
|
}
|
|
321
|
-
}
|
|
322
|
-
{ queue: queueName }
|
|
351
|
+
}
|
|
323
352
|
);
|
|
324
353
|
|
|
325
354
|
this.logger.info(`Subscribed to queue: ${queueName}`);
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* End-to-End Integration Tests for ServiceWrapper
|
|
5
|
+
* Tests the complete flow: MQ → ServiceWrapper → HTTP Service → Response
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ServiceWrapper = require('../../src/ServiceWrapper');
|
|
9
|
+
const MQConnector = require('@onlineapps/conn-infra-mq');
|
|
10
|
+
const express = require('express');
|
|
11
|
+
const http = require('http');
|
|
12
|
+
|
|
13
|
+
describe('ServiceWrapper E2E Tests', () => {
|
|
14
|
+
let testService;
|
|
15
|
+
let testServer;
|
|
16
|
+
let serviceWrapper;
|
|
17
|
+
let mqClient;
|
|
18
|
+
let servicePort;
|
|
19
|
+
const serviceName = 'test-service';
|
|
20
|
+
const rabbitUrl = process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:33023';
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
// 1. Create test HTTP service
|
|
24
|
+
testService = express();
|
|
25
|
+
testService.use(express.json());
|
|
26
|
+
|
|
27
|
+
// Add test endpoints
|
|
28
|
+
testService.post('/test/hello', (req, res) => {
|
|
29
|
+
res.json({
|
|
30
|
+
message: `Hello ${req.body.name}`,
|
|
31
|
+
timestamp: new Date().toISOString()
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
testService.post('/test/error', (req, res) => {
|
|
36
|
+
res.status(500).json({ error: 'Test error' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
testService.get('/health', (req, res) => {
|
|
40
|
+
res.json({ status: 'ok' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Start test service
|
|
44
|
+
servicePort = 40000 + Math.floor(Math.random() * 1000);
|
|
45
|
+
testServer = http.createServer(testService);
|
|
46
|
+
await new Promise(resolve => {
|
|
47
|
+
testServer.listen(servicePort, resolve);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 2. Create OpenAPI spec
|
|
51
|
+
const openApiSpec = {
|
|
52
|
+
openapi: '3.0.0',
|
|
53
|
+
info: {
|
|
54
|
+
title: 'Test Service',
|
|
55
|
+
version: '1.0.0'
|
|
56
|
+
},
|
|
57
|
+
paths: {
|
|
58
|
+
'/test/hello': {
|
|
59
|
+
post: {
|
|
60
|
+
operationId: 'sayHello',
|
|
61
|
+
requestBody: {
|
|
62
|
+
content: {
|
|
63
|
+
'application/json': {
|
|
64
|
+
schema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
name: { type: 'string' }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
'/test/error': {
|
|
76
|
+
post: {
|
|
77
|
+
operationId: 'triggerError'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// 3. Initialize ServiceWrapper
|
|
84
|
+
serviceWrapper = new ServiceWrapper({
|
|
85
|
+
serviceUrl: `http://localhost:${servicePort}`,
|
|
86
|
+
serviceName,
|
|
87
|
+
openApiSpec,
|
|
88
|
+
config: {
|
|
89
|
+
rabbitmq: rabbitUrl,
|
|
90
|
+
heartbeatInterval: 60000, // Long interval for tests
|
|
91
|
+
directCall: false // Force HTTP calls
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 4. Initialize MQ client for sending test messages
|
|
96
|
+
mqClient = new MQConnector({
|
|
97
|
+
type: 'rabbitmq',
|
|
98
|
+
host: rabbitUrl,
|
|
99
|
+
queue: `${serviceName}.workflow`,
|
|
100
|
+
durable: true
|
|
101
|
+
});
|
|
102
|
+
await mqClient.connect();
|
|
103
|
+
|
|
104
|
+
// 5. Start wrapper (with timeout for registration)
|
|
105
|
+
await serviceWrapper.start();
|
|
106
|
+
}, 60000); // 60s timeout for setup
|
|
107
|
+
|
|
108
|
+
afterAll(async () => {
|
|
109
|
+
// Cleanup
|
|
110
|
+
if (serviceWrapper) {
|
|
111
|
+
await serviceWrapper.stop();
|
|
112
|
+
}
|
|
113
|
+
if (mqClient) {
|
|
114
|
+
await mqClient.disconnect();
|
|
115
|
+
}
|
|
116
|
+
if (testServer) {
|
|
117
|
+
await new Promise(resolve => testServer.close(resolve));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Basic Message Flow', () => {
|
|
122
|
+
test('should process simple workflow message', async () => {
|
|
123
|
+
const testMessage = {
|
|
124
|
+
workflow_id: 'test-workflow-1',
|
|
125
|
+
current_step: {
|
|
126
|
+
service: serviceName,
|
|
127
|
+
operation: 'sayHello',
|
|
128
|
+
input: {
|
|
129
|
+
name: 'World'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Send message to queue
|
|
135
|
+
await mqClient.publish(testMessage);
|
|
136
|
+
|
|
137
|
+
// Wait for processing
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
139
|
+
|
|
140
|
+
// Verify by checking service logs or response queue
|
|
141
|
+
// Note: In real test, we'd capture response through response queue
|
|
142
|
+
expect(true).toBe(true); // Placeholder
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('should handle error responses', async () => {
|
|
146
|
+
const testMessage = {
|
|
147
|
+
workflow_id: 'test-workflow-2',
|
|
148
|
+
current_step: {
|
|
149
|
+
service: serviceName,
|
|
150
|
+
operation: 'triggerError',
|
|
151
|
+
input: {}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await mqClient.publish(testMessage);
|
|
156
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
157
|
+
|
|
158
|
+
// Verify error handling
|
|
159
|
+
expect(true).toBe(true); // Placeholder
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Workflow Orchestration', () => {
|
|
164
|
+
test('should process multi-step workflow', async () => {
|
|
165
|
+
const workflow = {
|
|
166
|
+
workflow_id: 'multi-step-1',
|
|
167
|
+
steps: [
|
|
168
|
+
{
|
|
169
|
+
service: serviceName,
|
|
170
|
+
operation: 'sayHello',
|
|
171
|
+
input: { name: 'Step1' }
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
service: serviceName,
|
|
175
|
+
operation: 'sayHello',
|
|
176
|
+
input: { name: 'Step2' }
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
current_step: {
|
|
180
|
+
service: serviceName,
|
|
181
|
+
operation: 'sayHello',
|
|
182
|
+
input: { name: 'Step1' }
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
await mqClient.publish(workflow);
|
|
187
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
188
|
+
|
|
189
|
+
expect(true).toBe(true); // Placeholder
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Error Handling & Recovery', () => {
|
|
194
|
+
test('should retry on transient failures', async () => {
|
|
195
|
+
// Test retry logic
|
|
196
|
+
const testMessage = {
|
|
197
|
+
workflow_id: 'retry-test-1',
|
|
198
|
+
current_step: {
|
|
199
|
+
service: serviceName,
|
|
200
|
+
operation: 'triggerError',
|
|
201
|
+
input: {},
|
|
202
|
+
retry_count: 0,
|
|
203
|
+
max_retries: 3
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
await mqClient.publish(testMessage);
|
|
208
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
209
|
+
|
|
210
|
+
// Verify retries occurred
|
|
211
|
+
expect(true).toBe(true); // Placeholder
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('should handle circuit breaker', async () => {
|
|
215
|
+
// Send multiple failing requests to trigger circuit breaker
|
|
216
|
+
for (let i = 0; i < 5; i++) {
|
|
217
|
+
await mqClient.publish({
|
|
218
|
+
workflow_id: `circuit-test-${i}`,
|
|
219
|
+
current_step: {
|
|
220
|
+
service: serviceName,
|
|
221
|
+
operation: 'triggerError',
|
|
222
|
+
input: {}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
228
|
+
|
|
229
|
+
// Verify circuit breaker activated
|
|
230
|
+
expect(true).toBe(true); // Placeholder
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('Performance & Load Testing', () => {
|
|
235
|
+
test('should handle concurrent messages', async () => {
|
|
236
|
+
const messages = [];
|
|
237
|
+
for (let i = 0; i < 10; i++) {
|
|
238
|
+
messages.push({
|
|
239
|
+
workflow_id: `concurrent-${i}`,
|
|
240
|
+
current_step: {
|
|
241
|
+
service: serviceName,
|
|
242
|
+
operation: 'sayHello',
|
|
243
|
+
input: { name: `User${i}` }
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Send all messages concurrently
|
|
249
|
+
await Promise.all(messages.map(msg => mqClient.publish(msg)));
|
|
250
|
+
|
|
251
|
+
// Wait for all to process
|
|
252
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
253
|
+
|
|
254
|
+
expect(true).toBe(true); // Placeholder
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('should respect prefetch limit', async () => {
|
|
258
|
+
// Send more messages than prefetch limit
|
|
259
|
+
const messages = [];
|
|
260
|
+
for (let i = 0; i < 20; i++) {
|
|
261
|
+
messages.push({
|
|
262
|
+
workflow_id: `prefetch-${i}`,
|
|
263
|
+
current_step: {
|
|
264
|
+
service: serviceName,
|
|
265
|
+
operation: 'sayHello',
|
|
266
|
+
input: { name: `Batch${i}` }
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await Promise.all(messages.map(msg => mqClient.publish(msg)));
|
|
272
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
273
|
+
|
|
274
|
+
// Verify prefetch was respected
|
|
275
|
+
expect(true).toBe(true); // Placeholder
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('Registry Integration', () => {
|
|
280
|
+
test('should register service on startup', () => {
|
|
281
|
+
// Verify service was registered
|
|
282
|
+
expect(serviceWrapper.isRunning).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('should send heartbeats', async () => {
|
|
286
|
+
// Wait for at least one heartbeat
|
|
287
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
288
|
+
|
|
289
|
+
// Verify heartbeat was sent (check logs or registry)
|
|
290
|
+
expect(true).toBe(true); // Placeholder
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|