@onlineapps/service-wrapper 2.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,170 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Integration test for ServiceWrapper with OrchestratorConnector
6
+ * Tests that the thin orchestration layer properly delegates to orchestrator
7
+ */
8
+
9
+ const path = require('path');
10
+
11
+ // Mock setup
12
+ const Module = require('module');
13
+ const originalRequire = Module.prototype.require;
14
+
15
+ // Create test doubles
16
+ const testDoubles = {
17
+ processWorkflowMessage: null,
18
+ lastMessage: null,
19
+ lastServiceName: null
20
+ };
21
+
22
+ Module.prototype.require = function(id) {
23
+ switch(id) {
24
+ case '@onlineapps/conn-infra-mq':
25
+ return class MockMQ {
26
+ constructor() { this.connected = false; }
27
+ async connect() { this.connected = true; }
28
+ async disconnect() { this.connected = false; }
29
+ async consume(handler) { this.handler = handler; }
30
+ isConnected() { return this.connected; }
31
+ };
32
+
33
+ case '@onlineapps/conn-orch-registry':
34
+ return {
35
+ ServiceRegistryClient: class {
36
+ async register() { return true; }
37
+ async unregister() { return true; }
38
+ async sendHeartbeat() { return true; }
39
+ isConnected() { return true; }
40
+ }
41
+ };
42
+
43
+ case '@onlineapps/conn-base-logger':
44
+ return {
45
+ create: () => ({
46
+ info: () => {},
47
+ error: () => {},
48
+ warn: () => {},
49
+ debug: () => {}
50
+ })
51
+ };
52
+
53
+ case '@onlineapps/conn-orch-orchestrator':
54
+ return {
55
+ create: (options) => ({
56
+ processWorkflowMessage: async (message, serviceName) => {
57
+ testDoubles.lastMessage = message;
58
+ testDoubles.lastServiceName = serviceName;
59
+ testDoubles.processWorkflowMessage = true;
60
+ return { processed: true };
61
+ }
62
+ })
63
+ };
64
+
65
+ case '@onlineapps/conn-orch-api-mapper':
66
+ return { create: () => ({ map: () => {} }) };
67
+
68
+ case '@onlineapps/conn-orch-cookbook':
69
+ return { validateCookbook: () => true };
70
+
71
+ case '@onlineapps/conn-base-cache':
72
+ return class {
73
+ async connect() {}
74
+ async disconnect() {}
75
+ };
76
+
77
+ case '@onlineapps/conn-infra-error-handler':
78
+ return class {
79
+ constructor() {}
80
+ };
81
+
82
+ default:
83
+ return originalRequire.apply(this, arguments);
84
+ }
85
+ };
86
+
87
+ // Run test
88
+ async function testOrchestratorIntegration() {
89
+ console.log('=== Orchestrator Integration Test ===\n');
90
+
91
+ try {
92
+ // Load ServiceWrapper with mocked dependencies
93
+ const ServiceWrapper = require('../../src/ServiceWrapper');
94
+
95
+ // Create instance
96
+ const wrapper = new ServiceWrapper({
97
+ service: function expressApp() {},
98
+ serviceName: 'test-service',
99
+ openApiSpec: {
100
+ openapi: '3.0.0',
101
+ info: { version: '1.0.0' },
102
+ paths: {}
103
+ },
104
+ config: {}
105
+ });
106
+
107
+ // Start wrapper
108
+ await wrapper.start();
109
+ console.log('✓ ServiceWrapper started');
110
+
111
+ // Get MQ client handler
112
+ const mqClient = wrapper.mqClient;
113
+ if (!mqClient.handler) {
114
+ throw new Error('No message handler registered');
115
+ }
116
+
117
+ // Simulate workflow message
118
+ const testMessage = {
119
+ workflow_id: 'wf-123',
120
+ current_step: {
121
+ id: 'step-1',
122
+ service: 'test-service',
123
+ operation: 'testOp',
124
+ input: { data: 'test' }
125
+ },
126
+ context: {
127
+ variables: {},
128
+ results: {}
129
+ }
130
+ };
131
+
132
+ // Process message through the handler
133
+ console.log('Sending test message to handler...');
134
+ await mqClient.handler(testMessage);
135
+
136
+ // Verify orchestrator was called
137
+ if (testDoubles.processWorkflowMessage) {
138
+ console.log('✓ Orchestrator.processWorkflowMessage was called');
139
+ console.log('✓ Message passed:', JSON.stringify(testDoubles.lastMessage, null, 2));
140
+ console.log('✓ Service name passed:', testDoubles.lastServiceName);
141
+
142
+ // Verify correct delegation
143
+ if (testDoubles.lastMessage === testMessage &&
144
+ testDoubles.lastServiceName === 'test-service') {
145
+ console.log('✓ Message and service name correctly delegated');
146
+ } else {
147
+ console.log('✗ Incorrect delegation parameters');
148
+ }
149
+ } else {
150
+ console.log('✗ Orchestrator.processWorkflowMessage was NOT called');
151
+ }
152
+
153
+ // Stop wrapper
154
+ await wrapper.stop();
155
+ console.log('✓ ServiceWrapper stopped');
156
+
157
+ console.log('\n=== Test Result ===');
158
+ console.log('✅ Orchestrator integration working correctly!');
159
+ console.log('ServiceWrapper properly delegates workflow messages to orchestrator.');
160
+
161
+ } catch (error) {
162
+ console.error('\n=== Test Failed ===');
163
+ console.error('✗ Error:', error.message);
164
+ console.error('Stack:', error.stack);
165
+ process.exit(1);
166
+ }
167
+ }
168
+
169
+ // Run the test
170
+ testOrchestratorIntegration();
@@ -0,0 +1,304 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Mock connectors for unit testing
5
+ * These mocks allow us to test ServiceWrapper in isolation
6
+ */
7
+
8
+ // Mock MQ Connector
9
+ class MockMQConnector {
10
+ constructor(config) {
11
+ this.config = config;
12
+ this.connected = false;
13
+ this.consumers = new Map();
14
+ this.publishedMessages = [];
15
+ }
16
+
17
+ async connect() {
18
+ this.connected = true;
19
+ return Promise.resolve();
20
+ }
21
+
22
+ async disconnect() {
23
+ this.connected = false;
24
+ return Promise.resolve();
25
+ }
26
+
27
+ isConnected() {
28
+ return this.connected;
29
+ }
30
+
31
+ async consume(handler, options) {
32
+ this.consumers.set(options.queue, handler);
33
+ return Promise.resolve();
34
+ }
35
+
36
+ async publish(queue, message) {
37
+ this.publishedMessages.push({ queue, message });
38
+ return Promise.resolve();
39
+ }
40
+
41
+ // Test helper - simulate message arrival
42
+ async simulateMessage(queue, message) {
43
+ const handler = this.consumers.get(queue);
44
+ if (handler) {
45
+ await handler(message);
46
+ }
47
+ }
48
+ }
49
+
50
+ // Mock Registry Connector
51
+ class MockServiceRegistryClient {
52
+ constructor(config) {
53
+ this.config = config;
54
+ this.registeredServices = new Map();
55
+ this.heartbeats = [];
56
+ this.connected = true;
57
+ }
58
+
59
+ async register(serviceInfo) {
60
+ this.registeredServices.set(serviceInfo.name, serviceInfo);
61
+ return Promise.resolve();
62
+ }
63
+
64
+ async unregister(serviceName) {
65
+ this.registeredServices.delete(serviceName);
66
+ return Promise.resolve();
67
+ }
68
+
69
+ async sendHeartbeat(serviceName) {
70
+ this.heartbeats.push({ service: serviceName, timestamp: Date.now() });
71
+ return Promise.resolve();
72
+ }
73
+
74
+ isConnected() {
75
+ return this.connected;
76
+ }
77
+ }
78
+
79
+ // Mock Logger Connector
80
+ class MockLogger {
81
+ constructor(serviceName) {
82
+ this.serviceName = serviceName;
83
+ this.logs = [];
84
+ }
85
+
86
+ _log(level, message, meta) {
87
+ this.logs.push({ level, message, meta, timestamp: Date.now() });
88
+ }
89
+
90
+ info(message, meta) {
91
+ this._log('info', message, meta);
92
+ }
93
+
94
+ warn(message, meta) {
95
+ this._log('warn', message, meta);
96
+ }
97
+
98
+ error(message, meta) {
99
+ this._log('error', message, meta);
100
+ }
101
+
102
+ debug(message, meta) {
103
+ this._log('debug', message, meta);
104
+ }
105
+
106
+ // Test helper
107
+ getLogsByLevel(level) {
108
+ return this.logs.filter(log => log.level === level);
109
+ }
110
+ }
111
+
112
+ // Add static create method to the class itself and as a property
113
+ MockLogger.create = function(serviceName) {
114
+ return new MockLogger(serviceName);
115
+ };
116
+
117
+ // Mock Orchestrator Connector
118
+ class MockWorkflowOrchestrator {
119
+ constructor(config) {
120
+ this.config = config;
121
+ this.processedMessages = [];
122
+ this.shouldFail = false;
123
+ }
124
+
125
+ async processWorkflowMessage(message, serviceName) {
126
+ this.processedMessages.push({ message, serviceName });
127
+
128
+ if (this.shouldFail) {
129
+ throw new Error('Mock processing failure');
130
+ }
131
+
132
+ return {
133
+ success: true,
134
+ workflow_id: message.workflow_id,
135
+ step_id: message.current_step,
136
+ result: { mocked: true }
137
+ };
138
+ }
139
+
140
+ // Test helper - make next processing fail
141
+ setNextProcessingToFail(shouldFail = true) {
142
+ this.shouldFail = shouldFail;
143
+ }
144
+ }
145
+
146
+ const MockOrchestratorConnector = {
147
+ create: (config) => new MockWorkflowOrchestrator(config)
148
+ };
149
+
150
+ // Mock API Mapper Connector
151
+ class MockApiMapper {
152
+ constructor(config) {
153
+ this.config = config;
154
+ this.operations = new Map();
155
+ }
156
+
157
+ async callOperation(operationId, input, context) {
158
+ return {
159
+ operationId,
160
+ input,
161
+ context,
162
+ result: 'mocked'
163
+ };
164
+ }
165
+ }
166
+
167
+ const MockApiMapperConnector = {
168
+ create: (config) => new MockApiMapper(config)
169
+ };
170
+
171
+ // Mock Cookbook Connector
172
+ const MockCookbookConnector = {
173
+ validateCookbook: (cookbook) => {
174
+ if (!cookbook || !cookbook.steps) {
175
+ throw new Error('Invalid cookbook');
176
+ }
177
+ return true;
178
+ },
179
+
180
+ createRouter: (mqClient, registryClient, options) => {
181
+ return {
182
+ routeToService: async (service, message) => {
183
+ return Promise.resolve();
184
+ },
185
+ routeToDLQ: async (cookbook, context, error, service) => {
186
+ return Promise.resolve();
187
+ }
188
+ };
189
+ },
190
+
191
+ CookbookExecutor: class {
192
+ constructor(cookbook, options) {
193
+ this.cookbook = cookbook;
194
+ this.options = options;
195
+ this.context = {
196
+ setInput: (input) => {},
197
+ data: { steps: {} }
198
+ };
199
+ }
200
+
201
+ async executeStep(step) {
202
+ return { executed: step.id };
203
+ }
204
+ },
205
+
206
+ CookbookGenerator: class {
207
+ constructor(options) {
208
+ this.options = options;
209
+ }
210
+
211
+ generate(openApiSpec) {
212
+ return { generated: true };
213
+ }
214
+ },
215
+
216
+ ResponseMapper: class {
217
+ mapResponse(response, mapping) {
218
+ return response;
219
+ }
220
+ }
221
+ };
222
+
223
+ /**
224
+ * MockCacheConnector - Simulates cache connector
225
+ */
226
+ class MockCacheConnector {
227
+ constructor(options = {}) {
228
+ this.options = options;
229
+ this.connected = false;
230
+ }
231
+
232
+ async connect() {
233
+ this.connected = true;
234
+ return Promise.resolve();
235
+ }
236
+
237
+ async disconnect() {
238
+ this.connected = false;
239
+ return Promise.resolve();
240
+ }
241
+
242
+ async get(key) {
243
+ return null;
244
+ }
245
+
246
+ async set(key, value, ttl) {
247
+ return 'OK';
248
+ }
249
+
250
+ async del(key) {
251
+ return 1;
252
+ }
253
+
254
+ isConnected() {
255
+ return this.connected;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * MockErrorHandlerConnector - Simulates error handler
261
+ */
262
+ class MockErrorHandlerConnector {
263
+ constructor(options = {}) {
264
+ this.maxRetries = options.maxRetries || 3;
265
+ this.retryDelay = options.retryDelay || 1000;
266
+ this.logger = options.logger;
267
+ }
268
+
269
+ async executeWithRetry(fn, context = {}) {
270
+ try {
271
+ return await fn();
272
+ } catch (error) {
273
+ if (this.logger) {
274
+ this.logger.error('Error in executeWithRetry', { error: error.message });
275
+ }
276
+ throw error;
277
+ }
278
+ }
279
+
280
+ handleError(error, context = {}) {
281
+ if (this.logger) {
282
+ this.logger.error('Handling error', { error: error.message, context });
283
+ }
284
+ return {
285
+ error: error.message,
286
+ retry: false
287
+ };
288
+ }
289
+
290
+ isRetryableError(error) {
291
+ return error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT';
292
+ }
293
+ }
294
+
295
+ module.exports = {
296
+ MockMQConnector,
297
+ MockServiceRegistryClient,
298
+ MockLogger,
299
+ MockOrchestratorConnector,
300
+ MockApiMapperConnector,
301
+ MockCookbookConnector,
302
+ MockCacheConnector,
303
+ MockErrorHandlerConnector
304
+ };
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Test runner that uses mocked dependencies
6
+ * Allows us to test service-wrapper without actual connectors
7
+ */
8
+
9
+ const path = require('path');
10
+
11
+ // Setup module aliases for mocked connectors
12
+ const mocks = require('./mocks/connectors');
13
+
14
+ const Module = require('module');
15
+ const originalRequire = Module.prototype.require;
16
+
17
+ Module.prototype.require = function(id) {
18
+ // Intercept connector imports and return appropriate mocks
19
+ switch(id) {
20
+ case '@onlineapps/conn-infra-mq':
21
+ return mocks.MockMQConnector;
22
+ case '@onlineapps/conn-orch-registry':
23
+ return { ServiceRegistryClient: mocks.MockServiceRegistryClient };
24
+ case '@onlineapps/conn-base-logger':
25
+ return mocks.MockLogger;
26
+ case '@onlineapps/conn-orch-orchestrator':
27
+ return mocks.MockOrchestratorConnector;
28
+ case '@onlineapps/conn-orch-api-mapper':
29
+ return mocks.MockApiMapperConnector;
30
+ case '@onlineapps/conn-orch-cookbook':
31
+ return mocks.MockCookbookConnector;
32
+ case '@onlineapps/conn-base-cache':
33
+ return mocks.MockCacheConnector;
34
+ case '@onlineapps/conn-infra-error-handler':
35
+ return mocks.MockErrorHandlerConnector;
36
+ default:
37
+ return originalRequire.apply(this, arguments);
38
+ }
39
+ };
40
+
41
+ // Now we can test the ServiceWrapper with mocked dependencies
42
+ console.log('Testing ServiceWrapper with mocked connectors...\n');
43
+
44
+ // Test 1: Basic instantiation
45
+ try {
46
+ const ServiceWrapper = require('../src/ServiceWrapper');
47
+ const wrapper = new ServiceWrapper({
48
+ service: function() {},
49
+ serviceName: 'test-service',
50
+ openApiSpec: { openapi: '3.0.0', paths: {} }
51
+ });
52
+ console.log('✓ ServiceWrapper instantiation works');
53
+ } catch (error) {
54
+ console.log('✗ ServiceWrapper instantiation failed:', error.message);
55
+ }
56
+
57
+ // Test 2: Check what connectors are imported
58
+ try {
59
+ const ServiceWrapper = require('../src/ServiceWrapper');
60
+ console.log('✓ All connector imports resolved');
61
+ } catch (error) {
62
+ console.log('✗ Failed to import connectors:', error.message);
63
+ }
64
+
65
+ // Test 3: Test start/stop lifecycle
66
+ async function testLifecycle() {
67
+ try {
68
+ const ServiceWrapper = require('../src/ServiceWrapper');
69
+ const wrapper = new ServiceWrapper({
70
+ service: function() {},
71
+ serviceName: 'test-service',
72
+ openApiSpec: { openapi: '3.0.0', paths: {} }
73
+ });
74
+
75
+ await wrapper.start();
76
+ console.log('✓ ServiceWrapper.start() works');
77
+
78
+ const status = wrapper.getStatus();
79
+ console.log('✓ ServiceWrapper.getStatus() returns:', status);
80
+
81
+ await wrapper.stop();
82
+ console.log('✓ ServiceWrapper.stop() works');
83
+ } catch (error) {
84
+ console.log('✗ Lifecycle test failed:', error.message);
85
+ }
86
+ }
87
+
88
+ // Test 4: Check for missing functionality
89
+ function checkMissingFunctionality() {
90
+ console.log('\n=== Missing Functionality Check ===');
91
+
92
+ const missing = [];
93
+
94
+ // Check for cache connector
95
+ try {
96
+ require('@onlineapps/conn-base-cache');
97
+ } catch (e) {
98
+ missing.push('conn-base-cache');
99
+ }
100
+
101
+ // Check for error handler
102
+ try {
103
+ require('@onlineapps/conn-infra-error-handler');
104
+ } catch (e) {
105
+ missing.push('conn-infra-error-handler');
106
+ }
107
+
108
+ // Check cookbook connector exports
109
+ const cookbook = require('./mocks/connectors').MockCookbookConnector;
110
+ const requiredExports = ['validateCookbook', 'createRouter', 'CookbookExecutor', 'CookbookGenerator', 'ResponseMapper'];
111
+ const missingExports = requiredExports.filter(exp => !cookbook[exp]);
112
+
113
+ if (missingExports.length > 0) {
114
+ missing.push(`cookbook missing: ${missingExports.join(', ')}`);
115
+ }
116
+
117
+ if (missing.length > 0) {
118
+ console.log('Missing connectors/functionality:');
119
+ missing.forEach(m => console.log(` - ${m}`));
120
+ } else {
121
+ console.log('All required functionality is available (mocked)');
122
+ }
123
+ }
124
+
125
+ // Run tests
126
+ testLifecycle().then(() => {
127
+ checkMissingFunctionality();
128
+ console.log('\n=== Test Summary ===');
129
+ console.log('Service-wrapper is working as a thin orchestration layer!');
130
+ console.log('Next steps:');
131
+ console.log('1. Implement conn-base-cache connector');
132
+ console.log('2. Implement conn-infra-error-handler connector');
133
+ console.log('3. Fix cookbook connector exports');
134
+ console.log('4. Test with real connectors');
135
+ });
package/test/setup.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Jest setup file
5
+ * Configure test environment
6
+ */
7
+
8
+ // Set test environment variables
9
+ process.env.NODE_ENV = 'test';
10
+ process.env.LOG_LEVEL = 'error'; // Reduce noise during tests
11
+
12
+ // Global test timeout
13
+ jest.setTimeout(10000);
14
+
15
+ // Mock timers for tests that need them
16
+ global.mockTimers = () => {
17
+ jest.useFakeTimers();
18
+ };
19
+
20
+ global.restoreTimers = () => {
21
+ jest.useRealTimers();
22
+ };
23
+
24
+ // Helper to wait for async operations
25
+ global.waitForAsync = () => new Promise(resolve => setImmediate(resolve));
26
+
27
+ // Suppress console errors in tests unless debugging
28
+ if (!process.env.DEBUG_TESTS) {
29
+ global.console.error = jest.fn();
30
+ global.console.warn = jest.fn();
31
+ }