@e22m4u/js-http-static-router 0.1.2 → 0.2.1

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 (43) hide show
  1. package/.mocharc.json +2 -1
  2. package/README.md +265 -17
  3. package/build-cjs.js +1 -1
  4. package/dist/cjs/index.cjs +114 -76
  5. package/example/server.js +23 -13
  6. package/mocha.setup.js +4 -0
  7. package/package.json +10 -8
  8. package/src/http-static-router.d.ts +7 -2
  9. package/src/http-static-router.js +73 -61
  10. package/src/http-static-router.spec.js +899 -0
  11. package/src/static-route.js +18 -8
  12. package/src/types.d.ts +7 -0
  13. package/src/utils/create-cookie-string.d.ts +6 -0
  14. package/src/utils/create-cookie-string.js +28 -0
  15. package/src/utils/create-cookie-string.spec.js +32 -0
  16. package/src/utils/create-error.d.ts +14 -0
  17. package/src/utils/create-error.js +29 -0
  18. package/src/utils/create-error.spec.js +42 -0
  19. package/src/utils/create-request-mock.d.ts +35 -0
  20. package/src/utils/create-request-mock.js +483 -0
  21. package/src/utils/create-request-mock.spec.js +646 -0
  22. package/src/utils/create-response-mock.d.ts +18 -0
  23. package/src/utils/create-response-mock.js +131 -0
  24. package/src/utils/create-response-mock.spec.js +150 -0
  25. package/src/utils/fetch-request-body.d.ts +26 -0
  26. package/src/utils/fetch-request-body.js +148 -0
  27. package/src/utils/fetch-request-body.spec.js +209 -0
  28. package/src/utils/get-pathname-from-url.d.ts +1 -1
  29. package/src/utils/{get-request-pathname.spec.js → get-pathname-from-url.spec.js} +1 -1
  30. package/src/utils/index.d.ts +8 -0
  31. package/src/utils/index.js +8 -0
  32. package/src/utils/is-readable-stream.d.ts +9 -0
  33. package/src/utils/is-readable-stream.js +12 -0
  34. package/src/utils/is-readable-stream.spec.js +23 -0
  35. package/src/utils/parse-content-type.d.ts +15 -0
  36. package/src/utils/parse-content-type.js +34 -0
  37. package/src/utils/parse-content-type.spec.js +79 -0
  38. package/src/utils/parse-cookie-string.d.ts +19 -0
  39. package/src/utils/parse-cookie-string.js +36 -0
  40. package/src/utils/parse-cookie-string.spec.js +45 -0
  41. package/{example/static → static}/index.html +2 -2
  42. /package/{example/static/nested/file.txt → static/assets/nested/heart.txt} +0 -0
  43. /package/{example/static/file.txt → static/assets/rabbit.txt} +0 -0
@@ -0,0 +1,899 @@
1
+ import path from 'path';
2
+ import {expect} from 'chai';
3
+ import {format} from '@e22m4u/js-format';
4
+ import {ServiceContainer} from '@e22m4u/js-service';
5
+ import {HttpStaticRouter} from './http-static-router.js';
6
+ import {createRequestMock, createResponseMock} from './utils/index.js';
7
+
8
+ const REL_ASSETS_DIR = '../static/assets';
9
+ const REL_RABBIT_FILE = path.join(REL_ASSETS_DIR, '/rabbit.txt');
10
+ const REL_HEART_FILE = path.join(REL_ASSETS_DIR, '/nested/heart.txt');
11
+ const ABS_RABBIT_FILE = path.join(import.meta.dirname, REL_RABBIT_FILE);
12
+ const ABS_HEART_FILE = path.join(import.meta.dirname, REL_HEART_FILE);
13
+ const ABS_ASSETS_DIR = path.join(import.meta.dirname, REL_ASSETS_DIR);
14
+ const RABBIT_FILE_SIZE = 824;
15
+ const HEART_FILE_SIZE = 660;
16
+
17
+ describe('HttpStaticRouter', function () {
18
+ describe('constructor', function () {
19
+ it('should require the first parameter to be a correct value', function () {
20
+ const throwable = v => () => new HttpStaticRouter(v);
21
+ const error = s =>
22
+ format(
23
+ 'First parameter must be an Object or an instance ' +
24
+ 'of ServiceContainer, but %s was given.',
25
+ s,
26
+ );
27
+ expect(throwable('str')).to.throw(error('"str"'));
28
+ expect(throwable('')).to.throw(error('""'));
29
+ expect(throwable(10)).to.throw(error('10'));
30
+ expect(throwable(0)).to.throw(error('0'));
31
+ expect(throwable(true)).to.throw(error('true'));
32
+ expect(throwable(false)).to.throw(error('false'));
33
+ expect(throwable([])).to.throw(error('Array'));
34
+ expect(throwable(null)).to.throw(error('null'));
35
+ throwable(new ServiceContainer())();
36
+ throwable({})();
37
+ throwable(undefined)();
38
+ });
39
+
40
+ it('should require the second parameter to be a correct value', function () {
41
+ const throwable = v => () => new HttpStaticRouter(undefined, v);
42
+ const error = s =>
43
+ format('Parameter "options" must be an Object, but %s was given.', s);
44
+ expect(throwable('str')).to.throw(error('"str"'));
45
+ expect(throwable('')).to.throw(error('""'));
46
+ expect(throwable(10)).to.throw(error('10'));
47
+ expect(throwable(0)).to.throw(error('0'));
48
+ expect(throwable(true)).to.throw(error('true'));
49
+ expect(throwable(false)).to.throw(error('false'));
50
+ expect(throwable([])).to.throw(error('Array'));
51
+ expect(throwable(null)).to.throw(error('null'));
52
+ throwable({})();
53
+ throwable(undefined)();
54
+ });
55
+
56
+ it('should require the option "baseDir" to be a String', function () {
57
+ const throwable = v => () => new HttpStaticRouter({baseDir: v});
58
+ const error = s =>
59
+ format('Option "baseDir" must be a String, but %s was given.', s);
60
+ expect(throwable(10)).to.throw(error('10'));
61
+ expect(throwable(0)).to.throw(error('0'));
62
+ expect(throwable(true)).to.throw(error('true'));
63
+ expect(throwable(false)).to.throw(error('false'));
64
+ expect(throwable([])).to.throw(error('Array'));
65
+ expect(throwable(null)).to.throw(error('null'));
66
+ throwable('/path')();
67
+ throwable(undefined)();
68
+ });
69
+
70
+ it('should require the option "baseDir" to be an absolute path', function () {
71
+ const throwable = () => new HttpStaticRouter({baseDir: 'test'});
72
+ expect(throwable).to.throw(
73
+ 'Option "baseDir" must be an absolute path, but "test" was given.',
74
+ );
75
+ });
76
+
77
+ it('should set the given service container to the current instance', function () {
78
+ const container = new ServiceContainer();
79
+ const S = new HttpStaticRouter(container);
80
+ expect(S.container).to.be.eq(container);
81
+ });
82
+
83
+ it('should set the service container and the options object to the current instance', function () {
84
+ const container = new ServiceContainer();
85
+ const options = {baseDir: '/dir'};
86
+ const S = new HttpStaticRouter(container, options);
87
+ expect(S.container).to.be.eq(container);
88
+ expect(S['_options']).to.be.eql(options);
89
+ });
90
+
91
+ it('should set an options object from the first parameter to the current instance', function () {
92
+ const options = {baseDir: '/dir'};
93
+ const S = new HttpStaticRouter(options);
94
+ expect(S['_options']).to.be.eql(options);
95
+ });
96
+
97
+ it('should set an options object from the second parameter to the current instance', function () {
98
+ const options = {baseDir: '/dir'};
99
+ const S = new HttpStaticRouter(undefined, options);
100
+ expect(S['_options']).to.be.eql(options);
101
+ });
102
+
103
+ it('should freeze an internal options object', function () {
104
+ const options = {baseDir: '/dir'};
105
+ const S = new HttpStaticRouter(undefined, options);
106
+ expect(Object.isFrozen(S['_options'])).to.be.true;
107
+ });
108
+ });
109
+
110
+ describe('defineRoute', function () {
111
+ it('should require the parameter "routeDef" to be an Object', function () {
112
+ const throwable = v => () => {
113
+ const S = new HttpStaticRouter();
114
+ S.defineRoute(v);
115
+ };
116
+ const error = s =>
117
+ format('Parameter "routeDef" must be an Object, but %s was given.', s);
118
+ expect(throwable('str')).to.throw(error('"str"'));
119
+ expect(throwable('')).to.throw(error('""'));
120
+ expect(throwable(10)).to.throw(error('10'));
121
+ expect(throwable(0)).to.throw(error('0'));
122
+ expect(throwable(true)).to.throw(error('true'));
123
+ expect(throwable(false)).to.throw(error('false'));
124
+ expect(throwable([])).to.throw(error('Array'));
125
+ expect(throwable(undefined)).to.throw(error('undefined'));
126
+ expect(throwable(null)).to.throw(error('null'));
127
+ throwable({
128
+ remotePath: '/path',
129
+ resourcePath: ABS_RABBIT_FILE,
130
+ })();
131
+ });
132
+
133
+ it('should require the option "remotePath" to be a String', function () {
134
+ const throwable = v => () => {
135
+ const S = new HttpStaticRouter();
136
+ S.defineRoute({
137
+ remotePath: v,
138
+ resourcePath: ABS_RABBIT_FILE,
139
+ });
140
+ };
141
+ const error = s =>
142
+ format('Option "remotePath" must be a String, but %s was given.', s);
143
+ expect(throwable(10)).to.throw(error('10'));
144
+ expect(throwable(0)).to.throw(error('0'));
145
+ expect(throwable(true)).to.throw(error('true'));
146
+ expect(throwable(false)).to.throw(error('false'));
147
+ expect(throwable([])).to.throw(error('Array'));
148
+ expect(throwable({})).to.throw(error('Object'));
149
+ expect(throwable(undefined)).to.throw(error('undefined'));
150
+ expect(throwable(null)).to.throw(error('null'));
151
+ throwable('/path')();
152
+ });
153
+
154
+ it('should require the option "remotePath" to start with "/"', function () {
155
+ const throwable = v => () => {
156
+ const S = new HttpStaticRouter();
157
+ S.defineRoute({
158
+ remotePath: v,
159
+ resourcePath: ABS_RABBIT_FILE,
160
+ });
161
+ };
162
+ const error = s =>
163
+ format('Option "remotePath" must start with "/", but %s was given.', s);
164
+ expect(throwable('path')).to.throw(error('"path"'));
165
+ expect(throwable('')).to.throw(error('""'));
166
+ throwable('/path')();
167
+ throwable('/')();
168
+ });
169
+
170
+ it('should require the option "resourcePath" to be a String', function () {
171
+ const throwable = v => () => {
172
+ const S = new HttpStaticRouter();
173
+ S.defineRoute({
174
+ remotePath: '/path',
175
+ resourcePath: v,
176
+ });
177
+ };
178
+ const error = s =>
179
+ format('Option "resourcePath" must be a String, but %s was given.', s);
180
+ expect(throwable(10)).to.throw(error('10'));
181
+ expect(throwable(0)).to.throw(error('0'));
182
+ expect(throwable(true)).to.throw(error('true'));
183
+ expect(throwable(false)).to.throw(error('false'));
184
+ expect(throwable([])).to.throw(error('Array'));
185
+ expect(throwable({})).to.throw(error('Object'));
186
+ expect(throwable(undefined)).to.throw(error('undefined'));
187
+ expect(throwable(null)).to.throw(error('null'));
188
+ throwable(ABS_RABBIT_FILE)();
189
+ });
190
+
191
+ it('should require the option "resourcePath" to be an absolute path when the route option "baseDir" is not specified', function () {
192
+ const throwable = () => {
193
+ const S = new HttpStaticRouter();
194
+ S.defineRoute({
195
+ remotePath: '/path',
196
+ resourcePath: 'test',
197
+ });
198
+ };
199
+ expect(throwable).to.throw(
200
+ 'Option "resourcePath" must be an absolute path when the router ' +
201
+ 'option "basePath" is not specified, but "test" was given.',
202
+ );
203
+ });
204
+
205
+ it('should resolve a relative path of the resource when the option "baseDir" is provided', function () {
206
+ const S = new HttpStaticRouter({baseDir: import.meta.dirname});
207
+ const route = S.defineRoute({
208
+ remotePath: '/path',
209
+ resourcePath: REL_RABBIT_FILE,
210
+ });
211
+ expect(route.resourcePath).to.be.eq(ABS_RABBIT_FILE);
212
+ });
213
+
214
+ it('should register a new route with the given route definition', function () {
215
+ const S = new HttpStaticRouter();
216
+ const route1 = S.defineRoute({
217
+ remotePath: '/first',
218
+ resourcePath: ABS_RABBIT_FILE,
219
+ });
220
+ const route2 = S.defineRoute({
221
+ remotePath: '/second',
222
+ resourcePath: ABS_RABBIT_FILE,
223
+ });
224
+ expect(S['_routes']).to.include(route1);
225
+ expect(S['_routes']).to.include(route2);
226
+ });
227
+
228
+ it('should sort registered routes by length of the remote path', function () {
229
+ const S = new HttpStaticRouter();
230
+ const route1 = S.defineRoute({
231
+ remotePath: '/foo',
232
+ resourcePath: ABS_RABBIT_FILE,
233
+ });
234
+ const route2 = S.defineRoute({
235
+ remotePath: '/fooBarBaz',
236
+ resourcePath: ABS_RABBIT_FILE,
237
+ });
238
+ const route3 = S.defineRoute({
239
+ remotePath: '/fooBar',
240
+ resourcePath: ABS_RABBIT_FILE,
241
+ });
242
+ expect(S['_routes']).to.be.eql([route2, route3, route1]);
243
+ });
244
+ });
245
+
246
+ describe('_findFileForRequest', function () {
247
+ it('should return undefined if no route matched', async function () {
248
+ const S = new HttpStaticRouter();
249
+ const req = createRequestMock();
250
+ const promise = S._findFileForRequest(req);
251
+ expect(promise).to.be.instanceOf(Promise);
252
+ const res = await promise;
253
+ expect(res).to.be.undefined;
254
+ });
255
+
256
+ describe('when a route points to a file', function () {
257
+ it('should return a file info even when the request url has an empty string', async function () {
258
+ const S = new HttpStaticRouter();
259
+ S.defineRoute({
260
+ remotePath: '/',
261
+ resourcePath: ABS_RABBIT_FILE,
262
+ });
263
+ const req = createRequestMock({url: ''});
264
+ const promise = S._findFileForRequest(req);
265
+ expect(promise).to.be.instanceOf(Promise);
266
+ const res = await promise;
267
+ expect(res).to.be.eql({
268
+ path: ABS_RABBIT_FILE,
269
+ size: RABBIT_FILE_SIZE,
270
+ });
271
+ });
272
+
273
+ it('should return a file info even when the request url has a protocol and host', async function () {
274
+ const S = new HttpStaticRouter();
275
+ S.defineRoute({
276
+ remotePath: '/',
277
+ resourcePath: ABS_RABBIT_FILE,
278
+ });
279
+ const req = createRequestMock({url: 'http://localhost:3000'});
280
+ const promise = S._findFileForRequest(req);
281
+ expect(promise).to.be.instanceOf(Promise);
282
+ const res = await promise;
283
+ expect(res).to.be.eql({
284
+ path: ABS_RABBIT_FILE,
285
+ size: RABBIT_FILE_SIZE,
286
+ });
287
+ });
288
+
289
+ it('should return a file info even when the request url has a protocol and host with a trailing slash', async function () {
290
+ const S = new HttpStaticRouter();
291
+ S.defineRoute({
292
+ remotePath: '/',
293
+ resourcePath: ABS_RABBIT_FILE,
294
+ });
295
+ const req = createRequestMock({url: 'http://localhost:3000/'});
296
+ const promise = S._findFileForRequest(req);
297
+ expect(promise).to.be.instanceOf(Promise);
298
+ const res = await promise;
299
+ expect(res).to.be.eql({
300
+ path: ABS_RABBIT_FILE,
301
+ size: RABBIT_FILE_SIZE,
302
+ });
303
+ });
304
+
305
+ it('should return undefined when the request url has a host with trailing slash duplicates', async function () {
306
+ const S = new HttpStaticRouter();
307
+ S.defineRoute({
308
+ remotePath: '/',
309
+ resourcePath: ABS_RABBIT_FILE,
310
+ });
311
+ const req = createRequestMock({url: 'http://localhost:3000//'});
312
+ const promise = S._findFileForRequest(req);
313
+ expect(promise).to.be.instanceOf(Promise);
314
+ const res = await promise;
315
+ expect(res).to.be.undefined;
316
+ });
317
+
318
+ it('should return a file info for a matched route with the root path', async function () {
319
+ const S = new HttpStaticRouter();
320
+ S.defineRoute({
321
+ remotePath: '/',
322
+ resourcePath: ABS_RABBIT_FILE,
323
+ });
324
+ const req = createRequestMock({path: '/'});
325
+ const promise = S._findFileForRequest(req);
326
+ expect(promise).to.be.instanceOf(Promise);
327
+ const res = await promise;
328
+ expect(res).to.be.eql({
329
+ path: ABS_RABBIT_FILE,
330
+ size: RABBIT_FILE_SIZE,
331
+ });
332
+ });
333
+
334
+ it('should return a file info when a route matches with the GET method', async function () {
335
+ const S = new HttpStaticRouter();
336
+ S.defineRoute({
337
+ remotePath: '/',
338
+ resourcePath: ABS_RABBIT_FILE,
339
+ });
340
+ const req = createRequestMock({method: 'GET'});
341
+ const promise = S._findFileForRequest(req);
342
+ expect(promise).to.be.instanceOf(Promise);
343
+ const res = await promise;
344
+ expect(res).to.be.eql({
345
+ path: ABS_RABBIT_FILE,
346
+ size: RABBIT_FILE_SIZE,
347
+ });
348
+ });
349
+
350
+ it('should return a file info when a route matches with the HEAD method', async function () {
351
+ const S = new HttpStaticRouter();
352
+ S.defineRoute({
353
+ remotePath: '/',
354
+ resourcePath: ABS_RABBIT_FILE,
355
+ });
356
+ const req = createRequestMock({method: 'HEAD'});
357
+ const promise = S._findFileForRequest(req);
358
+ expect(promise).to.be.instanceOf(Promise);
359
+ const res = await promise;
360
+ expect(res).to.be.eql({
361
+ path: ABS_RABBIT_FILE,
362
+ size: RABBIT_FILE_SIZE,
363
+ });
364
+ });
365
+
366
+ it('should return undefined for the POST method even when the route matches', async function () {
367
+ const S = new HttpStaticRouter();
368
+ S.defineRoute({
369
+ remotePath: '/',
370
+ resourcePath: ABS_RABBIT_FILE,
371
+ });
372
+ const req = createRequestMock({method: 'POST'});
373
+ const promise = S._findFileForRequest(req);
374
+ expect(promise).to.be.instanceOf(Promise);
375
+ const res = await promise;
376
+ expect(res).to.be.undefined;
377
+ });
378
+
379
+ it('should return undefined for the PUT method even when the route matches', async function () {
380
+ const S = new HttpStaticRouter();
381
+ S.defineRoute({
382
+ remotePath: '/',
383
+ resourcePath: ABS_RABBIT_FILE,
384
+ });
385
+ const req = createRequestMock({method: 'PUT'});
386
+ const promise = S._findFileForRequest(req);
387
+ expect(promise).to.be.instanceOf(Promise);
388
+ const res = await promise;
389
+ expect(res).to.be.undefined;
390
+ });
391
+
392
+ it('should return undefined for the PATCH method even when the route matches', async function () {
393
+ const S = new HttpStaticRouter();
394
+ S.defineRoute({
395
+ remotePath: '/',
396
+ resourcePath: ABS_RABBIT_FILE,
397
+ });
398
+ const req = createRequestMock({method: 'PATCH'});
399
+ const promise = S._findFileForRequest(req);
400
+ expect(promise).to.be.instanceOf(Promise);
401
+ const res = await promise;
402
+ expect(res).to.be.undefined;
403
+ });
404
+
405
+ it('should return undefined for the DELETE method even when the route matches', async function () {
406
+ const S = new HttpStaticRouter();
407
+ S.defineRoute({
408
+ remotePath: '/',
409
+ resourcePath: ABS_RABBIT_FILE,
410
+ });
411
+ const req = createRequestMock({method: 'DELETE'});
412
+ const promise = S._findFileForRequest(req);
413
+ expect(promise).to.be.instanceOf(Promise);
414
+ const res = await promise;
415
+ expect(res).to.be.undefined;
416
+ });
417
+
418
+ describe('when a remote path has a single segment', function () {
419
+ it('should return a file info for a matched route pointing to an existing file', async function () {
420
+ const S = new HttpStaticRouter();
421
+ S.defineRoute({
422
+ remotePath: '/segment',
423
+ resourcePath: ABS_RABBIT_FILE,
424
+ });
425
+ const req = createRequestMock({path: '/segment'});
426
+ const promise = S._findFileForRequest(req);
427
+ expect(promise).to.be.instanceOf(Promise);
428
+ const res = await promise;
429
+ expect(res).to.be.eql({
430
+ path: ABS_RABBIT_FILE,
431
+ size: RABBIT_FILE_SIZE,
432
+ });
433
+ });
434
+
435
+ it('should return a file info when a matched route has a trailing slash', async function () {
436
+ const S = new HttpStaticRouter();
437
+ S.defineRoute({
438
+ remotePath: '/segment/',
439
+ resourcePath: ABS_RABBIT_FILE,
440
+ });
441
+ const req = createRequestMock({path: '/segment/'});
442
+ const promise = S._findFileForRequest(req);
443
+ expect(promise).to.be.instanceOf(Promise);
444
+ const res = await promise;
445
+ expect(res).to.be.eql({
446
+ path: ABS_RABBIT_FILE,
447
+ size: RABBIT_FILE_SIZE,
448
+ });
449
+ });
450
+
451
+ it('should return undefined when the remote path does not match a trailing slash in the request url', async function () {
452
+ const S = new HttpStaticRouter();
453
+ S.defineRoute({
454
+ remotePath: '/segment',
455
+ resourcePath: ABS_RABBIT_FILE,
456
+ });
457
+ const req = createRequestMock({path: '/segment/'});
458
+ const promise = S._findFileForRequest(req);
459
+ expect(promise).to.be.instanceOf(Promise);
460
+ const res = await promise;
461
+ expect(res).to.be.undefined;
462
+ });
463
+
464
+ it('should return undefined when the request url does not match a trailing slash in the remote path', async function () {
465
+ const S = new HttpStaticRouter();
466
+ S.defineRoute({
467
+ remotePath: '/segment/',
468
+ resourcePath: ABS_RABBIT_FILE,
469
+ });
470
+ const req = createRequestMock({path: '/segment'});
471
+ const promise = S._findFileForRequest(req);
472
+ expect(promise).to.be.instanceOf(Promise);
473
+ const res = await promise;
474
+ expect(res).to.be.undefined;
475
+ });
476
+ });
477
+
478
+ describe('when a remote path has two segments', function () {
479
+ it('should return a file info for the matched route pointing to an existing file', async function () {
480
+ const S = new HttpStaticRouter();
481
+ S.defineRoute({
482
+ remotePath: '/segment1/segment2',
483
+ resourcePath: ABS_RABBIT_FILE,
484
+ });
485
+ const req = createRequestMock({path: '/segment1/segment2'});
486
+ const promise = S._findFileForRequest(req);
487
+ expect(promise).to.be.instanceOf(Promise);
488
+ const res = await promise;
489
+ expect(res).to.be.eql({
490
+ path: ABS_RABBIT_FILE,
491
+ size: RABBIT_FILE_SIZE,
492
+ });
493
+ });
494
+
495
+ it('should return a file info when a matched route has a trailing slash', async function () {
496
+ const S = new HttpStaticRouter();
497
+ S.defineRoute({
498
+ remotePath: '/segment1/segment2/',
499
+ resourcePath: ABS_RABBIT_FILE,
500
+ });
501
+ const req = createRequestMock({path: '/segment1/segment2/'});
502
+ const promise = S._findFileForRequest(req);
503
+ expect(promise).to.be.instanceOf(Promise);
504
+ const res = await promise;
505
+ expect(res).to.be.eql({
506
+ path: ABS_RABBIT_FILE,
507
+ size: RABBIT_FILE_SIZE,
508
+ });
509
+ });
510
+
511
+ it('should return undefined when the remote path does not match a trailing slash in the request url', async function () {
512
+ const S = new HttpStaticRouter();
513
+ S.defineRoute({
514
+ remotePath: '/segment1/segment2',
515
+ resourcePath: ABS_RABBIT_FILE,
516
+ });
517
+ const req = createRequestMock({path: '/segment1/segment2/'});
518
+ const promise = S._findFileForRequest(req);
519
+ expect(promise).to.be.instanceOf(Promise);
520
+ const res = await promise;
521
+ expect(res).to.be.undefined;
522
+ });
523
+
524
+ it('should return undefined when the request url does not match a trailing slash in the remote path', async function () {
525
+ const S = new HttpStaticRouter();
526
+ S.defineRoute({
527
+ remotePath: '/segment1/segment2/',
528
+ resourcePath: ABS_RABBIT_FILE,
529
+ });
530
+ const req = createRequestMock({path: '/segment1/segment2'});
531
+ const promise = S._findFileForRequest(req);
532
+ expect(promise).to.be.instanceOf(Promise);
533
+ const res = await promise;
534
+ expect(res).to.be.undefined;
535
+ });
536
+
537
+ it('should return undefined when a request path has duplicate slashes between its segments', async function () {
538
+ const S = new HttpStaticRouter();
539
+ S.defineRoute({
540
+ remotePath: '/segment1/segment2',
541
+ resourcePath: ABS_RABBIT_FILE,
542
+ });
543
+ const req = createRequestMock({path: '/segment1//segment2'});
544
+ const promise = S._findFileForRequest(req);
545
+ expect(promise).to.be.instanceOf(Promise);
546
+ const res = await promise;
547
+ expect(res).to.be.undefined;
548
+ });
549
+ });
550
+ });
551
+
552
+ describe('when a route points to a directory', function () {
553
+ it('should return a file info when an extra path points to an existing file', async function () {
554
+ const S = new HttpStaticRouter();
555
+ S.defineRoute({
556
+ remotePath: '/',
557
+ resourcePath: ABS_ASSETS_DIR,
558
+ });
559
+ const req = createRequestMock({path: '/rabbit.txt'});
560
+ const promise = S._findFileForRequest(req);
561
+ expect(promise).to.be.instanceOf(Promise);
562
+ const res = await promise;
563
+ expect(res).to.be.eql({
564
+ path: ABS_RABBIT_FILE,
565
+ size: RABBIT_FILE_SIZE,
566
+ });
567
+ });
568
+
569
+ it('should return undefined when an extra path points to a non-existent file', async function () {
570
+ const S = new HttpStaticRouter();
571
+ S.defineRoute({
572
+ remotePath: '/',
573
+ resourcePath: ABS_ASSETS_DIR,
574
+ });
575
+ const req = createRequestMock({path: '/unknown.txt'});
576
+ const promise = S._findFileForRequest(req);
577
+ expect(promise).to.be.instanceOf(Promise);
578
+ const res = await promise;
579
+ expect(res).to.be.undefined;
580
+ });
581
+
582
+ it('should return a file info when an extra path points to an existing file in a nested directory', async function () {
583
+ const S = new HttpStaticRouter();
584
+ S.defineRoute({
585
+ remotePath: '/',
586
+ resourcePath: ABS_ASSETS_DIR,
587
+ });
588
+ const req = createRequestMock({path: '/nested/heart.txt'});
589
+ const promise = S._findFileForRequest(req);
590
+ expect(promise).to.be.instanceOf(Promise);
591
+ const res = await promise;
592
+ expect(res).to.be.eql({
593
+ path: ABS_HEART_FILE,
594
+ size: HEART_FILE_SIZE,
595
+ });
596
+ });
597
+
598
+ it('should return undefined when an extra path pointing to an existing file has a trailing slash', async function () {
599
+ const S = new HttpStaticRouter();
600
+ S.defineRoute({
601
+ remotePath: '/',
602
+ resourcePath: ABS_ASSETS_DIR,
603
+ });
604
+ const req = createRequestMock({path: '/rabbit.txt/'});
605
+ const promise = S._findFileForRequest(req);
606
+ expect(promise).to.be.instanceOf(Promise);
607
+ const res = await promise;
608
+ expect(res).to.be.undefined;
609
+ });
610
+
611
+ it('should return undefined when an extra path pointing to an existing file in a nested directory has a trailing slash', async function () {
612
+ const S = new HttpStaticRouter();
613
+ S.defineRoute({
614
+ remotePath: '/',
615
+ resourcePath: ABS_ASSETS_DIR,
616
+ });
617
+ const req = createRequestMock({path: '/nested/heart.txt/'});
618
+ const promise = S._findFileForRequest(req);
619
+ expect(promise).to.be.instanceOf(Promise);
620
+ const res = await promise;
621
+ expect(res).to.be.undefined;
622
+ });
623
+
624
+ it('should return undefined when an extra path pointing to an existing directory but without a specified file name', async function () {
625
+ const S = new HttpStaticRouter();
626
+ S.defineRoute({
627
+ remotePath: '/',
628
+ resourcePath: ABS_ASSETS_DIR,
629
+ });
630
+ const req = createRequestMock({path: '/nested'});
631
+ const promise = S._findFileForRequest(req);
632
+ expect(promise).to.be.instanceOf(Promise);
633
+ const res = await promise;
634
+ expect(res).to.be.undefined;
635
+ });
636
+
637
+ describe('when a remote path has a single segment', function () {
638
+ it('should return a file info for an extra path pointing to an existing file', async function () {
639
+ const S = new HttpStaticRouter();
640
+ S.defineRoute({
641
+ remotePath: '/segment',
642
+ resourcePath: ABS_ASSETS_DIR,
643
+ });
644
+ const req = createRequestMock({path: '/segment/rabbit.txt'});
645
+ const promise = S._findFileForRequest(req);
646
+ expect(promise).to.be.instanceOf(Promise);
647
+ const res = await promise;
648
+ expect(res).to.be.eql({
649
+ path: ABS_RABBIT_FILE,
650
+ size: RABBIT_FILE_SIZE,
651
+ });
652
+ });
653
+
654
+ it('should return undefined for an extra path pointing to a non-existent file', async function () {
655
+ const S = new HttpStaticRouter();
656
+ S.defineRoute({
657
+ remotePath: '/segment',
658
+ resourcePath: ABS_ASSETS_DIR,
659
+ });
660
+ const req = createRequestMock({path: '/segment/unknown.txt'});
661
+ const promise = S._findFileForRequest(req);
662
+ expect(promise).to.be.instanceOf(Promise);
663
+ const res = await promise;
664
+ expect(res).to.be.undefined;
665
+ });
666
+
667
+ it('should return a file info for an extra path pointing to an existing file in a nested directory', async function () {
668
+ const S = new HttpStaticRouter();
669
+ S.defineRoute({
670
+ remotePath: '/segment',
671
+ resourcePath: ABS_ASSETS_DIR,
672
+ });
673
+ const req = createRequestMock({path: '/segment/nested/heart.txt'});
674
+ const promise = S._findFileForRequest(req);
675
+ expect(promise).to.be.instanceOf(Promise);
676
+ const res = await promise;
677
+ expect(res).to.be.eql({
678
+ path: ABS_HEART_FILE,
679
+ size: HEART_FILE_SIZE,
680
+ });
681
+ });
682
+
683
+ it('should return undefined when an extra path pointing to an existing file has a trailing slash', async function () {
684
+ const S = new HttpStaticRouter();
685
+ S.defineRoute({
686
+ remotePath: '/segment',
687
+ resourcePath: ABS_ASSETS_DIR,
688
+ });
689
+ const req = createRequestMock({path: '/segment/rabbit.txt/'});
690
+ const promise = S._findFileForRequest(req);
691
+ expect(promise).to.be.instanceOf(Promise);
692
+ const res = await promise;
693
+ expect(res).to.be.undefined;
694
+ });
695
+
696
+ it('should return undefined when an extra path pointing to an existing file in a nested directory has a trailing slash', async function () {
697
+ const S = new HttpStaticRouter();
698
+ S.defineRoute({
699
+ remotePath: '/segment',
700
+ resourcePath: ABS_ASSETS_DIR,
701
+ });
702
+ const req = createRequestMock({path: '/segment/nested/heart.txt/'});
703
+ const promise = S._findFileForRequest(req);
704
+ expect(promise).to.be.instanceOf(Promise);
705
+ const res = await promise;
706
+ expect(res).to.be.undefined;
707
+ });
708
+ });
709
+
710
+ describe('when a remote path has two segments', function () {
711
+ it('should return a file info for an extra path pointing to an existing file', async function () {
712
+ const S = new HttpStaticRouter();
713
+ S.defineRoute({
714
+ remotePath: '/segment1/segment2',
715
+ resourcePath: ABS_ASSETS_DIR,
716
+ });
717
+ const req = createRequestMock({
718
+ path: '/segment1/segment2/rabbit.txt',
719
+ });
720
+ const promise = S._findFileForRequest(req);
721
+ expect(promise).to.be.instanceOf(Promise);
722
+ const res = await promise;
723
+ expect(res).to.be.eql({
724
+ path: ABS_RABBIT_FILE,
725
+ size: RABBIT_FILE_SIZE,
726
+ });
727
+ });
728
+
729
+ it('should return undefined for an extra path pointing to a non-existent file', async function () {
730
+ const S = new HttpStaticRouter();
731
+ S.defineRoute({
732
+ remotePath: '/segment1/segment2',
733
+ resourcePath: ABS_ASSETS_DIR,
734
+ });
735
+ const req = createRequestMock({
736
+ path: '/segment1/segment2/unknown.txt',
737
+ });
738
+ const promise = S._findFileForRequest(req);
739
+ expect(promise).to.be.instanceOf(Promise);
740
+ const res = await promise;
741
+ expect(res).to.be.undefined;
742
+ });
743
+
744
+ it('should return a file info for an extra path pointing to an existing file in a nested directory', async function () {
745
+ const S = new HttpStaticRouter();
746
+ S.defineRoute({
747
+ remotePath: '/segment1/segment2',
748
+ resourcePath: ABS_ASSETS_DIR,
749
+ });
750
+ const req = createRequestMock({
751
+ path: '/segment1/segment2/nested/heart.txt',
752
+ });
753
+ const promise = S._findFileForRequest(req);
754
+ expect(promise).to.be.instanceOf(Promise);
755
+ const res = await promise;
756
+ expect(res).to.be.eql({
757
+ path: ABS_HEART_FILE,
758
+ size: HEART_FILE_SIZE,
759
+ });
760
+ });
761
+
762
+ it('should return undefined when an extra path pointing to an existing file has a trailing slash', async function () {
763
+ const S = new HttpStaticRouter();
764
+ S.defineRoute({
765
+ remotePath: '/segment1/segment2',
766
+ resourcePath: ABS_ASSETS_DIR,
767
+ });
768
+ const req = createRequestMock({
769
+ path: '/segment1/segment2/rabbit.txt/',
770
+ });
771
+ const promise = S._findFileForRequest(req);
772
+ expect(promise).to.be.instanceOf(Promise);
773
+ const res = await promise;
774
+ expect(res).to.be.undefined;
775
+ });
776
+
777
+ it('should return undefined when an extra path pointing to an existing file in a nested directory has a trailing slash', async function () {
778
+ const S = new HttpStaticRouter();
779
+ S.defineRoute({
780
+ remotePath: '/segment1/segment2',
781
+ resourcePath: ABS_ASSETS_DIR,
782
+ });
783
+ const req = createRequestMock({
784
+ path: '/segment1/segment2/nested/heart.txt/',
785
+ });
786
+ const promise = S._findFileForRequest(req);
787
+ expect(promise).to.be.instanceOf(Promise);
788
+ const res = await promise;
789
+ expect(res).to.be.undefined;
790
+ });
791
+ });
792
+ });
793
+ });
794
+
795
+ describe('_sendFile', function () {
796
+ it('should send correct headers and status code for an existing file', async function () {
797
+ const fileInfo = {path: ABS_HEART_FILE, size: HEART_FILE_SIZE};
798
+ const expectedContentType = 'text/plain; charset=utf-8';
799
+ const S = new HttpStaticRouter();
800
+ const req = createRequestMock();
801
+ const res = createResponseMock();
802
+ await S._sendFile(req, res, fileInfo);
803
+ expect(res.statusCode).to.be.eq(200);
804
+ expect(res.getHeader('Content-Type')).to.be.eq(expectedContentType);
805
+ expect(res.getHeader('Content-Length')).to.be.eq(String(fileInfo.size));
806
+ });
807
+
808
+ it('should send 500 Internal Server Error for a non-existing file', async function () {
809
+ const fileInfo = {path: path.join(ABS_ASSETS_DIR, 'unknown'), size: 0};
810
+ const expectedContentType = 'text/plain; charset=utf-8';
811
+ const S = new HttpStaticRouter();
812
+ const req = createRequestMock();
813
+ const res = createResponseMock();
814
+ await S._sendFile(req, res, fileInfo);
815
+ const body = await res.getBody();
816
+ expect(res.statusCode).to.be.eq(500);
817
+ expect(res.getHeader('Content-Type')).to.be.eq(expectedContentType);
818
+ expect(body).to.be.eq('500 Internal Server Error');
819
+ });
820
+
821
+ it('should send 500 Internal Server Error for non-ENOENT filesystem errors', async function () {
822
+ const fileInfo = {path: import.meta.dirname, size: 1024};
823
+ const expectedContentType = 'text/plain; charset=utf-8';
824
+ const S = new HttpStaticRouter();
825
+ const req = createRequestMock();
826
+ const res = createResponseMock();
827
+ await S._sendFile(req, res, fileInfo);
828
+ const body = await res.getBody();
829
+ expect(res.statusCode).to.be.eq(500);
830
+ expect(res.getHeader('Content-Type')).to.be.eq(expectedContentType);
831
+ expect(body).to.be.eq('500 Internal Server Error');
832
+ });
833
+
834
+ it('should resolve a promise when the client closes the request prematurely', async function () {
835
+ const fileInfo = {path: ABS_RABBIT_FILE, size: RABBIT_FILE_SIZE};
836
+ const S = new HttpStaticRouter();
837
+ const req = createRequestMock();
838
+ const res = createResponseMock();
839
+ const promise = S._sendFile(req, res, fileInfo);
840
+ req.emit('close');
841
+ await promise;
842
+ expect(res.writableFinished).to.be.false;
843
+ expect(res.headersSent).to.be.false;
844
+ });
845
+ });
846
+
847
+ describe('handleRequest', function () {
848
+ it('should resolve true and send file when route matches', async function () {
849
+ const S = new HttpStaticRouter();
850
+ S.defineRoute({remotePath: '/test', resourcePath: ABS_RABBIT_FILE});
851
+ const req = createRequestMock({path: '/test'});
852
+ const res = createResponseMock();
853
+ const result = await S.handleRequest(req, res);
854
+ expect(result).to.be.true;
855
+ expect(res.statusCode).to.be.eq(200);
856
+ expect(res.headersSent).to.be.true;
857
+ const body = await res.getBody();
858
+ expect(Buffer.from(body).byteLength).to.be.eq(RABBIT_FILE_SIZE);
859
+ });
860
+
861
+ it('should resolve false when no route matches', async function () {
862
+ const S = new HttpStaticRouter();
863
+ const req = createRequestMock({path: '/unknown'});
864
+ const res = createResponseMock();
865
+ const result = await S.handleRequest(req, res);
866
+ expect(result).to.be.false;
867
+ expect(res.headersSent).to.be.false;
868
+ });
869
+
870
+ it('should resolve false when file does not exist', async function () {
871
+ const S = new HttpStaticRouter({baseDir: ABS_ASSETS_DIR});
872
+ S.defineRoute({remotePath: '/', resourcePath: './'});
873
+ const req = createRequestMock({path: '/missing'});
874
+ const res = createResponseMock();
875
+ const result = await S.handleRequest(req, res);
876
+ expect(result).to.be.false;
877
+ expect(res.headersSent).to.be.false;
878
+ });
879
+
880
+ it('should resolve false for unsupported HTTP method', async function () {
881
+ const S = new HttpStaticRouter();
882
+ S.defineRoute({remotePath: '/', resourcePath: ABS_RABBIT_FILE});
883
+ const req = createRequestMock({path: '/', method: 'POST'});
884
+ const res = createResponseMock();
885
+ const result = await S.handleRequest(req, res);
886
+ expect(result).to.be.false;
887
+ });
888
+
889
+ it('should resolve true and handle request for directory correctly', async function () {
890
+ const S = new HttpStaticRouter({baseDir: ABS_ASSETS_DIR});
891
+ S.defineRoute({remotePath: '/', resourcePath: './'});
892
+ const req = createRequestMock({path: '/rabbit.txt'});
893
+ const res = createResponseMock();
894
+ const result = await S.handleRequest(req, res);
895
+ expect(result).to.be.true;
896
+ expect(res.statusCode).to.be.eq(200);
897
+ });
898
+ });
899
+ });