@e22m4u/js-trie-router 0.7.0 → 0.7.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.
@@ -427,13 +427,43 @@ __name(parseCookieString, "parseCookieString");
427
427
  // src/utils/create-request-mock.js
428
428
  var import_net = require("net");
429
429
  var import_tls = require("tls");
430
+ var import_querystring = __toESM(require("querystring"), 1);
430
431
  var import_http2 = require("http");
432
+ var import_js_format9 = require("@e22m4u/js-format");
433
+
434
+ // src/utils/create-cookie-string.js
431
435
  var import_js_format8 = require("@e22m4u/js-format");
436
+ function createCookieString(data) {
437
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
438
+ throw new import_js_format8.InvalidArgumentError(
439
+ "Cookie data must be an Object, but %v was given.",
440
+ data
441
+ );
442
+ }
443
+ let cookies = "";
444
+ for (const key in data) {
445
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
446
+ continue;
447
+ }
448
+ const val = data[key];
449
+ if (val == null) {
450
+ continue;
451
+ }
452
+ cookies += `${key}=${val}; `;
453
+ }
454
+ return cookies.trim().replace(/;$/, "");
455
+ }
456
+ __name(createCookieString, "createCookieString");
457
+
458
+ // src/utils/create-request-mock.js
432
459
  var SUPPORTED_OPTIONS = [
433
460
  "host",
434
461
  "method",
435
462
  "secure",
436
463
  "url",
464
+ "path",
465
+ "query",
466
+ "cookies",
437
467
  "headers",
438
468
  "body",
439
469
  "stream",
@@ -442,54 +472,106 @@ var SUPPORTED_OPTIONS = [
442
472
  function createRequestMock(options) {
443
473
  if (options !== void 0) {
444
474
  if (!options || typeof options !== "object" || Array.isArray(options)) {
445
- throw new import_js_format8.InvalidArgumentError(
475
+ throw new import_js_format9.InvalidArgumentError(
446
476
  'Parameter "options" must be an Object, but %v was given.',
447
477
  options
448
478
  );
449
479
  }
450
480
  Object.keys(options).forEach((optionName) => {
451
481
  if (!SUPPORTED_OPTIONS.includes(optionName)) {
452
- throw new import_js_format8.InvalidArgumentError(
482
+ throw new import_js_format9.InvalidArgumentError(
453
483
  "Option %v is not supported.",
454
484
  optionName
455
485
  );
456
486
  }
457
487
  });
458
488
  if (options.host !== void 0 && typeof options.host !== "string") {
459
- throw new import_js_format8.InvalidArgumentError(
489
+ throw new import_js_format9.InvalidArgumentError(
460
490
  'Option "host" must be a String, but %v was given.',
461
491
  options.host
462
492
  );
463
493
  }
464
494
  if (options.method !== void 0 && typeof options.method !== "string") {
465
- throw new import_js_format8.InvalidArgumentError(
495
+ throw new import_js_format9.InvalidArgumentError(
466
496
  'Option "method" must be a String, but %v was given.',
467
497
  options.method
468
498
  );
469
499
  }
470
500
  if (options.secure !== void 0 && typeof options.secure !== "boolean") {
471
- throw new import_js_format8.InvalidArgumentError(
501
+ throw new import_js_format9.InvalidArgumentError(
472
502
  'Option "secure" must be a Boolean, but %v was given.',
473
503
  options.secure
474
504
  );
475
505
  }
476
506
  if (options.url !== void 0) {
477
507
  if (typeof options.url !== "string") {
478
- throw new import_js_format8.InvalidArgumentError(
508
+ throw new import_js_format9.InvalidArgumentError(
479
509
  'Option "url" must be a String, but %v was given.',
480
510
  options.url
481
511
  );
482
512
  }
483
513
  if (options.url.indexOf("#") !== -1) {
484
- throw new import_js_format8.InvalidArgumentError(
514
+ throw new import_js_format9.InvalidArgumentError(
485
515
  'Option "url" must not contain "#", but %v was given.',
486
516
  options.url
487
517
  );
488
518
  }
489
519
  }
520
+ if (options.path !== void 0) {
521
+ if (typeof options.path !== "string") {
522
+ throw new import_js_format9.InvalidArgumentError(
523
+ 'Option "path" must be a String, but %v was given.',
524
+ options.path
525
+ );
526
+ }
527
+ if (options.path.indexOf("#") !== -1) {
528
+ throw new import_js_format9.InvalidArgumentError(
529
+ 'Option "path" must not contain "#", but %v was given.',
530
+ options.path
531
+ );
532
+ }
533
+ if (options.path.indexOf("?") !== -1) {
534
+ throw new import_js_format9.InvalidArgumentError(
535
+ 'Option "path" must not contain "?", but %v was given.',
536
+ options.path
537
+ );
538
+ }
539
+ if (!options.path.startsWith("/")) {
540
+ throw new import_js_format9.InvalidArgumentError(
541
+ 'Option "path" must start with "/", but %v was given.',
542
+ options.path
543
+ );
544
+ }
545
+ }
546
+ if (options.query !== void 0) {
547
+ if (options.query === null || typeof options.query !== "string" && typeof options.query !== "object" || Array.isArray(options.query)) {
548
+ throw new import_js_format9.InvalidArgumentError(
549
+ 'Option "query" must be a String or an Object, but %v was given.',
550
+ options.query
551
+ );
552
+ }
553
+ }
554
+ if (options.cookies !== void 0) {
555
+ if (!options.cookies || typeof options.cookies !== "object" || Array.isArray(options.cookies)) {
556
+ throw new import_js_format9.InvalidArgumentError(
557
+ 'Option "cookies" must be an Object, but %v was given.',
558
+ options.cookies
559
+ );
560
+ }
561
+ Object.keys(options.cookies).forEach((cookieName) => {
562
+ const cookieValue = options.cookies[cookieName];
563
+ if (cookieValue !== void 0 && typeof cookieValue !== "string") {
564
+ throw new import_js_format9.InvalidArgumentError(
565
+ "Cookie %v must be a String, but %v was given.",
566
+ cookieName,
567
+ cookieValue
568
+ );
569
+ }
570
+ });
571
+ }
490
572
  if (options.headers !== void 0) {
491
573
  if (!options.headers || typeof options.headers !== "object" || Array.isArray(options.headers)) {
492
- throw new import_js_format8.InvalidArgumentError(
574
+ throw new import_js_format9.InvalidArgumentError(
493
575
  'Option "headers" must be an Object, but %v was given.',
494
576
  options.headers
495
577
  );
@@ -498,7 +580,7 @@ function createRequestMock(options) {
498
580
  const headerValue = options.headers[headerName];
499
581
  if (headerValue !== void 0) {
500
582
  if (typeof headerValue !== "string" && !Array.isArray(headerValue)) {
501
- throw new import_js_format8.InvalidArgumentError(
583
+ throw new import_js_format9.InvalidArgumentError(
502
584
  "Header %v must be a String or an Array, but %v was given.",
503
585
  headerName,
504
586
  headerValue
@@ -507,7 +589,7 @@ function createRequestMock(options) {
507
589
  if (Array.isArray(headerValue)) {
508
590
  headerValue.forEach((headerEl, index) => {
509
591
  if (typeof headerEl !== "string") {
510
- throw new import_js_format8.InvalidArgumentError(
592
+ throw new import_js_format9.InvalidArgumentError(
511
593
  "Element %d of the header %v must be a String, but %v was given.",
512
594
  index,
513
595
  headerName,
@@ -520,38 +602,50 @@ function createRequestMock(options) {
520
602
  });
521
603
  }
522
604
  if (options.stream !== void 0 && !isReadableStream(options.stream)) {
523
- throw new import_js_format8.InvalidArgumentError(
605
+ throw new import_js_format9.InvalidArgumentError(
524
606
  'Option "stream" must be a Stream, but %v was given.',
525
607
  options.stream
526
608
  );
527
609
  }
528
610
  if (options.encoding !== void 0) {
529
611
  if (typeof options.encoding !== "string") {
530
- throw new import_js_format8.InvalidArgumentError(
612
+ throw new import_js_format9.InvalidArgumentError(
531
613
  'Option "encoding" must be a String, but %v was given.',
532
614
  options.encoding
533
615
  );
534
616
  }
535
617
  if (!CHARACTER_ENCODING_LIST.includes(options.encoding)) {
536
- throw new import_js_format8.InvalidArgumentError(
618
+ throw new import_js_format9.InvalidArgumentError(
537
619
  "Character encoding %v is not supported.",
538
620
  options.encoding
539
621
  );
540
622
  }
541
623
  }
542
- if (options.stream) {
624
+ if (options.url !== void 0) {
625
+ if (options.path !== void 0) {
626
+ throw new import_js_format9.InvalidArgumentError(
627
+ 'The "url" and "path" options cannot be used together.'
628
+ );
629
+ }
630
+ if (options.query !== void 0) {
631
+ throw new import_js_format9.InvalidArgumentError(
632
+ 'The "url" and "query" options cannot be used together.'
633
+ );
634
+ }
635
+ }
636
+ if (options.stream !== void 0) {
543
637
  if (options.secure !== void 0) {
544
- throw new import_js_format8.InvalidArgumentError(
638
+ throw new import_js_format9.InvalidArgumentError(
545
639
  'The "stream" and "secure" options cannot be used together.'
546
640
  );
547
641
  }
548
642
  if (options.body !== void 0) {
549
- throw new import_js_format8.InvalidArgumentError(
643
+ throw new import_js_format9.InvalidArgumentError(
550
644
  'The "stream" and "body" options cannot be used together.'
551
645
  );
552
646
  }
553
647
  if (options.encoding !== void 0) {
554
- throw new import_js_format8.InvalidArgumentError(
648
+ throw new import_js_format9.InvalidArgumentError(
555
649
  'The "stream" and "encoding" options cannot be used together.'
556
650
  );
557
651
  }
@@ -575,11 +669,17 @@ function createRequestMock(options) {
575
669
  Object.defineProperty(request.socket, "remoteAddress", { value: "127.0.0.1" });
576
670
  Object.defineProperty(request.socket, "localAddress", { value: "127.0.0.1" });
577
671
  request.httpVersion = "1.1";
578
- request.url = options.url || "/";
672
+ request.url = "/";
673
+ if (options.url !== void 0) {
674
+ request.url = options.url;
675
+ } else if (options.path !== void 0 || options.query !== void 0) {
676
+ request.url = createRequestUrl(options.path, options.query);
677
+ }
579
678
  request.headers = createRequestHeaders(
580
679
  options.host,
581
680
  options.secure,
582
681
  options.body,
682
+ options.cookies,
583
683
  options.encoding,
584
684
  options.headers
585
685
  );
@@ -589,7 +689,7 @@ function createRequestMock(options) {
589
689
  __name(createRequestMock, "createRequestMock");
590
690
  function createRequestStream(secure, body, encoding) {
591
691
  if (encoding !== void 0 && typeof encoding !== "string") {
592
- throw new import_js_format8.InvalidArgumentError(
692
+ throw new import_js_format9.InvalidArgumentError(
593
693
  'Parameter "encoding" must be a String, but %v was given.',
594
694
  encoding
595
695
  );
@@ -613,30 +713,67 @@ function createRequestStream(secure, body, encoding) {
613
713
  return request;
614
714
  }
615
715
  __name(createRequestStream, "createRequestStream");
616
- function createRequestHeaders(host, secure, body, encoding, headers) {
716
+ function createRequestUrl(path, query) {
717
+ if (path !== void 0 && typeof path !== "string") {
718
+ throw new import_js_format9.InvalidArgumentError(
719
+ 'Parameter "path" must be a String, but %v was given.',
720
+ path
721
+ );
722
+ }
723
+ if (query !== void 0) {
724
+ if (query === null || typeof query !== "string" && typeof query !== "object" || Array.isArray(query)) {
725
+ throw new import_js_format9.InvalidArgumentError(
726
+ 'Parameter "query" must be a String or an Object, but %v was given.',
727
+ query
728
+ );
729
+ }
730
+ }
731
+ let res = path !== void 0 ? path : "/";
732
+ if (typeof query === "object") {
733
+ const qs = import_querystring.default.stringify(query);
734
+ if (qs) {
735
+ res += `?${qs}`;
736
+ }
737
+ } else if (typeof query === "string" && query !== "" && query !== "?") {
738
+ res += `?${query.replace(/^\?/, "")}`;
739
+ }
740
+ return res;
741
+ }
742
+ __name(createRequestUrl, "createRequestUrl");
743
+ function createRequestHeaders(host, secure, body, cookies, encoding, headers) {
617
744
  if (host !== void 0 && typeof host !== "string") {
618
- throw new import_js_format8.InvalidArgumentError(
745
+ throw new import_js_format9.InvalidArgumentError(
619
746
  'Parameter "host" must be a non-empty String, but %v was given.',
620
747
  host
621
748
  );
622
749
  }
623
750
  host = host || "localhost";
624
751
  if (secure !== void 0 && typeof secure !== "boolean") {
625
- throw new import_js_format8.InvalidArgumentError(
752
+ throw new import_js_format9.InvalidArgumentError(
626
753
  'Parameter "secure" must be a Boolean, but %v was given.',
627
754
  secure
628
755
  );
629
756
  }
630
757
  secure = Boolean(secure);
631
- if (headers !== void 0 && typeof headers !== "object" || Array.isArray(headers)) {
632
- throw new import_js_format8.InvalidArgumentError(
633
- 'Parameter "headers" must be an Object, but %v was given.',
634
- headers
635
- );
758
+ if (cookies !== void 0) {
759
+ if (!cookies || typeof cookies !== "object" || Array.isArray(cookies)) {
760
+ throw new import_js_format9.InvalidArgumentError(
761
+ 'Parameter "cookies" must be an Object, but %v was given.',
762
+ cookies
763
+ );
764
+ }
765
+ }
766
+ if (headers !== void 0) {
767
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
768
+ throw new import_js_format9.InvalidArgumentError(
769
+ 'Parameter "headers" must be an Object, but %v was given.',
770
+ headers
771
+ );
772
+ }
636
773
  }
637
774
  headers = headers || {};
638
775
  if (encoding !== void 0 && typeof encoding !== "string") {
639
- throw new import_js_format8.InvalidArgumentError(
776
+ throw new import_js_format9.InvalidArgumentError(
640
777
  'Parameter "encoding" must be a String, but %v was given.',
641
778
  encoding
642
779
  );
@@ -652,6 +789,14 @@ function createRequestHeaders(host, secure, body, encoding, headers) {
652
789
  if (secure) {
653
790
  res["x-forwarded-proto"] = "https";
654
791
  }
792
+ if (typeof cookies === "object" && Object.keys(cookies).length) {
793
+ if (res["cookie"]) {
794
+ const existedCookies = parseCookieString(res["cookie"]);
795
+ res["cookie"] = createCookieString({ ...existedCookies, ...cookies });
796
+ } else {
797
+ res["cookie"] = createCookieString(cookies);
798
+ }
799
+ }
655
800
  if (body != null && !("content-type" in res)) {
656
801
  if (typeof body === "string") {
657
802
  res["content-type"] = "text/plain";
@@ -785,12 +930,12 @@ function patchBody(response) {
785
930
  __name(patchBody, "patchBody");
786
931
 
787
932
  // src/utils/get-request-pathname.js
788
- var import_js_format9 = require("@e22m4u/js-format");
933
+ var import_js_format10 = require("@e22m4u/js-format");
789
934
  var HOST_RE2 = /^https?:\/\/[^/]+/;
790
935
  var QUERY_STRING_RE = /\?.*$/;
791
936
  function getRequestPathname(request) {
792
937
  if (!request || typeof request !== "object" || Array.isArray(request) || typeof request.url !== "string") {
793
- throw new import_js_format9.InvalidArgumentError(
938
+ throw new import_js_format10.InvalidArgumentError(
794
939
  'Parameter "request" must be an instance of IncomingMessage, but %v was given.',
795
940
  request
796
941
  );
@@ -802,30 +947,6 @@ function getRequestPathname(request) {
802
947
  }
803
948
  __name(getRequestPathname, "getRequestPathname");
804
949
 
805
- // src/utils/create-cookie-string.js
806
- var import_js_format10 = require("@e22m4u/js-format");
807
- function createCookieString(data) {
808
- if (!data || typeof data !== "object" || Array.isArray(data)) {
809
- throw new import_js_format10.InvalidArgumentError(
810
- "Cookie data must be an Object, but %v was given.",
811
- data
812
- );
813
- }
814
- let cookies = "";
815
- for (const key in data) {
816
- if (!Object.prototype.hasOwnProperty.call(data, key)) {
817
- continue;
818
- }
819
- const val = data[key];
820
- if (val == null) {
821
- continue;
822
- }
823
- cookies += `${key}=${val}; `;
824
- }
825
- return cookies.trim();
826
- }
827
- __name(createCookieString, "createCookieString");
828
-
829
950
  // src/request-context.js
830
951
  var import_js_format11 = require("@e22m4u/js-format");
831
952
  var import_js_service2 = require("@e22m4u/js-service");
@@ -2227,7 +2348,7 @@ function parseJsonBody(input) {
2227
2348
  __name(parseJsonBody, "parseJsonBody");
2228
2349
 
2229
2350
  // src/parsers/request-query-parser.js
2230
- var import_querystring = __toESM(require("querystring"), 1);
2351
+ var import_querystring2 = __toESM(require("querystring"), 1);
2231
2352
  var _RequestQueryParser = class _RequestQueryParser extends DebuggableService {
2232
2353
  /**
2233
2354
  * Parse
@@ -2238,7 +2359,7 @@ var _RequestQueryParser = class _RequestQueryParser extends DebuggableService {
2238
2359
  parse(request) {
2239
2360
  const debug = this.getDebuggerFor(this.parse);
2240
2361
  const queryStr = request.url.replace(/^[^?]*\??/, "");
2241
- const query = queryStr ? import_querystring.default.parse(queryStr) : {};
2362
+ const query = queryStr ? import_querystring2.default.parse(queryStr) : {};
2242
2363
  const queryKeys = Object.keys(query);
2243
2364
  if (queryKeys.length) {
2244
2365
  queryKeys.forEach((key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e22m4u/js-trie-router",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "HTTP маршрутизатор для Node.js на основе префиксного дерева",
5
5
  "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
6
6
  "license": "MIT",
@@ -78,7 +78,7 @@ describe('RequestParser', function () {
78
78
 
79
79
  it('should return the result object with parsed cookies', function () {
80
80
  const S = new RequestParser();
81
- const req = createRequestMock({headers: {cookie: 'p1=foo; p2=bar;'}});
81
+ const req = createRequestMock({headers: {cookie: 'p1=foo; p2=bar'}});
82
82
  const res = S.parse(req);
83
83
  expect(res).to.be.eql({
84
84
  query: {},
@@ -86,7 +86,7 @@ describe('RequestParser', function () {
86
86
  body: undefined,
87
87
  headers: {
88
88
  host: 'localhost',
89
- cookie: 'p1=foo; p2=bar;',
89
+ cookie: 'p1=foo; p2=bar',
90
90
  },
91
91
  });
92
92
  });
@@ -12,13 +12,13 @@ describe('RequestQueryParser', function () {
12
12
 
13
13
  it('should return an empty object when the url does not have a query string', function () {
14
14
  const parser = new RequestQueryParser();
15
- const result = parser.parse({url: `/test`});
15
+ const result = parser.parse({url: '/test'});
16
16
  expect(result).to.be.eql({});
17
17
  });
18
18
 
19
19
  it('should return an empty object when the url has an empty query string', function () {
20
20
  const parser = new RequestQueryParser();
21
- const result = parser.parse({url: `/test?`});
21
+ const result = parser.parse({url: '/test?'});
22
22
  expect(result).to.be.eql({});
23
23
  });
24
24
  });
@@ -139,6 +139,16 @@ describe('RequestContext', function () {
139
139
  expect(req.url).to.be.eq('/pathname?foo=bar');
140
140
  expect(ctx.path).to.be.eq('/pathname?foo=bar');
141
141
  });
142
+
143
+ it('should ignore a protocol and host in the request url', function () {
144
+ const req = createRequestMock({url: 'http://example.com:80/pathname'});
145
+ const res = createResponseMock();
146
+ const route = createRouteMock();
147
+ const cont = new ServiceContainer();
148
+ const ctx = new RequestContext(cont, req, res, route);
149
+ expect(req.url).to.be.eq('http://example.com:80/pathname');
150
+ expect(ctx.path).to.be.eq('/pathname');
151
+ });
142
152
  });
143
153
 
144
154
  describe('pathname', function () {
@@ -142,8 +142,8 @@ describe('RouteRegistry', function () {
142
142
  handler,
143
143
  });
144
144
  const res = S.matchRouteByRequest({
145
- url: '/foo/baz/bar/qux',
146
145
  method: HttpMethod.GET,
146
+ url: '/foo/baz/bar/qux',
147
147
  });
148
148
  expect(typeof res).to.be.eq('object');
149
149
  expect(res.route).to.be.instanceof(Route);
@@ -88,7 +88,7 @@ describe('TrieRouter', function () {
88
88
  done();
89
89
  },
90
90
  });
91
- const req = createRequestMock({url: '/test'});
91
+ const req = createRequestMock({path: '/test'});
92
92
  const res = createResponseMock();
93
93
  router.handleRequest(req, res);
94
94
  });
@@ -103,7 +103,7 @@ describe('TrieRouter', function () {
103
103
  done();
104
104
  },
105
105
  });
106
- const req = createRequestMock({url: '/foo-bar'});
106
+ const req = createRequestMock({path: '/foo-bar'});
107
107
  const res = createResponseMock();
108
108
  router.handleRequest(req, res);
109
109
  });
@@ -118,7 +118,7 @@ describe('TrieRouter', function () {
118
118
  done();
119
119
  },
120
120
  });
121
- const req = createRequestMock({url: '?p1=foo&p2=bar'});
121
+ const req = createRequestMock({query: {p1: 'foo', p2: 'bar'}});
122
122
  const res = createResponseMock();
123
123
  router.handleRequest(req, res);
124
124
  });
@@ -385,7 +385,7 @@ describe('TrieRouter', function () {
385
385
  router.addHook(RouterHookType.ON_REQUEST, () => {
386
386
  hookCalled = true;
387
387
  });
388
- const req = createRequestMock({url: '/does-not-exist'});
388
+ const req = createRequestMock({path: '/does-not-exist'});
389
389
  const res = createResponseMock();
390
390
  await router.handleRequest(req, res);
391
391
  expect(hookCalled).to.be.true;
@@ -1641,7 +1641,7 @@ describe('TrieRouter', function () {
1641
1641
  });
1642
1642
  const req = createRequestMock({
1643
1643
  method: HttpMethod.OPTIONS,
1644
- url: '/api/resource',
1644
+ path: '/api/resource',
1645
1645
  });
1646
1646
  const res = createResponseMock();
1647
1647
  await router.handleRequest(req, res);
@@ -1662,7 +1662,7 @@ describe('TrieRouter', function () {
1662
1662
  });
1663
1663
  const req = createRequestMock({
1664
1664
  method: HttpMethod.OPTIONS,
1665
- url: '/api/resource',
1665
+ path: '/api/resource',
1666
1666
  });
1667
1667
  const res = createResponseMock();
1668
1668
  await router.handleRequest(req, res);
@@ -1676,7 +1676,7 @@ describe('TrieRouter', function () {
1676
1676
  const router = new TrieRouter();
1677
1677
  const req = createRequestMock({
1678
1678
  method: HttpMethod.OPTIONS,
1679
- url: '/unknown',
1679
+ path: '/unknown',
1680
1680
  });
1681
1681
  const res = createResponseMock();
1682
1682
  await router.handleRequest(req, res);
@@ -24,5 +24,5 @@ export function createCookieString(data) {
24
24
  }
25
25
  cookies += `${key}=${val}; `;
26
26
  }
27
- return cookies.trim();
27
+ return cookies.trim().replace(/;$/, '');
28
28
  }
@@ -27,6 +27,6 @@ describe('createCookieString', function () {
27
27
  it('should return the cookie string for the given object', function () {
28
28
  const data = {foo: 'bar', baz: 'quz'};
29
29
  const result = createCookieString(data);
30
- expect(result).to.be.eq('foo=bar; baz=quz;');
30
+ expect(result).to.be.eq('foo=bar; baz=quz');
31
31
  });
32
32
  });
@@ -16,6 +16,9 @@ type RequestOptions = {
16
16
  method?: string;
17
17
  secure?: boolean;
18
18
  url?: string;
19
+ path?: string;
20
+ query?: string | object;
21
+ cookies?: object;
19
22
  headers?: RequestHeadersInput;
20
23
  body?: unknown;
21
24
  stream?: Readable;
@@ -1,8 +1,11 @@
1
1
  import {Socket} from 'net';
2
2
  import {TLSSocket} from 'tls';
3
+ import queryString from 'querystring';
3
4
  import {IncomingMessage} from 'http';
4
5
  import {InvalidArgumentError} from '@e22m4u/js-format';
5
6
  import {isReadableStream} from './is-readable-stream.js';
7
+ import {parseCookieString} from './parse-cookie-string.js';
8
+ import {createCookieString} from './create-cookie-string.js';
6
9
  import {CHARACTER_ENCODING_LIST} from './fetch-request-body.js';
7
10
 
8
11
  /**
@@ -13,6 +16,9 @@ const SUPPORTED_OPTIONS = [
13
16
  'method',
14
17
  'secure',
15
18
  'url',
19
+ 'path',
20
+ 'query',
21
+ 'cookies',
16
22
  'headers',
17
23
  'body',
18
24
  'stream',
@@ -77,6 +83,74 @@ export function createRequestMock(options) {
77
83
  );
78
84
  }
79
85
  }
86
+ // options.path
87
+ if (options.path !== undefined) {
88
+ if (typeof options.path !== 'string') {
89
+ throw new InvalidArgumentError(
90
+ 'Option "path" must be a String, but %v was given.',
91
+ options.path,
92
+ );
93
+ }
94
+ // contain #
95
+ if (options.path.indexOf('#') !== -1) {
96
+ throw new InvalidArgumentError(
97
+ 'Option "path" must not contain "#", but %v was given.',
98
+ options.path,
99
+ );
100
+ }
101
+ // contain ?
102
+ if (options.path.indexOf('?') !== -1) {
103
+ throw new InvalidArgumentError(
104
+ 'Option "path" must not contain "?", but %v was given.',
105
+ options.path,
106
+ );
107
+ }
108
+ // not starting with /
109
+ if (!options.path.startsWith('/')) {
110
+ throw new InvalidArgumentError(
111
+ 'Option "path" must start with "/", but %v was given.',
112
+ options.path,
113
+ );
114
+ }
115
+ }
116
+ // options.query
117
+ if (options.query !== undefined) {
118
+ if (
119
+ options.query === null ||
120
+ (typeof options.query !== 'string' &&
121
+ typeof options.query !== 'object') ||
122
+ Array.isArray(options.query)
123
+ ) {
124
+ throw new InvalidArgumentError(
125
+ 'Option "query" must be a String or an Object, but %v was given.',
126
+ options.query,
127
+ );
128
+ }
129
+ }
130
+ // options.cookies
131
+ if (options.cookies !== undefined) {
132
+ if (
133
+ !options.cookies ||
134
+ typeof options.cookies !== 'object' ||
135
+ Array.isArray(options.cookies)
136
+ ) {
137
+ throw new InvalidArgumentError(
138
+ 'Option "cookies" must be an Object, but %v was given.',
139
+ options.cookies,
140
+ );
141
+ }
142
+ // options.cookies[k]
143
+ Object.keys(options.cookies).forEach(cookieName => {
144
+ const cookieValue = options.cookies[cookieName];
145
+ if (cookieValue !== undefined && typeof cookieValue !== 'string') {
146
+ throw new InvalidArgumentError(
147
+ 'Cookie %v must be a String, but %v was given.',
148
+ cookieName,
149
+ cookieValue,
150
+ );
151
+ }
152
+ });
153
+ }
80
154
  // options.headers
81
155
  if (options.headers !== undefined) {
82
156
  if (
@@ -139,9 +213,23 @@ export function createRequestMock(options) {
139
213
  );
140
214
  }
141
215
  }
216
+ // если определен url, выполняется
217
+ // проверка на несовместимые опции
218
+ if (options.url !== undefined) {
219
+ if (options.path !== undefined) {
220
+ throw new InvalidArgumentError(
221
+ 'The "url" and "path" options cannot be used together.',
222
+ );
223
+ }
224
+ if (options.query !== undefined) {
225
+ throw new InvalidArgumentError(
226
+ 'The "url" and "query" options cannot be used together.',
227
+ );
228
+ }
229
+ }
142
230
  // если передан поток, выполняется
143
231
  // проверка на несовместимые опции
144
- if (options.stream) {
232
+ if (options.stream !== undefined) {
145
233
  if (options.secure !== undefined) {
146
234
  throw new InvalidArgumentError(
147
235
  'The "stream" and "secure" options cannot be used together.',
@@ -184,11 +272,17 @@ export function createRequestMock(options) {
184
272
  // определение остальных свойств
185
273
  // экземпляра IncomingMessage
186
274
  request.httpVersion = '1.1';
187
- request.url = options.url || '/';
275
+ request.url = '/';
276
+ if (options.url !== undefined) {
277
+ request.url = options.url;
278
+ } else if (options.path !== undefined || options.query !== undefined) {
279
+ request.url = createRequestUrl(options.path, options.query);
280
+ }
188
281
  request.headers = createRequestHeaders(
189
282
  options.host,
190
283
  options.secure,
191
284
  options.body,
285
+ options.cookies,
192
286
  options.encoding,
193
287
  options.headers,
194
288
  );
@@ -238,17 +332,56 @@ function createRequestStream(secure, body, encoding) {
238
332
  return request;
239
333
  }
240
334
 
335
+ /**
336
+ * Create request url.
337
+ *
338
+ * @param {string|undefined} path
339
+ * @param {string|object|undefined} query
340
+ * @returns {string}
341
+ */
342
+ function createRequestUrl(path, query) {
343
+ if (path !== undefined && typeof path !== 'string') {
344
+ throw new InvalidArgumentError(
345
+ 'Parameter "path" must be a String, but %v was given.',
346
+ path,
347
+ );
348
+ }
349
+ if (query !== undefined) {
350
+ if (
351
+ query === null ||
352
+ (typeof query !== 'string' && typeof query !== 'object') ||
353
+ Array.isArray(query)
354
+ ) {
355
+ throw new InvalidArgumentError(
356
+ 'Parameter "query" must be a String or an Object, but %v was given.',
357
+ query,
358
+ );
359
+ }
360
+ }
361
+ let res = path !== undefined ? path : '/';
362
+ if (typeof query === 'object') {
363
+ const qs = queryString.stringify(query);
364
+ if (qs) {
365
+ res += `?${qs}`;
366
+ }
367
+ } else if (typeof query === 'string' && query !== '' && query !== '?') {
368
+ res += `?${query.replace(/^\?/, '')}`;
369
+ }
370
+ return res;
371
+ }
372
+
241
373
  /**
242
374
  * Create request headers.
243
375
  *
244
376
  * @param {string|undefined} host
245
377
  * @param {boolean|undefined} secure
246
378
  * @param {*} body
379
+ * @param {object|undefined} cookies
247
380
  * @param {string|undefined} encoding
248
381
  * @param {object|undefined} headers
249
382
  * @returns {object}
250
383
  */
251
- function createRequestHeaders(host, secure, body, encoding, headers) {
384
+ function createRequestHeaders(host, secure, body, cookies, encoding, headers) {
252
385
  if (host !== undefined && typeof host !== 'string') {
253
386
  throw new InvalidArgumentError(
254
387
  'Parameter "host" must be a non-empty String, but %v was given.',
@@ -263,14 +396,21 @@ function createRequestHeaders(host, secure, body, encoding, headers) {
263
396
  );
264
397
  }
265
398
  secure = Boolean(secure);
266
- if (
267
- (headers !== undefined && typeof headers !== 'object') ||
268
- Array.isArray(headers)
269
- ) {
270
- throw new InvalidArgumentError(
271
- 'Parameter "headers" must be an Object, but %v was given.',
272
- headers,
273
- );
399
+ if (cookies !== undefined) {
400
+ if (!cookies || typeof cookies !== 'object' || Array.isArray(cookies)) {
401
+ throw new InvalidArgumentError(
402
+ 'Parameter "cookies" must be an Object, but %v was given.',
403
+ cookies,
404
+ );
405
+ }
406
+ }
407
+ if (headers !== undefined) {
408
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) {
409
+ throw new InvalidArgumentError(
410
+ 'Parameter "headers" must be an Object, but %v was given.',
411
+ headers,
412
+ );
413
+ }
274
414
  }
275
415
  headers = headers || {};
276
416
  if (encoding !== undefined && typeof encoding !== 'string') {
@@ -290,6 +430,17 @@ function createRequestHeaders(host, secure, body, encoding, headers) {
290
430
  if (secure) {
291
431
  res['x-forwarded-proto'] = 'https';
292
432
  }
433
+ // формирование заголовка Cookie используя
434
+ // существующие данные заголовка и объекта,
435
+ // переданного в параметр данной функции
436
+ if (typeof cookies === 'object' && Object.keys(cookies).length) {
437
+ if (res['cookie']) {
438
+ const existedCookies = parseCookieString(res['cookie']);
439
+ res['cookie'] = createCookieString({...existedCookies, ...cookies});
440
+ } else {
441
+ res['cookie'] = createCookieString(cookies);
442
+ }
443
+ }
293
444
  // установка заголовка "content-type"
294
445
  // в зависимости от тела запроса
295
446
  if (body != null && !('content-type' in res)) {
@@ -88,19 +88,121 @@ describe('createRequestMock', function () {
88
88
  throwable(undefined)();
89
89
  });
90
90
 
91
- it('should require the option "url" to not contain "#" symbol', function () {
91
+ it('should require the option "url" to not contain "#"', function () {
92
92
  const throwable = v => () => createRequestMock({url: v});
93
- const mustThrowFor = v => {
93
+ const mustThrowWith = v => {
94
94
  expect(throwable(v)).to.throw(
95
95
  format('Option "url" must not contain "#", but %v was given.', v),
96
96
  );
97
97
  };
98
- mustThrowFor('#');
99
- mustThrowFor('pathname#');
100
- mustThrowFor('/pathname#');
101
- mustThrowFor('http://example.com/#');
102
- mustThrowFor('http://example.com/pathname#');
103
- mustThrowFor('http://example.com/pathname?foo=bar#');
98
+ mustThrowWith('#');
99
+ mustThrowWith('pathname#');
100
+ mustThrowWith('/pathname#');
101
+ mustThrowWith('http://example.com/#');
102
+ mustThrowWith('http://example.com/pathname#');
103
+ mustThrowWith('http://example.com/pathname?foo=bar#');
104
+ });
105
+
106
+ it('should require the option "path" to be a String', function () {
107
+ const throwable = v => () => createRequestMock({path: v});
108
+ const error = v =>
109
+ format('Option "path" must be a String, but %s was given.', v);
110
+ expect(throwable(10)).to.throw(error('10'));
111
+ expect(throwable(0)).to.throw(error('0'));
112
+ expect(throwable(true)).to.throw(error('true'));
113
+ expect(throwable(false)).to.throw(error('false'));
114
+ expect(throwable([])).to.throw(error('Array'));
115
+ expect(throwable({})).to.throw(error('Object'));
116
+ expect(throwable(null)).to.throw(error('null'));
117
+ throwable('/path')();
118
+ throwable('/')();
119
+ throwable(undefined)();
120
+ });
121
+
122
+ it('should require the option "path" to not contain "#"', function () {
123
+ const throwable = v => () => createRequestMock({path: v});
124
+ const mustThrowWith = v => {
125
+ expect(throwable(v)).to.throw(
126
+ format('Option "path" must not contain "#", but %v was given.', v),
127
+ );
128
+ };
129
+ mustThrowWith('/#');
130
+ mustThrowWith('/path#');
131
+ });
132
+
133
+ it('should require the option "path" to not contain "?"', function () {
134
+ const throwable = v => () => createRequestMock({path: v});
135
+ const mustThrowWith = v => {
136
+ expect(throwable(v)).to.throw(
137
+ format('Option "path" must not contain "?", but %v was given.', v),
138
+ );
139
+ };
140
+ mustThrowWith('/?');
141
+ mustThrowWith('/path?');
142
+ });
143
+
144
+ it('should require the option "path" to start with "/"', function () {
145
+ const throwable = v => () => createRequestMock({path: v});
146
+ const mustThrowWith = v => {
147
+ expect(throwable(v)).to.throw(
148
+ format('Option "path" must start with "/", but %v was given.', v),
149
+ );
150
+ };
151
+ mustThrowWith('path');
152
+ mustThrowWith('');
153
+ });
154
+
155
+ it('should require the option "query" to be a String or an Object', function () {
156
+ const throwable = v => () => createRequestMock({query: v});
157
+ const error = v =>
158
+ format(
159
+ 'Option "query" must be a String or an Object, but %s was given.',
160
+ v,
161
+ );
162
+ expect(throwable(10)).to.throw(error('10'));
163
+ expect(throwable(0)).to.throw(error('0'));
164
+ expect(throwable(true)).to.throw(error('true'));
165
+ expect(throwable(false)).to.throw(error('false'));
166
+ expect(throwable([])).to.throw(error('Array'));
167
+ expect(throwable(null)).to.throw(error('null'));
168
+ throwable({foo: 'bar'})();
169
+ throwable({})();
170
+ throwable('foo=bar')();
171
+ throwable('')();
172
+ throwable(undefined)();
173
+ });
174
+
175
+ it('should require the option "cookies" to be an Object', function () {
176
+ const throwable = v => () => createRequestMock({cookies: v});
177
+ const error = v =>
178
+ format('Option "cookies" must be an Object, but %s was given.', v);
179
+ expect(throwable('str')).to.throw(error('"str"'));
180
+ expect(throwable('')).to.throw(error('""'));
181
+ expect(throwable(10)).to.throw(error('10'));
182
+ expect(throwable(0)).to.throw(error('0'));
183
+ expect(throwable(true)).to.throw(error('true'));
184
+ expect(throwable(false)).to.throw(error('false'));
185
+ expect(throwable([])).to.throw(error('Array'));
186
+ expect(throwable(null)).to.throw(error('null'));
187
+ throwable({foo: 'bar'})();
188
+ throwable({})();
189
+ throwable(undefined)();
190
+ });
191
+
192
+ it('should require values in the option "cookies" to be a String', function () {
193
+ const throwable = v => () => createRequestMock({cookies: {test: v}});
194
+ const error = v =>
195
+ format('Cookie "test" must be a String, but %s was given.', v);
196
+ expect(throwable(10)).to.throw(error('10'));
197
+ expect(throwable(0)).to.throw(error('0'));
198
+ expect(throwable(true)).to.throw(error('true'));
199
+ expect(throwable(false)).to.throw(error('false'));
200
+ expect(throwable([])).to.throw(error('Array'));
201
+ expect(throwable({})).to.throw(error('Object'));
202
+ expect(throwable(null)).to.throw(error('null'));
203
+ throwable('str')();
204
+ throwable('')();
205
+ throwable(undefined)();
104
206
  });
105
207
 
106
208
  it('should require the option "headers" to be an Object', function () {
@@ -120,7 +222,7 @@ describe('createRequestMock', function () {
120
222
  throwable(undefined)();
121
223
  });
122
224
 
123
- it('should require values of the option "headers" to be a String or an Array', function () {
225
+ it('should require values in the option "headers" to be a String or an Array', function () {
124
226
  const throwable = v => () => createRequestMock({headers: {Test: v}});
125
227
  const error = v =>
126
228
  format(
@@ -196,6 +298,18 @@ describe('createRequestMock', function () {
196
298
  expect(throwable).to.throw('Option "unknownOption" is not supported.');
197
299
  });
198
300
 
301
+ it('should not allow using the "url" and "path" options together', function () {
302
+ const throwable = () => createRequestMock({url: 'url', path: '/path'});
303
+ const error = 'The "url" and "path" options cannot be used together.';
304
+ expect(throwable).to.throw(error);
305
+ });
306
+
307
+ it('should not allow using the "url" and "query" options together', function () {
308
+ const throwable = () => createRequestMock({url: 'url', query: {p: 1}});
309
+ const error = 'The "url" and "query" options cannot be used together.';
310
+ expect(throwable).to.throw(error);
311
+ });
312
+
199
313
  it('should require the option "encoding" to be a correct value', function () {
200
314
  const throwable = v => () => createRequestMock({encoding: v});
201
315
  const error = v => format('Character encoding %s is not supported.', v);
@@ -232,11 +346,6 @@ describe('createRequestMock', function () {
232
346
  throwable(undefined)();
233
347
  });
234
348
 
235
- it('should use "localhost" as the default host', function () {
236
- const req = createRequestMock();
237
- expect(req.headers['host']).to.be.eq('localhost');
238
- });
239
-
240
349
  it('should use "GET" as the default method', function () {
241
350
  const req = createRequestMock();
242
351
  expect(req.method).to.be.eq('GET');
@@ -247,17 +356,17 @@ describe('createRequestMock', function () {
247
356
  expect(req.socket).to.be.instanceof(Socket);
248
357
  });
249
358
 
250
- it('should use the default path "/" without a query string', function () {
359
+ it('should use "/" as the default value of the request url', function () {
251
360
  const req = createRequestMock();
252
361
  expect(req.url).to.be.eq('/');
253
362
  });
254
363
 
255
- it('should use "localhost" as the default value for the "host" header', function () {
364
+ it('should use "localhost" as the default value of the "host" header', function () {
256
365
  const req = createRequestMock();
257
366
  expect(req.headers).to.be.eql({host: 'localhost'});
258
367
  });
259
368
 
260
- it('should use "utf-8" encoding by default', async function () {
369
+ it('should use "utf-8" as the default value of the data encoding', async function () {
261
370
  const body = 'test';
262
371
  const req = createRequestMock({body: Buffer.from(body)});
263
372
  const chunks = [];
@@ -327,7 +436,43 @@ describe('createRequestMock', function () {
327
436
  expect(data).to.be.eq(body);
328
437
  });
329
438
 
330
- it('should stringify and pass an Object body to the stream', async function () {
439
+ it('should pass a number from the "body" option to the stream as a string', async function () {
440
+ const body = 10;
441
+ const req = createRequestMock({body});
442
+ const chunks = [];
443
+ const data = await new Promise((resolve, reject) => {
444
+ req.on('data', chunk => chunks.push(Buffer.from(chunk)));
445
+ req.on('error', err => reject(err));
446
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
447
+ });
448
+ expect(data).to.be.eq('10');
449
+ });
450
+
451
+ it('should pass a boolean from the "body" option to the stream as a string', async function () {
452
+ const body = true;
453
+ const req = createRequestMock({body});
454
+ const chunks = [];
455
+ const data = await new Promise((resolve, reject) => {
456
+ req.on('data', chunk => chunks.push(Buffer.from(chunk)));
457
+ req.on('error', err => reject(err));
458
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
459
+ });
460
+ expect(data).to.be.eq('true');
461
+ });
462
+
463
+ it('should pass an array from the "body" option to the stream as JSON', async function () {
464
+ const body = [1, 2];
465
+ const req = createRequestMock({body});
466
+ const chunks = [];
467
+ const data = await new Promise((resolve, reject) => {
468
+ req.on('data', chunk => chunks.push(Buffer.from(chunk)));
469
+ req.on('error', err => reject(err));
470
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
471
+ });
472
+ expect(data).to.be.eq(JSON.stringify(body));
473
+ });
474
+
475
+ it('should pass an object from the "body" option to the stream as JSON', async function () {
331
476
  const body = {foo: 'bar'};
332
477
  const req = createRequestMock({body});
333
478
  const chunks = [];
@@ -339,7 +484,7 @@ describe('createRequestMock', function () {
339
484
  expect(data).to.be.eq(JSON.stringify(body));
340
485
  });
341
486
 
342
- it('should pass a binary data to the stream', async function () {
487
+ it('should pass a Buffer from the "body" option to the stream', async function () {
343
488
  const body = Buffer.from('test');
344
489
  const req = createRequestMock({body});
345
490
  const chunks = [];
@@ -351,12 +496,38 @@ describe('createRequestMock', function () {
351
496
  expect(data).to.be.eql(body);
352
497
  });
353
498
 
354
- it('should set the "url" option to the request url', function () {
499
+ it('should pass a value form the "url" option to the request url', function () {
355
500
  const req = createRequestMock({url: '/test'});
356
501
  expect(req.url).to.be.eq('/test');
357
502
  });
358
503
 
359
- it('should set the property "method" in upper case', async function () {
504
+ it('should pass a value form the "path" option to the request url', function () {
505
+ const req = createRequestMock({path: '/test'});
506
+ expect(req.url).to.be.eq('/test');
507
+ });
508
+
509
+ it('should pass a string form the "query" option to the request url', async function () {
510
+ const req1 = createRequestMock({query: 'p1=foo&p2=bar'});
511
+ const req2 = createRequestMock({query: '?p1=foo&p2=bar'});
512
+ expect(req1.url).to.be.eq('/?p1=foo&p2=bar');
513
+ expect(req2.url).to.be.eq('/?p1=foo&p2=bar');
514
+ });
515
+
516
+ it('should pass an object form the "query" option to the request url', async function () {
517
+ const req = createRequestMock({query: {foo: 'bar', baz: 'qux'}});
518
+ expect(req.url).to.be.eq('/?foo=bar&baz=qux');
519
+ });
520
+
521
+ it('should combine the "path" and "query" options in the request url', function () {
522
+ const req1 = createRequestMock({path: '/test', query: 'foo=bar'});
523
+ const req2 = createRequestMock({path: '/test', query: '?foo=bar'});
524
+ const req3 = createRequestMock({path: '/test', query: {foo: 'bar'}});
525
+ expect(req1.url).to.be.eq('/test?foo=bar');
526
+ expect(req2.url).to.be.eq('/test?foo=bar');
527
+ expect(req3.url).to.be.eq('/test?foo=bar');
528
+ });
529
+
530
+ it('should set a value from the "method" option to the request method in upper case', async function () {
360
531
  const req1 = createRequestMock({method: 'get'});
361
532
  const req2 = createRequestMock({method: 'post'});
362
533
  expect(req1.method).to.be.eq('GET');
@@ -374,6 +545,19 @@ describe('createRequestMock', function () {
374
545
  expect(req.headers['x-forwarded-proto']).to.be.eq('https');
375
546
  });
376
547
 
548
+ it('should serialize and set a value from the "cookies" option to the "cookie" header', function () {
549
+ const req = createRequestMock({cookies: {p1: 'foo', p2: 'bar'}});
550
+ expect(req.headers['cookie']).to.be.eq('p1=foo; p2=bar');
551
+ });
552
+
553
+ it('should merge the "cookie" header with the "cookies" option', function () {
554
+ const req = createRequestMock({
555
+ headers: {cookie: 'p1=foo; p2=bar'},
556
+ cookies: {p2: 'baz', p3: 'qux'},
557
+ });
558
+ expect(req.headers['cookie']).to.be.eq('p1=foo; p2=baz; p3=qux');
559
+ });
560
+
377
561
  it('should set the "content-type" header for a String body', function () {
378
562
  const req = createRequestMock({body: 'test'});
379
563
  expect(req.headers['content-type']).to.be.eq('text/plain');