orator-http-proxy 1.0.1 → 1.0.4

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,1003 @@
1
+ /**
2
+ * Unit tests for Orator HTTP Proxy
3
+ *
4
+ * @license MIT
5
+ *
6
+ * @author Steven Velozo <steven@velozo.com>
7
+ */
8
+
9
+ const Chai = require("chai");
10
+ const Expect = Chai.expect;
11
+
12
+ const libFable = require('fable');
13
+ const libOrator = require('orator');
14
+ const libOratorHTTPProxy = require('../source/Orator-HTTP-Proxy.js');
15
+
16
+ const defaultFableSettings = (
17
+ {
18
+ Product: 'OratorHTTPProxy-Tests',
19
+ ProductVersion: '0.0.0',
20
+ APIServerPort: 0
21
+ });
22
+
23
+ /**
24
+ * Helper that creates a Fable + Orator + Proxy harness, starts the service, then calls back.
25
+ *
26
+ * @param {object} pFableSettings - Fable settings to merge with defaults.
27
+ * @param {object} pProxyOptions - Options for the proxy instance.
28
+ * @param {Function} fCallback - Called with the harness object after the service starts.
29
+ */
30
+ function createStartedHarness(pFableSettings, pProxyOptions, fCallback)
31
+ {
32
+ let tmpFableSettings = Object.assign({}, defaultFableSettings, pFableSettings || {});
33
+ let tmpFable = new libFable(tmpFableSettings);
34
+
35
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
36
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
37
+
38
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
39
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', pProxyOptions || {});
40
+
41
+ let tmpResult = (
42
+ {
43
+ fable: tmpFable,
44
+ orator: tmpOrator,
45
+ proxy: tmpProxy
46
+ });
47
+
48
+ tmpOrator.startService(
49
+ () =>
50
+ {
51
+ return fCallback(tmpResult);
52
+ });
53
+ }
54
+
55
+ suite
56
+ (
57
+ 'Orator HTTP Proxy',
58
+ () =>
59
+ {
60
+ suite
61
+ (
62
+ 'Object Sanity',
63
+ () =>
64
+ {
65
+ test
66
+ (
67
+ 'the class should initialize itself into a happy little object',
68
+ (fDone) =>
69
+ {
70
+ let tmpFable = new libFable(defaultFableSettings);
71
+
72
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
73
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
74
+
75
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
76
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', {});
77
+
78
+ Expect(tmpProxy).to.be.an('object', 'OratorHTTPProxy should initialize as an object.');
79
+ Expect(tmpProxy).to.have.a.property('connectProxyRoutes');
80
+ Expect(tmpProxy.connectProxyRoutes).to.be.a('function');
81
+ Expect(tmpProxy).to.have.a.property('httpProxyServer');
82
+ Expect(tmpProxy.httpProxyServer).to.be.an('object');
83
+
84
+ return fDone();
85
+ }
86
+ );
87
+
88
+ test
89
+ (
90
+ 'the proxy should have default configuration values when no options are provided',
91
+ (fDone) =>
92
+ {
93
+ let tmpFable = new libFable(defaultFableSettings);
94
+
95
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
96
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
97
+
98
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
99
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', {});
100
+
101
+ // Default log level should be 0
102
+ Expect(tmpProxy.LogLevel).to.equal(0);
103
+
104
+ // Default proxy destination URL should be loopback
105
+ Expect(tmpProxy.proxyDestinationURL).to.equal('http://127.0.0.1/');
106
+
107
+ // Default request prefix list should be the meadow default
108
+ Expect(tmpProxy.requestPrefixList).to.be.an('array');
109
+ Expect(tmpProxy.requestPrefixList).to.deep.equal(['/1.0/*']);
110
+
111
+ return fDone();
112
+ }
113
+ );
114
+
115
+ test
116
+ (
117
+ 'the proxy should accept DestinationURL and LogLevel via options',
118
+ (fDone) =>
119
+ {
120
+ let tmpFable = new libFable(defaultFableSettings);
121
+
122
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
123
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
124
+
125
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
126
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
127
+ {
128
+ LogLevel: 3,
129
+ DestinationURL: 'http://api.example.com:3000/'
130
+ });
131
+
132
+ Expect(tmpProxy.LogLevel).to.equal(3);
133
+ Expect(tmpProxy.proxyDestinationURL).to.equal('http://api.example.com:3000/');
134
+
135
+ return fDone();
136
+ }
137
+ );
138
+
139
+ test
140
+ (
141
+ 'the proxy should accept configuration via fable settings fallback',
142
+ (fDone) =>
143
+ {
144
+ let tmpFableSettings = Object.assign({}, defaultFableSettings,
145
+ {
146
+ OratorHTTPProxyLogLevel: 2,
147
+ OratorHTTPProxyDestinationURL: 'http://backend.local:8080/',
148
+ OratorHTTPProxyRequestPrefixList: ['/api/v1/*', '/api/v2/*']
149
+ });
150
+ let tmpFable = new libFable(tmpFableSettings);
151
+
152
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
153
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
154
+
155
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
156
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', {});
157
+
158
+ Expect(tmpProxy.LogLevel).to.equal(2);
159
+ Expect(tmpProxy.proxyDestinationURL).to.equal('http://backend.local:8080/');
160
+ Expect(tmpProxy.requestPrefixList).to.be.an('array');
161
+ Expect(tmpProxy.requestPrefixList).to.deep.equal(['/api/v1/*', '/api/v2/*']);
162
+
163
+ return fDone();
164
+ }
165
+ );
166
+
167
+ test
168
+ (
169
+ 'options should take precedence over fable settings',
170
+ (fDone) =>
171
+ {
172
+ let tmpFableSettings = Object.assign({}, defaultFableSettings,
173
+ {
174
+ OratorHTTPProxyLogLevel: 2,
175
+ OratorHTTPProxyDestinationURL: 'http://fallback.local/'
176
+ });
177
+ let tmpFable = new libFable(tmpFableSettings);
178
+
179
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
180
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
181
+
182
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
183
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
184
+ {
185
+ LogLevel: 5,
186
+ DestinationURL: 'http://primary.local/'
187
+ });
188
+
189
+ // Options values should win over fable settings
190
+ Expect(tmpProxy.LogLevel).to.equal(5);
191
+ Expect(tmpProxy.proxyDestinationURL).to.equal('http://primary.local/');
192
+
193
+ return fDone();
194
+ }
195
+ );
196
+
197
+ test
198
+ (
199
+ 'the httpProxyServer should be created as a real http-proxy instance',
200
+ (fDone) =>
201
+ {
202
+ let tmpFable = new libFable(defaultFableSettings);
203
+
204
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
205
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
206
+
207
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
208
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', {});
209
+
210
+ Expect(tmpProxy.httpProxyServer).to.be.an('object');
211
+ Expect(tmpProxy.httpProxyServer.web).to.be.a('function');
212
+ Expect(tmpProxy.httpProxyServer.ws).to.be.a('function');
213
+
214
+ return fDone();
215
+ }
216
+ );
217
+ }
218
+ );
219
+
220
+ suite
221
+ (
222
+ 'Proxy Route Connection',
223
+ () =>
224
+ {
225
+ test
226
+ (
227
+ 'connectProxyRoutes should register routes on the orator service server',
228
+ (fDone) =>
229
+ {
230
+ createStartedHarness(null,
231
+ {
232
+ DestinationURL: 'http://localhost:9999/'
233
+ },
234
+ (pHarness) =>
235
+ {
236
+ pHarness.proxy.connectProxyRoutes(['/api/v1/*']);
237
+
238
+ // Verify the route is registered by checking the IPC router can find it
239
+ let tmpHandler = pHarness.orator.serviceServer.router.find('GET', '/api/v1/users');
240
+ Expect(tmpHandler).to.be.an('object');
241
+ Expect(tmpHandler).to.have.a.property('handler');
242
+ Expect(tmpHandler.handler).to.be.a('function');
243
+
244
+ return fDone();
245
+ });
246
+ }
247
+ );
248
+
249
+ test
250
+ (
251
+ 'connectProxyRoutes should register routes for GET, PUT, POST and DELETE',
252
+ (fDone) =>
253
+ {
254
+ createStartedHarness(null,
255
+ {
256
+ DestinationURL: 'http://localhost:9999/'
257
+ },
258
+ (pHarness) =>
259
+ {
260
+ pHarness.proxy.connectProxyRoutes(['/proxy/*']);
261
+
262
+ let tmpExpectedMethods = ['GET', 'PUT', 'POST', 'DELETE'];
263
+
264
+ tmpExpectedMethods.forEach(
265
+ (pMethod) =>
266
+ {
267
+ let tmpHandler = pHarness.orator.serviceServer.router.find(pMethod, '/proxy/resource');
268
+ Expect(tmpHandler).to.be.an('object', `Route for ${pMethod} should be registered.`);
269
+ Expect(tmpHandler).to.have.a.property('handler');
270
+ });
271
+
272
+ return fDone();
273
+ });
274
+ }
275
+ );
276
+
277
+ test
278
+ (
279
+ 'connectProxyRoutes should register multiple prefixes at once',
280
+ (fDone) =>
281
+ {
282
+ createStartedHarness(null,
283
+ {
284
+ DestinationURL: 'http://localhost:9999/'
285
+ },
286
+ (pHarness) =>
287
+ {
288
+ pHarness.proxy.connectProxyRoutes(['/api/v1/*', '/api/v2/*', '/legacy/*']);
289
+
290
+ // Verify all three prefixes are registered
291
+ let tmpPrefixes = ['/api/v1/users', '/api/v2/items', '/legacy/data'];
292
+
293
+ tmpPrefixes.forEach(
294
+ (pPrefix) =>
295
+ {
296
+ let tmpHandler = pHarness.orator.serviceServer.router.find('GET', pPrefix);
297
+ Expect(tmpHandler).to.be.an('object', `Route for ${pPrefix} should be registered.`);
298
+ Expect(tmpHandler).to.have.a.property('handler');
299
+ });
300
+
301
+ return fDone();
302
+ });
303
+ }
304
+ );
305
+
306
+ test
307
+ (
308
+ 'connectProxyRoutes should accept a function that returns a prefix list',
309
+ (fDone) =>
310
+ {
311
+ createStartedHarness(null,
312
+ {
313
+ DestinationURL: 'http://localhost:9999/'
314
+ },
315
+ (pHarness) =>
316
+ {
317
+ let tmpPrefixFunction = () =>
318
+ {
319
+ return ['/dynamic/v1/*', '/dynamic/v2/*'];
320
+ };
321
+
322
+ pHarness.proxy.connectProxyRoutes(tmpPrefixFunction);
323
+
324
+ // Verify the dynamic routes are registered
325
+ let tmpHandler1 = pHarness.orator.serviceServer.router.find('GET', '/dynamic/v1/test');
326
+ Expect(tmpHandler1).to.be.an('object');
327
+ Expect(tmpHandler1).to.have.a.property('handler');
328
+
329
+ let tmpHandler2 = pHarness.orator.serviceServer.router.find('GET', '/dynamic/v2/test');
330
+ Expect(tmpHandler2).to.be.an('object');
331
+ Expect(tmpHandler2).to.have.a.property('handler');
332
+
333
+ return fDone();
334
+ });
335
+ }
336
+ );
337
+
338
+ test
339
+ (
340
+ 'connectProxyRoutes should fall back to default prefix list when no arguments are passed',
341
+ (fDone) =>
342
+ {
343
+ createStartedHarness(null,
344
+ {
345
+ DestinationURL: 'http://localhost:9999/'
346
+ },
347
+ (pHarness) =>
348
+ {
349
+ // Call with no arguments -- should use the constructor defaults ['/1.0/*']
350
+ pHarness.proxy.connectProxyRoutes();
351
+
352
+ let tmpHandler = pHarness.orator.serviceServer.router.find('GET', '/1.0/SomeEntity');
353
+ Expect(tmpHandler).to.be.an('object');
354
+ Expect(tmpHandler).to.have.a.property('handler');
355
+
356
+ return fDone();
357
+ });
358
+ }
359
+ );
360
+
361
+ test
362
+ (
363
+ 'connectProxyRoutes should register routes for a custom proxy URL parameter',
364
+ (fDone) =>
365
+ {
366
+ createStartedHarness(null,
367
+ {
368
+ DestinationURL: 'http://default-backend.local/'
369
+ },
370
+ (pHarness) =>
371
+ {
372
+ // Pass a custom proxy URL as the second parameter
373
+ pHarness.proxy.connectProxyRoutes(['/custom/*'], 'http://custom-backend.local:5000/');
374
+
375
+ // Verify the route is registered
376
+ let tmpHandler = pHarness.orator.serviceServer.router.find('GET', '/custom/resource');
377
+ Expect(tmpHandler).to.be.an('object');
378
+ Expect(tmpHandler).to.have.a.property('handler');
379
+
380
+ return fDone();
381
+ });
382
+ }
383
+ );
384
+
385
+ test
386
+ (
387
+ 'connectProxyRoutes should be callable multiple times to add routes incrementally',
388
+ (fDone) =>
389
+ {
390
+ createStartedHarness(null,
391
+ {
392
+ DestinationURL: 'http://localhost:9999/'
393
+ },
394
+ (pHarness) =>
395
+ {
396
+ // Register routes in two separate calls
397
+ pHarness.proxy.connectProxyRoutes(['/first-batch/*']);
398
+ pHarness.proxy.connectProxyRoutes(['/second-batch/*']);
399
+
400
+ let tmpHandler1 = pHarness.orator.serviceServer.router.find('GET', '/first-batch/resource');
401
+ Expect(tmpHandler1).to.be.an('object');
402
+ Expect(tmpHandler1).to.have.a.property('handler');
403
+
404
+ let tmpHandler2 = pHarness.orator.serviceServer.router.find('GET', '/second-batch/resource');
405
+ Expect(tmpHandler2).to.be.an('object');
406
+ Expect(tmpHandler2).to.have.a.property('handler');
407
+
408
+ return fDone();
409
+ });
410
+ }
411
+ );
412
+ }
413
+ );
414
+
415
+ suite
416
+ (
417
+ 'Proxy Request Behavior',
418
+ () =>
419
+ {
420
+ test
421
+ (
422
+ 'the proxy handler should call httpProxyServer.web with the correct target',
423
+ (fDone) =>
424
+ {
425
+ createStartedHarness(null,
426
+ {
427
+ DestinationURL: 'http://api.example.com:3000/'
428
+ },
429
+ (pHarness) =>
430
+ {
431
+ let tmpWebCalled = false;
432
+ let tmpCapturedOptions = null;
433
+
434
+ // Mock BEFORE connecting routes — the handler captures `this.httpProxyServer`
435
+ // at call time, so we can replace it before invocation
436
+ pHarness.proxy.httpProxyServer.web = (pRequest, pResponse, pOptions) =>
437
+ {
438
+ tmpWebCalled = true;
439
+ tmpCapturedOptions = pOptions;
440
+ };
441
+
442
+ pHarness.proxy.connectProxyRoutes(['/api/*']);
443
+
444
+ // Invoke the handler via the IPC router directly
445
+ // The proxy handler does NOT call fNext() on success (by design for real HTTP),
446
+ // so the returned promise will never resolve. We use setTimeout to check
447
+ // our mock assertions after the synchronous code has executed.
448
+ let tmpMockRequest = { method: 'GET', url: '/api/users' };
449
+ let tmpMockResponse = { send: () => {}, end: () => {} };
450
+
451
+ let tmpRouteHandler = pHarness.orator.serviceServer.router.find('GET', '/api/users');
452
+ tmpRouteHandler.handler(tmpMockRequest, tmpMockResponse, {});
453
+
454
+ setTimeout(
455
+ () =>
456
+ {
457
+ Expect(tmpWebCalled).to.equal(true);
458
+ Expect(tmpCapturedOptions.target).to.equal('http://api.example.com:3000/');
459
+ Expect(tmpCapturedOptions.secure).to.equal(false);
460
+ return fDone();
461
+ }, 100);
462
+ });
463
+ }
464
+ );
465
+
466
+ test
467
+ (
468
+ 'the proxy handler should merge custom httpProxyOptions',
469
+ (fDone) =>
470
+ {
471
+ createStartedHarness(null,
472
+ {
473
+ DestinationURL: 'http://localhost:9999/',
474
+ httpProxyOptions:
475
+ {
476
+ changeOrigin: true,
477
+ xfwd: true
478
+ }
479
+ },
480
+ (pHarness) =>
481
+ {
482
+ // Verify options are stored
483
+ Expect(pHarness.proxy.options.httpProxyOptions).to.be.an('object');
484
+ Expect(pHarness.proxy.options.httpProxyOptions.changeOrigin).to.equal(true);
485
+
486
+ let tmpCapturedOptions = null;
487
+ pHarness.proxy.httpProxyServer.web = (pRequest, pResponse, pOptions) =>
488
+ {
489
+ tmpCapturedOptions = pOptions;
490
+ };
491
+
492
+ pHarness.proxy.connectProxyRoutes(['/merged/*']);
493
+
494
+ let tmpMockRequest = { method: 'GET', url: '/merged/test' };
495
+ let tmpMockResponse = { send: () => {}, end: () => {} };
496
+
497
+ let tmpRouteHandler = pHarness.orator.serviceServer.router.find('GET', '/merged/test');
498
+ tmpRouteHandler.handler(tmpMockRequest, tmpMockResponse, {});
499
+
500
+ setTimeout(
501
+ () =>
502
+ {
503
+ Expect(tmpCapturedOptions).to.be.an('object');
504
+ Expect(tmpCapturedOptions.target).to.equal('http://localhost:9999/');
505
+ Expect(tmpCapturedOptions.secure).to.equal(false);
506
+ Expect(tmpCapturedOptions.changeOrigin).to.equal(true);
507
+ Expect(tmpCapturedOptions.xfwd).to.equal(true);
508
+ return fDone();
509
+ }, 100);
510
+ });
511
+ }
512
+ );
513
+
514
+ test
515
+ (
516
+ 'the proxy handler should pass the request URL through to httpProxyServer.web',
517
+ (fDone) =>
518
+ {
519
+ createStartedHarness(null,
520
+ {
521
+ DestinationURL: 'http://localhost:9999/'
522
+ },
523
+ (pHarness) =>
524
+ {
525
+ let tmpCapturedRequest = null;
526
+ pHarness.proxy.httpProxyServer.web = (pRequest, pResponse, pOptions) =>
527
+ {
528
+ tmpCapturedRequest = pRequest;
529
+ };
530
+
531
+ pHarness.proxy.connectProxyRoutes(['/1.0/*']);
532
+
533
+ let tmpMockRequest = { method: 'GET', url: '/1.0/Users/123' };
534
+ let tmpMockResponse = { send: () => {}, end: () => {} };
535
+
536
+ let tmpRouteHandler = pHarness.orator.serviceServer.router.find('GET', '/1.0/Users/123');
537
+ tmpRouteHandler.handler(tmpMockRequest, tmpMockResponse, {});
538
+
539
+ setTimeout(
540
+ () =>
541
+ {
542
+ Expect(tmpCapturedRequest).to.be.an('object');
543
+ Expect(tmpCapturedRequest.url).to.equal('/1.0/Users/123');
544
+ return fDone();
545
+ }, 100);
546
+ });
547
+ }
548
+ );
549
+
550
+ test
551
+ (
552
+ 'the proxy handler should use the custom URL when one is provided to connectProxyRoutes',
553
+ (fDone) =>
554
+ {
555
+ createStartedHarness(null,
556
+ {
557
+ DestinationURL: 'http://default.local/'
558
+ },
559
+ (pHarness) =>
560
+ {
561
+ let tmpCapturedTarget = null;
562
+ pHarness.proxy.httpProxyServer.web = (pRequest, pResponse, pOptions) =>
563
+ {
564
+ tmpCapturedTarget = pOptions.target;
565
+ };
566
+
567
+ pHarness.proxy.connectProxyRoutes(['/custom/*'], 'http://override.local:5000/');
568
+
569
+ let tmpMockRequest = { method: 'GET', url: '/custom/endpoint' };
570
+ let tmpMockResponse = { send: () => {}, end: () => {} };
571
+
572
+ let tmpRouteHandler = pHarness.orator.serviceServer.router.find('GET', '/custom/endpoint');
573
+ tmpRouteHandler.handler(tmpMockRequest, tmpMockResponse, {});
574
+
575
+ setTimeout(
576
+ () =>
577
+ {
578
+ Expect(tmpCapturedTarget).to.equal('http://override.local:5000/');
579
+ return fDone();
580
+ }, 100);
581
+ });
582
+ }
583
+ );
584
+
585
+ test
586
+ (
587
+ 'the proxy handler should use the constructor URL when no URL is provided to connectProxyRoutes',
588
+ (fDone) =>
589
+ {
590
+ createStartedHarness(null,
591
+ {
592
+ DestinationURL: 'http://configured.local:8080/'
593
+ },
594
+ (pHarness) =>
595
+ {
596
+ let tmpCapturedTarget = null;
597
+ pHarness.proxy.httpProxyServer.web = (pRequest, pResponse, pOptions) =>
598
+ {
599
+ tmpCapturedTarget = pOptions.target;
600
+ };
601
+
602
+ pHarness.proxy.connectProxyRoutes(['/default/*']);
603
+
604
+ let tmpMockRequest = { method: 'GET', url: '/default/endpoint' };
605
+ let tmpMockResponse = { send: () => {}, end: () => {} };
606
+
607
+ let tmpRouteHandler = pHarness.orator.serviceServer.router.find('GET', '/default/endpoint');
608
+ tmpRouteHandler.handler(tmpMockRequest, tmpMockResponse, {});
609
+
610
+ setTimeout(
611
+ () =>
612
+ {
613
+ Expect(tmpCapturedTarget).to.equal('http://configured.local:8080/');
614
+ return fDone();
615
+ }, 100);
616
+ });
617
+ }
618
+ );
619
+
620
+ test
621
+ (
622
+ 'the proxy should always set secure:false for HTTPS targets',
623
+ (fDone) =>
624
+ {
625
+ createStartedHarness(null,
626
+ {
627
+ DestinationURL: 'https://secure-api.example.com/'
628
+ },
629
+ (pHarness) =>
630
+ {
631
+ let tmpCapturedSecure = null;
632
+ pHarness.proxy.httpProxyServer.web = (pRequest, pResponse, pOptions) =>
633
+ {
634
+ tmpCapturedSecure = pOptions.secure;
635
+ };
636
+
637
+ pHarness.proxy.connectProxyRoutes(['/secure/*']);
638
+
639
+ let tmpMockRequest = { method: 'GET', url: '/secure/endpoint' };
640
+ let tmpMockResponse = { send: () => {}, end: () => {} };
641
+
642
+ let tmpRouteHandler = pHarness.orator.serviceServer.router.find('GET', '/secure/endpoint');
643
+ tmpRouteHandler.handler(tmpMockRequest, tmpMockResponse, {});
644
+
645
+ setTimeout(
646
+ () =>
647
+ {
648
+ Expect(tmpCapturedSecure).to.equal(false);
649
+ return fDone();
650
+ }, 100);
651
+ });
652
+ }
653
+ );
654
+ }
655
+ );
656
+
657
+ suite
658
+ (
659
+ 'Proxy Error Handling',
660
+ () =>
661
+ {
662
+ test
663
+ (
664
+ 'the proxy should catch errors thrown by httpProxyServer.web',
665
+ (fDone) =>
666
+ {
667
+ createStartedHarness(null,
668
+ {
669
+ DestinationURL: 'http://localhost:59999/'
670
+ },
671
+ (pHarness) =>
672
+ {
673
+ pHarness.proxy.connectProxyRoutes(['/error-test/*']);
674
+
675
+ let tmpErrorLogged = false;
676
+
677
+ pHarness.proxy.httpProxyServer.web = (pRequest, pResponse, pOptions) =>
678
+ {
679
+ throw new Error('Simulated proxy connection failure');
680
+ };
681
+
682
+ let tmpMockRequest = { method: 'GET', url: '/error-test/resource' };
683
+ let tmpMockResponse =
684
+ {
685
+ send: () => {},
686
+ end: (pData) =>
687
+ {
688
+ // The catch block calls pResponse.end(JSON.stringify(pError))
689
+ tmpErrorLogged = true;
690
+ }
691
+ };
692
+
693
+ let tmpRouteHandler = pHarness.orator.serviceServer.router.find('GET', '/error-test/resource');
694
+ tmpRouteHandler.handler(tmpMockRequest, tmpMockResponse, {}).then(
695
+ () =>
696
+ {
697
+ // The error path calls fNext() after end()
698
+ Expect(tmpErrorLogged).to.equal(true);
699
+ return fDone();
700
+ }).catch(
701
+ (pError) =>
702
+ {
703
+ // May propagate depending on IPC internals
704
+ Expect(tmpErrorLogged).to.equal(true);
705
+ return fDone();
706
+ });
707
+ });
708
+ }
709
+ );
710
+
711
+ test
712
+ (
713
+ 'the proxy error handler should log the error with the request URL',
714
+ (fDone) =>
715
+ {
716
+ createStartedHarness(null,
717
+ {
718
+ DestinationURL: 'http://localhost:59999/'
719
+ },
720
+ (pHarness) =>
721
+ {
722
+ pHarness.proxy.connectProxyRoutes(['/log-error/*']);
723
+
724
+ let tmpErrorMessage = null;
725
+ let tmpOriginalLogError = pHarness.proxy.log.error.bind(pHarness.proxy.log);
726
+ pHarness.proxy.log.error = (pMessage, pData) =>
727
+ {
728
+ tmpErrorMessage = pMessage;
729
+ tmpOriginalLogError(pMessage, pData);
730
+ };
731
+
732
+ pHarness.proxy.httpProxyServer.web = (pRequest, pResponse, pOptions) =>
733
+ {
734
+ throw new Error('Connection refused');
735
+ };
736
+
737
+ let tmpMockRequest = { method: 'GET', url: '/log-error/resource' };
738
+ let tmpMockResponse = { send: () => {}, end: () => {} };
739
+
740
+ let tmpRouteHandler = pHarness.orator.serviceServer.router.find('GET', '/log-error/resource');
741
+ tmpRouteHandler.handler(tmpMockRequest, tmpMockResponse, {}).then(
742
+ () =>
743
+ {
744
+ Expect(tmpErrorMessage).to.be.a('string');
745
+ Expect(tmpErrorMessage).to.include('/log-error/resource');
746
+ Expect(tmpErrorMessage).to.include('Connection refused');
747
+ return fDone();
748
+ }).catch(
749
+ (pError) =>
750
+ {
751
+ if (tmpErrorMessage)
752
+ {
753
+ Expect(tmpErrorMessage).to.include('/log-error/resource');
754
+ Expect(tmpErrorMessage).to.include('Connection refused');
755
+ return fDone();
756
+ }
757
+ return fDone(pError);
758
+ });
759
+ });
760
+ }
761
+ );
762
+ }
763
+ );
764
+
765
+ suite
766
+ (
767
+ 'Multiple Proxy Instances',
768
+ () =>
769
+ {
770
+ test
771
+ (
772
+ 'multiple proxy instances should coexist with different configurations',
773
+ (fDone) =>
774
+ {
775
+ let tmpFable = new libFable(defaultFableSettings);
776
+
777
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
778
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
779
+
780
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
781
+ let tmpProxyA = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
782
+ {
783
+ DestinationURL: 'http://backend-a.local:3000/'
784
+ });
785
+ let tmpProxyB = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
786
+ {
787
+ DestinationURL: 'http://backend-b.local:4000/'
788
+ });
789
+
790
+ Expect(tmpProxyA.proxyDestinationURL).to.equal('http://backend-a.local:3000/');
791
+ Expect(tmpProxyB.proxyDestinationURL).to.equal('http://backend-b.local:4000/');
792
+
793
+ // Each instance should have its own proxy server
794
+ Expect(tmpProxyA.httpProxyServer).to.be.an('object');
795
+ Expect(tmpProxyB.httpProxyServer).to.be.an('object');
796
+ Expect(tmpProxyA.httpProxyServer).to.not.equal(tmpProxyB.httpProxyServer);
797
+
798
+ return fDone();
799
+ }
800
+ );
801
+
802
+ test
803
+ (
804
+ 'multiple proxy instances can register routes to different backends',
805
+ (fDone) =>
806
+ {
807
+ let tmpFable = new libFable(defaultFableSettings);
808
+
809
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
810
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
811
+
812
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
813
+ let tmpAPIProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
814
+ {
815
+ DestinationURL: 'http://api-server.local:3000/'
816
+ });
817
+ let tmpAuthProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
818
+ {
819
+ DestinationURL: 'http://auth-server.local:4000/'
820
+ });
821
+
822
+ tmpOrator.startService(
823
+ () =>
824
+ {
825
+ tmpAPIProxy.connectProxyRoutes(['/1.0/*']);
826
+ tmpAuthProxy.connectProxyRoutes(['/auth/*', '/oauth/*']);
827
+
828
+ // Verify all routes are registered
829
+ let tmpAPIHandler = tmpFable.Orator.serviceServer.router.find('GET', '/1.0/Users');
830
+ Expect(tmpAPIHandler).to.be.an('object');
831
+ Expect(tmpAPIHandler).to.have.a.property('handler');
832
+
833
+ let tmpAuthHandler = tmpFable.Orator.serviceServer.router.find('POST', '/auth/login');
834
+ Expect(tmpAuthHandler).to.be.an('object');
835
+ Expect(tmpAuthHandler).to.have.a.property('handler');
836
+
837
+ let tmpOAuthHandler = tmpFable.Orator.serviceServer.router.find('GET', '/oauth/callback');
838
+ Expect(tmpOAuthHandler).to.be.an('object');
839
+ Expect(tmpOAuthHandler).to.have.a.property('handler');
840
+
841
+ return fDone();
842
+ });
843
+ }
844
+ );
845
+ }
846
+ );
847
+
848
+ suite
849
+ (
850
+ 'Configuration Edge Cases',
851
+ () =>
852
+ {
853
+ test
854
+ (
855
+ 'LogLevel zero should be the default when no log level is configured',
856
+ (fDone) =>
857
+ {
858
+ let tmpFable = new libFable(defaultFableSettings);
859
+
860
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
861
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
862
+
863
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
864
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', {});
865
+
866
+ Expect(tmpProxy.LogLevel).to.equal(0);
867
+
868
+ return fDone();
869
+ }
870
+ );
871
+
872
+ test
873
+ (
874
+ 'LogLevel should be settable to zero explicitly via options without falling to fable settings',
875
+ (fDone) =>
876
+ {
877
+ let tmpFableSettings = Object.assign({}, defaultFableSettings,
878
+ {
879
+ OratorHTTPProxyLogLevel: 5
880
+ });
881
+ let tmpFable = new libFable(tmpFableSettings);
882
+
883
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
884
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
885
+
886
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
887
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
888
+ {
889
+ LogLevel: 0
890
+ });
891
+
892
+ // The `in` operator returns true for LogLevel:0 since the key exists
893
+ Expect(tmpProxy.LogLevel).to.equal(0);
894
+
895
+ return fDone();
896
+ }
897
+ );
898
+
899
+ test
900
+ (
901
+ 'the proxy destination URL should default to loopback when nothing is configured',
902
+ (fDone) =>
903
+ {
904
+ let tmpFable = new libFable(defaultFableSettings);
905
+
906
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
907
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
908
+
909
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
910
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', {});
911
+
912
+ Expect(tmpProxy.proxyDestinationURL).to.equal('http://127.0.0.1/');
913
+
914
+ return fDone();
915
+ }
916
+ );
917
+
918
+ test
919
+ (
920
+ 'the httpProxyServer should be an independent instance per proxy',
921
+ (fDone) =>
922
+ {
923
+ let tmpFable = new libFable(defaultFableSettings);
924
+
925
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
926
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
927
+
928
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
929
+ let tmpProxyOne = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
930
+ {
931
+ DestinationURL: 'http://one.local/'
932
+ });
933
+ let tmpProxyTwo = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
934
+ {
935
+ DestinationURL: 'http://two.local/'
936
+ });
937
+
938
+ // Each proxy should have its own http-proxy server instance
939
+ Expect(tmpProxyOne.httpProxyServer).to.not.equal(tmpProxyTwo.httpProxyServer);
940
+ // And distinct destination URLs
941
+ Expect(tmpProxyOne.proxyDestinationURL).to.not.equal(tmpProxyTwo.proxyDestinationURL);
942
+
943
+ return fDone();
944
+ }
945
+ );
946
+
947
+ test
948
+ (
949
+ 'the RequestPrefixList options key has a known quirk where it reads RequestPrefix instead',
950
+ (fDone) =>
951
+ {
952
+ // This test documents the behavior of the constructor's RequestPrefixList handling.
953
+ // The constructor checks `RequestPrefixList` in options (using the `in` operator),
954
+ // but then reads `this.options.RequestPrefix` (note: singular, not plural).
955
+ // This means when RequestPrefixList is set in options, the value read is
956
+ // this.options.RequestPrefix which is undefined.
957
+ let tmpFable = new libFable(defaultFableSettings);
958
+
959
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
960
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
961
+
962
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
963
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy',
964
+ {
965
+ RequestPrefixList: ['/should-be-ignored/*']
966
+ });
967
+
968
+ // Because the constructor reads this.options.RequestPrefix (which is undefined),
969
+ // the requestPrefixList ends up as undefined rather than the expected array.
970
+ // This documents the current behavior — the mismatched key name.
971
+ Expect(tmpProxy.requestPrefixList).to.equal(undefined);
972
+
973
+ return fDone();
974
+ }
975
+ );
976
+
977
+ test
978
+ (
979
+ 'fable settings should provide the request prefix list when options do not',
980
+ (fDone) =>
981
+ {
982
+ let tmpFableSettings = Object.assign({}, defaultFableSettings,
983
+ {
984
+ OratorHTTPProxyRequestPrefixList: ['/custom/v1/*', '/custom/v2/*']
985
+ });
986
+ let tmpFable = new libFable(tmpFableSettings);
987
+
988
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
989
+ tmpFable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
990
+
991
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
992
+ let tmpProxy = tmpFable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', {});
993
+
994
+ Expect(tmpProxy.requestPrefixList).to.be.an('array');
995
+ Expect(tmpProxy.requestPrefixList).to.deep.equal(['/custom/v1/*', '/custom/v2/*']);
996
+
997
+ return fDone();
998
+ }
999
+ );
1000
+ }
1001
+ );
1002
+ }
1003
+ );