@alt-javascript/camel-lite-core 1.0.2

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,132 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { CamelContext, ProducerTemplate, Exchange } from '../src/index.js';
4
+ import { DirectComponent } from '@alt-javascript/camel-lite-component-direct';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function makeContext() {
11
+ const ctx = new CamelContext();
12
+ ctx.addComponent('direct', new DirectComponent());
13
+ return ctx;
14
+ }
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Unit tests (no running context)
18
+ // ---------------------------------------------------------------------------
19
+
20
+ describe('ProducerTemplate: constructor', () => {
21
+ it('throws when no context provided', () => {
22
+ assert.throws(() => new ProducerTemplate(null), /requires a CamelContext/);
23
+ });
24
+ });
25
+
26
+ describe('ProducerTemplate: sendBody (unit)', () => {
27
+ it('throws for unknown scheme', async () => {
28
+ const ctx = makeContext();
29
+ const pt = new ProducerTemplate(ctx);
30
+ await assert.rejects(
31
+ () => pt.sendBody('unknown:foo', 'body'),
32
+ /no component registered for scheme 'unknown'/
33
+ );
34
+ });
35
+
36
+ it('throws for URI with no scheme', async () => {
37
+ const ctx = makeContext();
38
+ const pt = new ProducerTemplate(ctx);
39
+ await assert.rejects(
40
+ () => pt.sendBody('nodots', 'body'),
41
+ /invalid URI/
42
+ );
43
+ });
44
+ });
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Integration tests (live context with direct: component)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ describe('ProducerTemplate: sendBody integration', () => {
51
+ let ctx;
52
+
53
+ before(async () => {
54
+ ctx = makeContext();
55
+ const { RouteBuilder } = await import('../src/RouteBuilder.js');
56
+ const builder = new RouteBuilder();
57
+ builder.from('direct:upper').process(ex => {
58
+ ex.in.body = ex.in.body.toUpperCase();
59
+ });
60
+ ctx.addRoutes(builder);
61
+ await ctx.start();
62
+ });
63
+
64
+ after(async () => {
65
+ await ctx.stop();
66
+ });
67
+
68
+ it('sendBody dispatches through the route pipeline', async () => {
69
+ const pt = new ProducerTemplate(ctx);
70
+ const exchange = await pt.sendBody('direct:upper', 'hello');
71
+ assert.equal(exchange.in.body, 'HELLO');
72
+ assert.equal(exchange.exception, null);
73
+ });
74
+
75
+ it('sendBody passes headers through to the route', async () => {
76
+ const { RouteBuilder } = await import('../src/RouteBuilder.js');
77
+ const builder = new RouteBuilder();
78
+ builder.from('direct:echoHeader').process(ex => {
79
+ ex.in.body = ex.in.getHeader('x-test');
80
+ });
81
+ ctx.addRoutes(builder);
82
+ // no restart needed — direct: registers consumer on addRoutes start()
83
+ // but context is already started so we need to start consumer manually
84
+ // Actually the route is registered but not started — we need a second context start
85
+ // or use the consumer directly. Simpler: test header forwarding via existing route.
86
+ const pt = new ProducerTemplate(ctx);
87
+ const exchange = await pt.sendBody('direct:upper', 'world', { 'x-val': '42' });
88
+ assert.equal(exchange.in.getHeader('x-val'), '42');
89
+ });
90
+
91
+ it('sendBody returns exchange with no exception on success', async () => {
92
+ const pt = new ProducerTemplate(ctx);
93
+ const exchange = await pt.sendBody('direct:upper', 'test');
94
+ assert.equal(exchange.isFailed(), false);
95
+ });
96
+ });
97
+
98
+ describe('ProducerTemplate: requestBody integration', () => {
99
+ let ctx;
100
+
101
+ before(async () => {
102
+ ctx = makeContext();
103
+ const { RouteBuilder } = await import('../src/RouteBuilder.js');
104
+ const builder = new RouteBuilder();
105
+ // Route sets out.body explicitly (InOut response pattern)
106
+ builder.from('direct:echo-out').process(ex => {
107
+ ex.out.body = `echo:${ex.in.body}`;
108
+ });
109
+ // Route only mutates in.body (in-place pattern — requestBody falls back)
110
+ builder.from('direct:mutate-in').process(ex => {
111
+ ex.in.body = `mutated:${ex.in.body}`;
112
+ });
113
+ ctx.addRoutes(builder);
114
+ await ctx.start();
115
+ });
116
+
117
+ after(async () => {
118
+ await ctx.stop();
119
+ });
120
+
121
+ it('returns out.body when route sets it', async () => {
122
+ const pt = new ProducerTemplate(ctx);
123
+ const result = await pt.requestBody('direct:echo-out', 'ping');
124
+ assert.equal(result, 'echo:ping');
125
+ });
126
+
127
+ it('falls back to in.body when out.body is not set', async () => {
128
+ const pt = new ProducerTemplate(ctx);
129
+ const result = await pt.requestBody('direct:mutate-in', 'data');
130
+ assert.equal(result, 'mutated:data');
131
+ });
132
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { CamelContext } from '../src/CamelContext.js';
4
+
5
+ describe('CamelContext', () => {
6
+ it('constructs without error', () => {
7
+ const ctx = new CamelContext();
8
+ assert.ok(ctx instanceof CamelContext);
9
+ });
10
+
11
+ it('started is false initially', () => {
12
+ const ctx = new CamelContext();
13
+ assert.equal(ctx.started, false);
14
+ });
15
+
16
+ it('start() is async and sets started to true', async () => {
17
+ const ctx = new CamelContext();
18
+ await ctx.start();
19
+ assert.equal(ctx.started, true);
20
+ });
21
+
22
+ it('stop() sets started to false', async () => {
23
+ const ctx = new CamelContext();
24
+ await ctx.start();
25
+ await ctx.stop();
26
+ assert.equal(ctx.started, false);
27
+ });
28
+
29
+ it('addComponent returns this (fluent)', () => {
30
+ const ctx = new CamelContext();
31
+ const stub = {};
32
+ const result = ctx.addComponent('test', stub);
33
+ assert.equal(result, ctx);
34
+ });
35
+
36
+ it('getComponent returns the registered component', () => {
37
+ const ctx = new CamelContext();
38
+ const stub = { name: 'stubComponent' };
39
+ ctx.addComponent('direct', stub);
40
+ assert.equal(ctx.getComponent('direct'), stub);
41
+ });
42
+
43
+ it('addComponent is chainable', () => {
44
+ const ctx = new CamelContext();
45
+ const a = { id: 'a' };
46
+ const b = { id: 'b' };
47
+ ctx.addComponent('a', a).addComponent('b', b);
48
+ assert.equal(ctx.getComponent('a'), a);
49
+ assert.equal(ctx.getComponent('b'), b);
50
+ });
51
+ });
52
+
53
+ describe('CamelContext bean registry', () => {
54
+ it('registerBean/getBean round-trips a bean by name', () => {
55
+ const ctx = new CamelContext();
56
+ const db = { type: 'sqlite' };
57
+ ctx.registerBean('myDb', db);
58
+ assert.equal(ctx.getBean('myDb'), db);
59
+ });
60
+
61
+ it('getBean returns undefined for unknown name', () => {
62
+ const ctx = new CamelContext();
63
+ assert.equal(ctx.getBean('ghost'), undefined);
64
+ });
65
+
66
+ it('getBeans returns all registered beans as [name, bean] pairs', () => {
67
+ const ctx = new CamelContext();
68
+ const db1 = { id: 1 };
69
+ const db2 = { id: 2 };
70
+ ctx.registerBean('ds1', db1);
71
+ ctx.registerBean('ds2', db2);
72
+ const entries = ctx.getBeans();
73
+ assert.equal(entries.length, 2);
74
+ assert.deepEqual(entries.find(([n]) => n === 'ds1')?.[1], db1);
75
+ assert.deepEqual(entries.find(([n]) => n === 'ds2')?.[1], db2);
76
+ });
77
+
78
+ it('getBeans returns empty array when no beans registered', () => {
79
+ const ctx = new CamelContext();
80
+ assert.deepEqual(ctx.getBeans(), []);
81
+ });
82
+
83
+ it('registerBean is fluent (returns context)', () => {
84
+ const ctx = new CamelContext();
85
+ const result = ctx.registerBean('x', {});
86
+ assert.equal(result, ctx);
87
+ });
88
+
89
+ it('registerBean overwrites existing bean with same name', () => {
90
+ const ctx = new CamelContext();
91
+ const orig = { v: 1 };
92
+ const updated = { v: 2 };
93
+ ctx.registerBean('key', orig);
94
+ ctx.registerBean('key', updated);
95
+ assert.equal(ctx.getBean('key'), updated);
96
+ });
97
+ });
@@ -0,0 +1,375 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ Exchange, CamelContext, RouteDefinition,
5
+ simple, js, constant,
6
+ } from '../src/index.js';
7
+ import { Pipeline } from '../src/Pipeline.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Helper: compile a RouteDefinition to a Pipeline and run it against an exchange
11
+ // ---------------------------------------------------------------------------
12
+
13
+ function makeExchange(body) {
14
+ const ex = new Exchange();
15
+ ex.in.body = body;
16
+ return ex;
17
+ }
18
+
19
+ async function run(routeDef, exchange, context = null) {
20
+ const pipeline = routeDef.compile(context);
21
+ await pipeline.run(exchange);
22
+ return exchange;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // constant() expression
27
+ // ---------------------------------------------------------------------------
28
+
29
+ describe('constant() expression', () => {
30
+ it('always returns the given value regardless of exchange', async () => {
31
+ const fn = constant('hello');
32
+ assert.equal(typeof fn._fn, 'function');
33
+ assert.equal(fn._fn(), 'hello');
34
+ assert.equal(fn._fn({ whatever: true }), 'hello');
35
+ });
36
+
37
+ it('works with normaliseExpression', () => {
38
+ // constant returns a _camelExpr object compatible with normaliseExpression
39
+ const expr = constant(42);
40
+ assert.ok(expr._camelExpr);
41
+ assert.equal(expr._fn(), 42);
42
+ });
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // setBody
47
+ // ---------------------------------------------------------------------------
48
+
49
+ describe('setBody()', () => {
50
+ it('replaces body with constant expression', async () => {
51
+ const route = new RouteDefinition('direct:test');
52
+ route.setBody(constant('hello'));
53
+ const ex = makeExchange('original');
54
+ await run(route, ex);
55
+ assert.equal(ex.in.body, 'hello');
56
+ });
57
+
58
+ it('replaces body with simple expression referencing a header', async () => {
59
+ const route = new RouteDefinition('direct:test');
60
+ route.setBody(simple('${header.X-Type}'));
61
+ const ex = makeExchange('original');
62
+ ex.in.setHeader('X-Type', 'order');
63
+ await run(route, ex);
64
+ assert.equal(ex.in.body, 'order');
65
+ });
66
+
67
+ it('replaces body with js expression', async () => {
68
+ const route = new RouteDefinition('direct:test');
69
+ route.setBody(js('exchange.in.body.toUpperCase()'));
70
+ const ex = makeExchange('hello');
71
+ await run(route, ex);
72
+ assert.equal(ex.in.body, 'HELLO');
73
+ });
74
+ });
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // setHeader
78
+ // ---------------------------------------------------------------------------
79
+
80
+ describe('setHeader()', () => {
81
+ it('sets a header with constant value', async () => {
82
+ const route = new RouteDefinition('direct:test');
83
+ route.setHeader('X-Source', constant('camel-lite'));
84
+ const ex = makeExchange('body');
85
+ await run(route, ex);
86
+ assert.equal(ex.in.getHeader('X-Source'), 'camel-lite');
87
+ });
88
+
89
+ it('sets a header from body via simple expression', async () => {
90
+ const route = new RouteDefinition('direct:test');
91
+ route.setHeader('X-Body', simple('${body}'));
92
+ const ex = makeExchange('my-value');
93
+ await run(route, ex);
94
+ assert.equal(ex.in.getHeader('X-Body'), 'my-value');
95
+ });
96
+
97
+ it('does not modify exchange body', async () => {
98
+ const route = new RouteDefinition('direct:test');
99
+ route.setHeader('X-Foo', constant('bar'));
100
+ const ex = makeExchange('unchanged');
101
+ await run(route, ex);
102
+ assert.equal(ex.in.body, 'unchanged');
103
+ });
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // setProperty
108
+ // ---------------------------------------------------------------------------
109
+
110
+ describe('setProperty()', () => {
111
+ it('sets an exchange property with js expression', async () => {
112
+ const route = new RouteDefinition('direct:test');
113
+ route.setBody(constant({ name: 'Alice' }));
114
+ route.setProperty('saved', js('exchange.in.body'));
115
+ const ex = makeExchange(null);
116
+ await run(route, ex);
117
+ assert.deepEqual(ex.getProperty('saved'), { name: 'Alice' });
118
+ });
119
+
120
+ it('sets property with constant', async () => {
121
+ const route = new RouteDefinition('direct:test');
122
+ route.setProperty('version', constant(2));
123
+ const ex = makeExchange(null);
124
+ await run(route, ex);
125
+ assert.equal(ex.getProperty('version'), 2);
126
+ });
127
+ });
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // removeHeader
131
+ // ---------------------------------------------------------------------------
132
+
133
+ describe('removeHeader()', () => {
134
+ it('removes a named header from the exchange', async () => {
135
+ const route = new RouteDefinition('direct:test');
136
+ route.removeHeader('X-Temp');
137
+ const ex = makeExchange('body');
138
+ ex.in.setHeader('X-Temp', 'to-be-removed');
139
+ await run(route, ex);
140
+ assert.equal(ex.in.getHeader('X-Temp'), undefined);
141
+ });
142
+
143
+ it('does not throw when header is absent', async () => {
144
+ const route = new RouteDefinition('direct:test');
145
+ route.removeHeader('X-Missing');
146
+ const ex = makeExchange('body');
147
+ await assert.doesNotReject(() => run(route, ex));
148
+ });
149
+ });
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // log
153
+ // ---------------------------------------------------------------------------
154
+
155
+ describe('log()', () => {
156
+ it('does not throw and does not modify exchange body', async () => {
157
+ const route = new RouteDefinition('direct:test');
158
+ route.log('hello from route');
159
+ const ex = makeExchange('original-body');
160
+ await assert.doesNotReject(() => run(route, ex));
161
+ assert.equal(ex.in.body, 'original-body');
162
+ });
163
+
164
+ it('accepts a simple() expression as message', async () => {
165
+ // simple() expressions must be pure expressions, not text with embedded values.
166
+ // For log, use a js() expression to build a formatted message string.
167
+ const route = new RouteDefinition('direct:test');
168
+ route.log(js('`body is ${exchange.in.body}`'));
169
+ const ex = makeExchange('test-value');
170
+ await assert.doesNotReject(() => run(route, ex));
171
+ });
172
+ });
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // marshal / unmarshal
176
+ // ---------------------------------------------------------------------------
177
+
178
+ describe('marshal()', () => {
179
+ it('serialises object body to JSON string', async () => {
180
+ const route = new RouteDefinition('direct:test');
181
+ route.marshal('json');
182
+ const ex = makeExchange({ name: 'Widget', price: 9.99 });
183
+ await run(route, ex);
184
+ assert.equal(typeof ex.in.body, 'string');
185
+ const parsed = JSON.parse(ex.in.body);
186
+ assert.equal(parsed.name, 'Widget');
187
+ });
188
+
189
+ it('defaults to json format', async () => {
190
+ const route = new RouteDefinition('direct:test');
191
+ route.marshal(); // no arg
192
+ const ex = makeExchange({ x: 1 });
193
+ await run(route, ex);
194
+ assert.equal(ex.in.body, '{"x":1}');
195
+ });
196
+ });
197
+
198
+ describe('unmarshal()', () => {
199
+ it('deserialises JSON string to object', async () => {
200
+ const route = new RouteDefinition('direct:test');
201
+ route.unmarshal('json');
202
+ const ex = makeExchange('{"name":"Gadget","price":24.99}');
203
+ await run(route, ex);
204
+ assert.equal(ex.in.body.name, 'Gadget');
205
+ assert.equal(ex.in.body.price, 24.99);
206
+ });
207
+ });
208
+
209
+ describe('marshal → unmarshal round-trip', () => {
210
+ it('round-trips an object through JSON serialisation', async () => {
211
+ const original = { id: 1, tags: ['a', 'b'], nested: { x: true } };
212
+ const route = new RouteDefinition('direct:test');
213
+ route.marshal().unmarshal();
214
+ const ex = makeExchange(original);
215
+ await run(route, ex);
216
+ assert.deepEqual(ex.in.body, original);
217
+ });
218
+ });
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // convertBodyTo
222
+ // ---------------------------------------------------------------------------
223
+
224
+ describe('convertBodyTo()', () => {
225
+ it('converts number to String', async () => {
226
+ const route = new RouteDefinition('direct:test');
227
+ route.convertBodyTo('String');
228
+ const ex = makeExchange(42);
229
+ await run(route, ex);
230
+ assert.equal(ex.in.body, '42');
231
+ assert.equal(typeof ex.in.body, 'string');
232
+ });
233
+
234
+ it('converts string to Number', async () => {
235
+ const route = new RouteDefinition('direct:test');
236
+ route.convertBodyTo('Number');
237
+ const ex = makeExchange('3.14');
238
+ await run(route, ex);
239
+ assert.equal(ex.in.body, 3.14);
240
+ assert.equal(typeof ex.in.body, 'number');
241
+ });
242
+
243
+ it('converts string "true" to Boolean true', async () => {
244
+ const route = new RouteDefinition('direct:test');
245
+ route.convertBodyTo('Boolean');
246
+ const ex = makeExchange('true');
247
+ await run(route, ex);
248
+ assert.equal(ex.in.body, true);
249
+ });
250
+
251
+ it('converts string "false" to Boolean false', async () => {
252
+ const route = new RouteDefinition('direct:test');
253
+ route.convertBodyTo('Boolean');
254
+ const ex = makeExchange('false');
255
+ await run(route, ex);
256
+ assert.equal(ex.in.body, false);
257
+ });
258
+
259
+ it('throws on unsupported type', async () => {
260
+ const route = new RouteDefinition('direct:test');
261
+ route.convertBodyTo('Date');
262
+ const ex = makeExchange('2024-01-01');
263
+ await run(route, ex);
264
+ // Pipeline captures errors on exchange.exception
265
+ assert.ok(ex.exception != null, 'exchange should have an exception');
266
+ assert.match(ex.exception.message, /unsupported type 'Date'/i);
267
+ });
268
+ });
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // stop
272
+ // ---------------------------------------------------------------------------
273
+
274
+ describe('stop()', () => {
275
+ it('stops exchange processing cleanly — body not modified by subsequent steps', async () => {
276
+ const route = new RouteDefinition('direct:test');
277
+ route
278
+ .setBody(constant('before-stop'))
279
+ .stop()
280
+ .setBody(constant('after-stop')); // should not execute
281
+
282
+ const ex = makeExchange('initial');
283
+ // Pipeline swallows CamelFilterStopException — no exception to caller
284
+ await assert.doesNotReject(() => run(route, ex));
285
+ assert.equal(ex.in.body, 'before-stop');
286
+ });
287
+ });
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // bean
291
+ // ---------------------------------------------------------------------------
292
+
293
+ describe('bean() with function', () => {
294
+ it('executes a function processor', async () => {
295
+ const route = new RouteDefinition('direct:test');
296
+ route.bean(async (exchange) => { exchange.in.body = 'from-bean-fn'; });
297
+ const ex = makeExchange('original');
298
+ await run(route, ex);
299
+ assert.equal(ex.in.body, 'from-bean-fn');
300
+ });
301
+ });
302
+
303
+ describe('bean() with object', () => {
304
+ it('executes an object with process() method', async () => {
305
+ const processor = {
306
+ async process(exchange) { exchange.in.body = 'from-bean-obj'; },
307
+ };
308
+ const route = new RouteDefinition('direct:test');
309
+ route.bean(processor);
310
+ const ex = makeExchange('original');
311
+ await run(route, ex);
312
+ assert.equal(ex.in.body, 'from-bean-obj');
313
+ });
314
+ });
315
+
316
+ describe('bean() with string name (context lookup)', () => {
317
+ it('looks up bean from context at runtime and executes it', async () => {
318
+ const ctx = new CamelContext();
319
+ ctx.registerBean('myProc', async (exchange) => { exchange.in.body = 'from-ctx-bean'; });
320
+
321
+ const route = new RouteDefinition('direct:test');
322
+ route.bean('myProc');
323
+ const ex = makeExchange('original');
324
+ const pipeline = route.compile(ctx);
325
+ await pipeline.run(ex);
326
+ assert.equal(ex.in.body, 'from-ctx-bean');
327
+ });
328
+
329
+ it('throws descriptively when bean not found in context', async () => {
330
+ const ctx = new CamelContext();
331
+ const route = new RouteDefinition('direct:test');
332
+ route.bean('ghost');
333
+ const ex = makeExchange('x');
334
+ const pipeline = route.compile(ctx);
335
+ await pipeline.run(ex);
336
+ // Pipeline captures error on exchange.exception
337
+ assert.ok(ex.exception != null, 'exchange should have an exception');
338
+ assert.match(ex.exception.message, /bean\('ghost'\).*no bean registered/i);
339
+ });
340
+ });
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Chained pipeline integration
344
+ // ---------------------------------------------------------------------------
345
+
346
+ describe('chained new DSL steps integration', () => {
347
+ it('setHeader → setBody(simple) → setProperty → marshal → unmarshal round-trip', async () => {
348
+ const ctx = new CamelContext();
349
+ const route = new RouteDefinition('direct:test');
350
+ route
351
+ .setHeader('X-Type', constant('invoice'))
352
+ .setBody(simple('${header.X-Type}'))
353
+ .setProperty('type', js('exchange.in.body'))
354
+ .setBody(constant({ amount: 100, currency: 'USD' }))
355
+ .marshal()
356
+ .unmarshal();
357
+
358
+ const ex = makeExchange(null);
359
+ const pipeline = route.compile(ctx);
360
+ await pipeline.run(ex);
361
+
362
+ assert.equal(ex.in.getHeader('X-Type'), 'invoice');
363
+ assert.equal(ex.getProperty('type'), 'invoice');
364
+ assert.equal(ex.in.body.amount, 100);
365
+ assert.equal(ex.in.body.currency, 'USD');
366
+ });
367
+
368
+ it('convertBodyTo after marshal converts JSON string to String type (no-op)', async () => {
369
+ const route = new RouteDefinition('direct:test');
370
+ route.marshal().convertBodyTo('String');
371
+ const ex = makeExchange({ x: 1 });
372
+ await run(route, ex);
373
+ assert.equal(typeof ex.in.body, 'string');
374
+ });
375
+ });