@onlineapps/conn-orch-validator 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.
Files changed (37) hide show
  1. package/README.md +78 -0
  2. package/TESTING_STRATEGY.md +92 -0
  3. package/docs/DESIGN.md +134 -0
  4. package/examples/service-wrapper-usage.js +250 -0
  5. package/examples/three-tier-testing.js +144 -0
  6. package/jest.config.js +23 -0
  7. package/onlineapps-conn-e2e-testing-1.0.0.tgz +0 -0
  8. package/package.json +43 -0
  9. package/src/CookbookTestRunner.js +434 -0
  10. package/src/CookbookTestUtils.js +237 -0
  11. package/src/ServiceReadinessValidator.js +430 -0
  12. package/src/ServiceTestHarness.js +256 -0
  13. package/src/ServiceValidator.js +387 -0
  14. package/src/TestOrchestrator.js +727 -0
  15. package/src/ValidationOrchestrator.js +506 -0
  16. package/src/WorkflowTestRunner.js +396 -0
  17. package/src/helpers/README.md +235 -0
  18. package/src/helpers/createPreValidationTests.js +321 -0
  19. package/src/helpers/createServiceReadinessTests.js +245 -0
  20. package/src/index.js +62 -0
  21. package/src/mocks/MockMQClient.js +176 -0
  22. package/src/mocks/MockRegistry.js +164 -0
  23. package/src/mocks/MockStorage.js +186 -0
  24. package/src/validators/ServiceStructureValidator.js +487 -0
  25. package/src/validators/ValidationProofGenerator.js +79 -0
  26. package/test-mq-flow.js +72 -0
  27. package/test-orchestrator.js +95 -0
  28. package/tests/component/testing-framework-integration.test.js +313 -0
  29. package/tests/integration/ServiceReadiness.test.js +265 -0
  30. package/tests/monitoring-e2e.test.js +315 -0
  31. package/tests/run-example.js +257 -0
  32. package/tests/unit/CookbookTestRunner.test.js +353 -0
  33. package/tests/unit/MockMQClient.test.js +190 -0
  34. package/tests/unit/MockRegistry.test.js +233 -0
  35. package/tests/unit/MockStorage.test.js +257 -0
  36. package/tests/unit/ServiceValidator.test.js +429 -0
  37. package/tests/unit/WorkflowTestRunner.test.js +546 -0
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Service Structure Validator
3
+ *
4
+ * Validates that a business service has correct directory structure,
5
+ * configuration files, and follows OA Drive standards.
6
+ *
7
+ * Used by test helpers to provide clear error messages when something is wrong.
8
+ *
9
+ * @module validators/ServiceStructureValidator
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ class ServiceStructureValidator {
18
+ constructor(serviceRoot) {
19
+ this.serviceRoot = serviceRoot;
20
+ this.errors = [];
21
+ this.warnings = [];
22
+ this.info = [];
23
+ }
24
+
25
+ /**
26
+ * Validate complete service structure
27
+ *
28
+ * @returns {Object} Validation result
29
+ */
30
+ validate() {
31
+ this.errors = [];
32
+ this.warnings = [];
33
+ this.info = [];
34
+
35
+ this.info.push(`Validating service structure: ${this.serviceRoot}`);
36
+
37
+ // 1. Validate directory structure
38
+ this.validateDirectoryStructure();
39
+
40
+ // 2. Validate configuration files
41
+ this.validateConfigurationFiles();
42
+
43
+ // 3. Validate package.json
44
+ this.validatePackageJson();
45
+
46
+ // 4. Validate source code structure
47
+ this.validateSourceStructure();
48
+
49
+ // 5. Validate test structure
50
+ this.validateTestStructure();
51
+
52
+ const valid = this.errors.length === 0;
53
+
54
+ return {
55
+ valid,
56
+ errors: this.errors,
57
+ warnings: this.warnings,
58
+ info: this.info
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Validate directory structure exists
64
+ */
65
+ validateDirectoryStructure() {
66
+ const requiredDirs = [
67
+ { path: 'conn-config', description: 'Configuration directory' },
68
+ { path: 'src', description: 'Source code directory' },
69
+ { path: 'tests', description: 'Tests directory' }
70
+ ];
71
+
72
+ const recommendedDirs = [
73
+ { path: 'tests/unit', description: 'Unit tests' },
74
+ { path: 'tests/integration', description: 'Integration tests' },
75
+ { path: 'tests/cookbooks', description: 'Cookbook tests' }
76
+ ];
77
+
78
+ for (const dir of requiredDirs) {
79
+ const fullPath = path.join(this.serviceRoot, dir.path);
80
+ if (!fs.existsSync(fullPath)) {
81
+ this.errors.push({
82
+ type: 'MISSING_DIRECTORY',
83
+ path: dir.path,
84
+ message: `Required directory missing: ${dir.path}`,
85
+ description: dir.description,
86
+ fix: `Create directory: mkdir -p ${dir.path}`
87
+ });
88
+ } else {
89
+ this.info.push(`✓ Found ${dir.description}: ${dir.path}`);
90
+ }
91
+ }
92
+
93
+ for (const dir of recommendedDirs) {
94
+ const fullPath = path.join(this.serviceRoot, dir.path);
95
+ if (!fs.existsSync(fullPath)) {
96
+ this.warnings.push({
97
+ type: 'MISSING_RECOMMENDED_DIRECTORY',
98
+ path: dir.path,
99
+ message: `Recommended directory missing: ${dir.path}`,
100
+ description: dir.description,
101
+ fix: `Create directory: mkdir -p ${dir.path}`
102
+ });
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Validate configuration files
109
+ */
110
+ validateConfigurationFiles() {
111
+ // 1. Validate conn-config/config.json
112
+ const configPath = path.join(this.serviceRoot, 'conn-config/config.json');
113
+ if (!fs.existsSync(configPath)) {
114
+ this.errors.push({
115
+ type: 'MISSING_CONFIG',
116
+ path: 'conn-config/config.json',
117
+ message: 'Service configuration missing: conn-config/config.json',
118
+ fix: 'Create config.json with service metadata. See: /docs/standards/SERVICE_TEMPLATE.md'
119
+ });
120
+ } else {
121
+ try {
122
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
123
+ this.validateConfigStructure(config);
124
+ this.info.push('✓ Found valid config.json');
125
+ } catch (error) {
126
+ this.errors.push({
127
+ type: 'INVALID_CONFIG',
128
+ path: 'conn-config/config.json',
129
+ message: `Invalid JSON in config.json: ${error.message}`,
130
+ fix: 'Fix JSON syntax errors in config.json'
131
+ });
132
+ }
133
+ }
134
+
135
+ // 2. Validate conn-config/operations.json
136
+ const operationsPath = path.join(this.serviceRoot, 'conn-config/operations.json');
137
+ if (!fs.existsSync(operationsPath)) {
138
+ this.errors.push({
139
+ type: 'MISSING_OPERATIONS',
140
+ path: 'conn-config/operations.json',
141
+ message: 'Operations specification missing: conn-config/operations.json',
142
+ fix: 'Create operations.json. See: /docs/standards/OPERATIONS.md'
143
+ });
144
+ } else {
145
+ try {
146
+ const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf-8'));
147
+ this.validateOperationsStructure(operations);
148
+ this.info.push('✓ Found valid operations.json');
149
+ } catch (error) {
150
+ this.errors.push({
151
+ type: 'INVALID_OPERATIONS',
152
+ path: 'conn-config/operations.json',
153
+ message: `Invalid JSON in operations.json: ${error.message}`,
154
+ fix: 'Fix JSON syntax errors in operations.json'
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Validate config.json structure
162
+ */
163
+ validateConfigStructure(config) {
164
+ // Required fields
165
+ const requiredFields = [
166
+ 'service.name',
167
+ 'service.version',
168
+ 'service.port'
169
+ ];
170
+
171
+ for (const field of requiredFields) {
172
+ const parts = field.split('.');
173
+ let value = config;
174
+ for (const part of parts) {
175
+ value = value?.[part];
176
+ }
177
+
178
+ if (!value) {
179
+ this.errors.push({
180
+ type: 'MISSING_CONFIG_FIELD',
181
+ path: 'conn-config/config.json',
182
+ field: field,
183
+ message: `Required field missing in config.json: ${field}`,
184
+ fix: `Add "${field}" to config.json`
185
+ });
186
+ }
187
+ }
188
+
189
+ // Validate service name format
190
+ if (config.service?.name) {
191
+ if (!/^[a-z][a-z0-9-]*$/.test(config.service.name)) {
192
+ this.warnings.push({
193
+ type: 'INVALID_SERVICE_NAME',
194
+ path: 'conn-config/config.json',
195
+ field: 'service.name',
196
+ value: config.service.name,
197
+ message: `Service name should be lowercase kebab-case: ${config.service.name}`,
198
+ fix: 'Use lowercase letters, numbers, and hyphens only (e.g., "my-service")'
199
+ });
200
+ }
201
+ }
202
+
203
+ // Validate version format
204
+ if (config.service?.version) {
205
+ if (!/^\d+\.\d+\.\d+$/.test(config.service.version)) {
206
+ this.warnings.push({
207
+ type: 'INVALID_VERSION',
208
+ path: 'conn-config/config.json',
209
+ field: 'service.version',
210
+ value: config.service.version,
211
+ message: `Version should follow semantic versioning: ${config.service.version}`,
212
+ fix: 'Use format: MAJOR.MINOR.PATCH (e.g., "1.0.0")'
213
+ });
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Validate operations.json structure
220
+ */
221
+ validateOperationsStructure(operations) {
222
+ if (!operations.operations) {
223
+ this.errors.push({
224
+ type: 'INVALID_OPERATIONS_STRUCTURE',
225
+ path: 'conn-config/operations.json',
226
+ message: 'operations.json must have "operations" key',
227
+ fix: 'Wrap operations in {"operations": {...}}. See: /docs/standards/OPERATIONS.md'
228
+ });
229
+ return;
230
+ }
231
+
232
+ const ops = operations.operations;
233
+ if (typeof ops !== 'object' || Array.isArray(ops)) {
234
+ this.errors.push({
235
+ type: 'INVALID_OPERATIONS_TYPE',
236
+ path: 'conn-config/operations.json',
237
+ message: 'operations must be an object',
238
+ fix: 'operations should be key-value pairs: {"operation-name": {...}}'
239
+ });
240
+ return;
241
+ }
242
+
243
+ if (Object.keys(ops).length === 0) {
244
+ this.warnings.push({
245
+ type: 'NO_OPERATIONS',
246
+ path: 'conn-config/operations.json',
247
+ message: 'No operations defined',
248
+ fix: 'Add at least one operation to operations.json'
249
+ });
250
+ return;
251
+ }
252
+
253
+ // Validate each operation
254
+ for (const [operationName, operationSpec] of Object.entries(ops)) {
255
+ this.validateOperation(operationName, operationSpec);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Validate single operation structure
261
+ */
262
+ validateOperation(name, spec) {
263
+ const requiredFields = ['endpoint', 'method'];
264
+
265
+ for (const field of requiredFields) {
266
+ if (!spec[field]) {
267
+ this.errors.push({
268
+ type: 'MISSING_OPERATION_FIELD',
269
+ path: 'conn-config/operations.json',
270
+ operation: name,
271
+ field: field,
272
+ message: `Operation "${name}" missing required field: ${field}`,
273
+ fix: `Add "${field}" to operation "${name}"`
274
+ });
275
+ }
276
+ }
277
+
278
+ // Validate endpoint format
279
+ if (spec.endpoint && !spec.endpoint.startsWith('/')) {
280
+ this.errors.push({
281
+ type: 'INVALID_ENDPOINT',
282
+ path: 'conn-config/operations.json',
283
+ operation: name,
284
+ field: 'endpoint',
285
+ value: spec.endpoint,
286
+ message: `Endpoint must start with /: ${spec.endpoint}`,
287
+ fix: `Change endpoint to: /${spec.endpoint}`
288
+ });
289
+ }
290
+
291
+ // Validate HTTP method
292
+ const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
293
+ if (spec.method && !validMethods.includes(spec.method)) {
294
+ this.errors.push({
295
+ type: 'INVALID_HTTP_METHOD',
296
+ path: 'conn-config/operations.json',
297
+ operation: name,
298
+ field: 'method',
299
+ value: spec.method,
300
+ message: `Invalid HTTP method: ${spec.method}`,
301
+ fix: `Use one of: ${validMethods.join(', ')}`
302
+ });
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Validate package.json
308
+ */
309
+ validatePackageJson() {
310
+ const packagePath = path.join(this.serviceRoot, 'package.json');
311
+ if (!fs.existsSync(packagePath)) {
312
+ this.errors.push({
313
+ type: 'MISSING_PACKAGE_JSON',
314
+ path: 'package.json',
315
+ message: 'package.json missing',
316
+ fix: 'Create package.json: npm init'
317
+ });
318
+ return;
319
+ }
320
+
321
+ try {
322
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
323
+
324
+ // Check for required scripts
325
+ const recommendedScripts = [
326
+ { name: 'test', description: 'Run tests' },
327
+ { name: 'test:unit', description: 'Run unit tests' },
328
+ { name: 'test:integration', description: 'Run integration tests' },
329
+ { name: 'test:cookbooks', description: 'Run pre-validation cookbook tests' }
330
+ ];
331
+
332
+ for (const script of recommendedScripts) {
333
+ if (!pkg.scripts?.[script.name]) {
334
+ this.warnings.push({
335
+ type: 'MISSING_NPM_SCRIPT',
336
+ path: 'package.json',
337
+ script: script.name,
338
+ message: `Recommended npm script missing: ${script.name}`,
339
+ description: script.description,
340
+ fix: `Add to package.json scripts: "${script.name}": "..."`
341
+ });
342
+ }
343
+ }
344
+
345
+ // Check for @onlineapps dependencies
346
+ const requiredDeps = [
347
+ '@onlineapps/service-wrapper',
348
+ '@onlineapps/conn-orch-validator'
349
+ ];
350
+
351
+ for (const dep of requiredDeps) {
352
+ const hasDep = pkg.dependencies?.[dep] || pkg.devDependencies?.[dep];
353
+ if (!hasDep) {
354
+ this.warnings.push({
355
+ type: 'MISSING_DEPENDENCY',
356
+ path: 'package.json',
357
+ dependency: dep,
358
+ message: `Recommended dependency missing: ${dep}`,
359
+ fix: `Install: npm install ${dep}`
360
+ });
361
+ }
362
+ }
363
+
364
+ this.info.push('✓ Found valid package.json');
365
+ } catch (error) {
366
+ this.errors.push({
367
+ type: 'INVALID_PACKAGE_JSON',
368
+ path: 'package.json',
369
+ message: `Invalid JSON in package.json: ${error.message}`,
370
+ fix: 'Fix JSON syntax errors in package.json'
371
+ });
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Validate source code structure
377
+ */
378
+ validateSourceStructure() {
379
+ const appPath = path.join(this.serviceRoot, 'src/app.js');
380
+ if (!fs.existsSync(appPath)) {
381
+ this.errors.push({
382
+ type: 'MISSING_APP',
383
+ path: 'src/app.js',
384
+ message: 'Express application missing: src/app.js',
385
+ fix: 'Create src/app.js with Express app. See: /docs/standards/SERVICE_TEMPLATE.md'
386
+ });
387
+ } else {
388
+ this.info.push('✓ Found src/app.js');
389
+ }
390
+
391
+ const indexPath = path.join(this.serviceRoot, 'index.js');
392
+ if (!fs.existsSync(indexPath)) {
393
+ this.warnings.push({
394
+ type: 'MISSING_INDEX',
395
+ path: 'index.js',
396
+ message: 'Entry point missing: index.js',
397
+ fix: 'Create index.js as service entry point'
398
+ });
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Validate test structure
404
+ */
405
+ validateTestStructure() {
406
+ const cookbooksPath = path.join(this.serviceRoot, 'tests/cookbooks');
407
+ if (fs.existsSync(cookbooksPath)) {
408
+ const cookbookFiles = fs.readdirSync(cookbooksPath).filter(f => f.endsWith('.json'));
409
+ if (cookbookFiles.length === 0) {
410
+ this.warnings.push({
411
+ type: 'NO_COOKBOOK_TESTS',
412
+ path: 'tests/cookbooks',
413
+ message: 'No cookbook tests found in tests/cookbooks/',
414
+ fix: 'Create cookbook tests for pre-validation. See: /docs/standards/TESTING.md'
415
+ });
416
+ } else {
417
+ this.info.push(`✓ Found ${cookbookFiles.length} cookbook test(s)`);
418
+ }
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Format validation result as readable message
424
+ */
425
+ static formatResult(result) {
426
+ const lines = [];
427
+
428
+ lines.push('');
429
+ lines.push('═══════════════════════════════════════════════════════════════');
430
+ lines.push(' SERVICE STRUCTURE VALIDATION');
431
+ lines.push('═══════════════════════════════════════════════════════════════');
432
+ lines.push('');
433
+
434
+ if (result.errors.length === 0 && result.warnings.length === 0) {
435
+ lines.push('✅ ALL CHECKS PASSED');
436
+ lines.push('');
437
+ for (const info of result.info) {
438
+ lines.push(` ${info}`);
439
+ }
440
+ } else {
441
+ // Errors
442
+ if (result.errors.length > 0) {
443
+ lines.push(`❌ ERRORS (${result.errors.length}):`);
444
+ lines.push('');
445
+ for (const error of result.errors) {
446
+ lines.push(` ✗ ${error.message}`);
447
+ lines.push(` Type: ${error.type}`);
448
+ if (error.path) lines.push(` File: ${error.path}`);
449
+ if (error.field) lines.push(` Field: ${error.field}`);
450
+ if (error.value) lines.push(` Value: ${error.value}`);
451
+ lines.push(` Fix: ${error.fix}`);
452
+ lines.push('');
453
+ }
454
+ }
455
+
456
+ // Warnings
457
+ if (result.warnings.length > 0) {
458
+ lines.push(`⚠️ WARNINGS (${result.warnings.length}):`);
459
+ lines.push('');
460
+ for (const warning of result.warnings) {
461
+ lines.push(` ⚠ ${warning.message}`);
462
+ if (warning.fix) {
463
+ lines.push(` Suggestion: ${warning.fix}`);
464
+ }
465
+ lines.push('');
466
+ }
467
+ }
468
+
469
+ // Info
470
+ if (result.info.length > 0) {
471
+ lines.push('ℹ️ INFO:');
472
+ lines.push('');
473
+ for (const info of result.info) {
474
+ lines.push(` ${info}`);
475
+ }
476
+ lines.push('');
477
+ }
478
+ }
479
+
480
+ lines.push('═══════════════════════════════════════════════════════════════');
481
+ lines.push('');
482
+
483
+ return lines.join('\n');
484
+ }
485
+ }
486
+
487
+ module.exports = { ServiceStructureValidator };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const { ValidationProofCodec } = require('@onlineapps/service-validator-core');
4
+
5
+ /**
6
+ * ValidationProofGenerator - Generates validation proof from test results
7
+ *
8
+ * This is a THIN WRAPPER for use in testing workflows.
9
+ * All encoding logic is delegated to ValidationProofCodec.
10
+ *
11
+ * Responsibility: Bridge between test results and proof generation
12
+ * - Accept test results in test framework format
13
+ * - Transform to ValidationProofSchema format
14
+ * - Delegate encoding to ValidationProofCodec
15
+ */
16
+ class ValidationProofGenerator {
17
+ constructor(options = {}) {
18
+ this.serviceName = options.serviceName;
19
+ this.serviceVersion = options.serviceVersion;
20
+ this.validatorName = options.validatorName || '@onlineapps/conn-orch-validator';
21
+ this.validatorVersion = options.validatorVersion || '1.0.0';
22
+ }
23
+
24
+ /**
25
+ * Generate validation proof from test results
26
+ *
27
+ * @param {Object} testResults - Results from test execution
28
+ * @param {Object} dependencies - Service dependencies with versions
29
+ * @returns {Object} Validation proof with metadata
30
+ */
31
+ generateProof(testResults, dependencies = {}) {
32
+ // Transform test results to ValidationProofSchema format
33
+ const validationData = {
34
+ serviceName: this.serviceName,
35
+ version: this.serviceVersion,
36
+ validator: this.validatorName,
37
+ validatorVersion: this.validatorVersion,
38
+ validatedAt: new Date().toISOString(),
39
+ durationMs: testResults.duration || 0,
40
+ testsRun: testResults.total || 0,
41
+ testsPassed: testResults.passed || 0,
42
+ testsFailed: testResults.failed || 0,
43
+ dependencies: dependencies || {}
44
+ };
45
+
46
+ // Delegate encoding to centralized codec
47
+ // This ensures Generator and Verifier use EXACTLY the same algorithm
48
+ return ValidationProofCodec.encode(validationData);
49
+ }
50
+
51
+ /**
52
+ * Extract test results summary
53
+ */
54
+ extractTestSummary(testResults) {
55
+ return {
56
+ total: testResults.total || 0,
57
+ passed: testResults.passed || 0,
58
+ failed: testResults.failed || 0,
59
+ duration: testResults.duration || 0,
60
+ coverage: testResults.coverage || 0
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Create validation error
66
+ */
67
+ createValidationError(code, message, details = {}) {
68
+ return {
69
+ error: {
70
+ code,
71
+ message,
72
+ details,
73
+ timestamp: new Date().toISOString()
74
+ }
75
+ };
76
+ }
77
+ }
78
+
79
+ module.exports = ValidationProofGenerator;
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Simple test script to verify MQ message flow
6
+ * Usage: node test-mq-flow.js
7
+ */
8
+
9
+ const amqp = require('amqplib');
10
+
11
+ async function sendTestMessage() {
12
+ const rabbitUrl = 'amqp://guest:guest@localhost:33023';
13
+ const queueName = 'hello-service.workflow';
14
+
15
+ try {
16
+ // Connect to RabbitMQ
17
+ const connection = await amqp.connect(rabbitUrl);
18
+ const channel = await connection.createChannel();
19
+
20
+ // Ensure queue exists
21
+ await channel.assertQueue(queueName, { durable: true });
22
+
23
+ // Create test message with cookbook
24
+ const testMessage = {
25
+ workflow_id: `test-${Date.now()}`,
26
+ cookbook: {
27
+ name: 'test-workflow',
28
+ version: '1.0.0',
29
+ steps: [
30
+ {
31
+ id: 'goodDay', // ID se použije jako operation name
32
+ type: 'task',
33
+ service: 'hello-service',
34
+ input: {
35
+ name: 'E2E Test'
36
+ }
37
+ }
38
+ ]
39
+ },
40
+ current_step: 'step1',
41
+ step_index: 0,
42
+ context: {}
43
+ };
44
+
45
+ console.log('Sending test message:', JSON.stringify(testMessage, null, 2));
46
+
47
+ // Send message
48
+ channel.sendToQueue(
49
+ queueName,
50
+ Buffer.from(JSON.stringify(testMessage)),
51
+ { persistent: true }
52
+ );
53
+
54
+ console.log(`Message sent to queue: ${queueName}`);
55
+
56
+ // Wait a bit to ensure message is sent
57
+ await new Promise(resolve => setTimeout(resolve, 1000));
58
+
59
+ // Clean up
60
+ await channel.close();
61
+ await connection.close();
62
+
63
+ console.log('Test completed successfully');
64
+
65
+ } catch (error) {
66
+ console.error('Test failed:', error.message);
67
+ process.exit(1);
68
+ }
69
+ }
70
+
71
+ // Run the test
72
+ sendTestMessage();