@onlineapps/service-wrapper 2.0.6 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.0.6",
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-registry": "^1.1.4",
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-api-mapper": "^1.0.0",
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
- "jest": "^29.5.0",
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
  },
@@ -69,7 +69,7 @@ class ServiceWrapper {
69
69
  this.config = options.config || {};
70
70
 
71
71
  // Initialize connectors
72
- this.logger = LoggerConnector.create(this.serviceName);
72
+ this.logger = new LoggerConnector({ serviceName: this.serviceName });
73
73
  this.mqClient = null;
74
74
  this.registryClient = null;
75
75
  this.orchestrator = null;
@@ -298,8 +298,37 @@ class ServiceWrapper {
298
298
  // Subscribe to workflow messages
299
299
  await this.mqClient.consume(
300
300
  queueName,
301
- async (message) => {
301
+ async (rawMessage) => {
302
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
+
303
332
  // Delegate ALL processing to orchestrator
304
333
  const result = await this.orchestrator.processWorkflowMessage(
305
334
  message,
@@ -314,8 +343,8 @@ class ServiceWrapper {
314
343
 
315
344
  } catch (error) {
316
345
  this.logger.error('Message processing failed', {
317
- workflow_id: message.workflow_id,
318
- step: message.current_step,
346
+ workflow_id: message?.workflow_id || 'unknown',
347
+ step: message?.current_step || 'unknown',
319
348
  error: error.message
320
349
  });
321
350
  }
@@ -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
+ });