trie-router.spec.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. import {expect} from 'chai';
  2. import {TrieRouter} from './trie-router.js';
  3. import {Route, HttpMethod} from './route.js';
  4. import {RequestContext} from './request-context.js';
  5. import {ServerResponse, IncomingMessage} from 'http';
  6. import {DataSender, ErrorSender} from './senders/index.js';
  7. import {HookRegistry, RouterHookType} from './hooks/index.js';
  8. import {createRequestMock, createResponseMock} from './utils/index.js';
  9. describe('TrieRouter', function () {
  10. describe('defineRoute', function () {
  11. it('returns the Route instance', function () {
  12. const router = new TrieRouter();
  13. const path = '/path';
  14. const handler = () => 'ok';
  15. const res = router.defineRoute({method: HttpMethod.GET, path, handler});
  16. expect(res).to.be.instanceof(Route);
  17. expect(res.method).to.be.eq(HttpMethod.GET);
  18. expect(res.path).to.be.eq(path);
  19. expect(res.handler).to.be.eq(handler);
  20. });
  21. });
  22. describe('requestListener', function () {
  23. it('should be a function', function () {
  24. const router = new TrieRouter();
  25. expect(typeof router.requestListener).to.be.eq('function');
  26. });
  27. it('provides the request context to the route handler', function (done) {
  28. const router = new TrieRouter();
  29. router.defineRoute({
  30. method: HttpMethod.GET,
  31. path: '/test',
  32. handler: ctx => {
  33. expect(ctx).to.be.instanceof(RequestContext);
  34. done();
  35. },
  36. });
  37. const req = createRequestMock({path: '/test'});
  38. const res = createResponseMock();
  39. router.requestListener(req, res);
  40. });
  41. it('provides path parameters to the request context', function (done) {
  42. const router = new TrieRouter();
  43. router.defineRoute({
  44. method: HttpMethod.GET,
  45. path: '/:p1-:p2',
  46. handler: ({params}) => {
  47. expect(params).to.be.eql({p1: 'foo', p2: 'bar'});
  48. done();
  49. },
  50. });
  51. const req = createRequestMock({path: '/foo-bar'});
  52. const res = createResponseMock();
  53. router.requestListener(req, res);
  54. });
  55. it('provides query parameters to the request context', function (done) {
  56. const router = new TrieRouter();
  57. router.defineRoute({
  58. method: HttpMethod.GET,
  59. path: '/',
  60. handler: ({query}) => {
  61. expect(query).to.be.eql({p1: 'foo', p2: 'bar'});
  62. done();
  63. },
  64. });
  65. const req = createRequestMock({path: '?p1=foo&p2=bar'});
  66. const res = createResponseMock();
  67. router.requestListener(req, res);
  68. });
  69. it('provides parsed cookies to the request context', function (done) {
  70. const router = new TrieRouter();
  71. router.defineRoute({
  72. method: HttpMethod.GET,
  73. path: '/',
  74. handler: ({cookies}) => {
  75. expect(cookies).to.be.eql({p1: 'foo', p2: 'bar'});
  76. done();
  77. },
  78. });
  79. const req = createRequestMock({headers: {cookie: 'p1=foo; p2=bar;'}});
  80. const res = createResponseMock();
  81. router.requestListener(req, res);
  82. });
  83. it('provides the plain text body to the request context', function (done) {
  84. const router = new TrieRouter();
  85. const body = 'Lorem Ipsum is simply dummy text.';
  86. router.defineRoute({
  87. method: HttpMethod.POST,
  88. path: '/',
  89. handler: ctx => {
  90. expect(ctx.body).to.be.eq(body);
  91. done();
  92. },
  93. });
  94. const req = createRequestMock({method: HttpMethod.POST, body});
  95. const res = createResponseMock();
  96. router.requestListener(req, res);
  97. });
  98. it('provides the parsed JSON body to the request context', function (done) {
  99. const router = new TrieRouter();
  100. const data = {p1: 'foo', p2: 'bar'};
  101. router.defineRoute({
  102. method: HttpMethod.POST,
  103. path: '/',
  104. handler: ({body}) => {
  105. expect(body).to.be.eql(data);
  106. done();
  107. },
  108. });
  109. const req = createRequestMock({method: HttpMethod.POST, body: data});
  110. const res = createResponseMock();
  111. router.requestListener(req, res);
  112. });
  113. it('provides request headers to the request context', function (done) {
  114. const router = new TrieRouter();
  115. router.defineRoute({
  116. method: HttpMethod.GET,
  117. path: '/',
  118. handler: ({headers}) => {
  119. expect(headers).to.be.eql({
  120. host: 'localhost',
  121. foo: 'bar',
  122. });
  123. done();
  124. },
  125. });
  126. const req = createRequestMock({headers: {foo: 'bar'}});
  127. const res = createResponseMock();
  128. router.requestListener(req, res);
  129. });
  130. it('provides the route to the request context', function (done) {
  131. const router = new TrieRouter();
  132. const metaData = {foo: {bar: {baz: 'qux'}}};
  133. const currentRoute = router.defineRoute({
  134. method: HttpMethod.GET,
  135. path: '/',
  136. meta: metaData,
  137. handler: ({route}) => {
  138. expect(route).to.be.eq(currentRoute);
  139. done();
  140. },
  141. });
  142. const req = createRequestMock();
  143. const res = createResponseMock();
  144. router.requestListener(req, res);
  145. });
  146. it('provides access to route meta via the request context', function (done) {
  147. const router = new TrieRouter();
  148. const metaData = {role: 'admin'};
  149. router.defineRoute({
  150. method: HttpMethod.GET,
  151. path: '/',
  152. meta: metaData,
  153. handler: ({meta}) => {
  154. expect(meta).to.eql(metaData);
  155. done();
  156. },
  157. });
  158. const req = createRequestMock();
  159. const res = createResponseMock();
  160. router.requestListener(req, res);
  161. });
  162. it('uses the DataSender to send the server response', function (done) {
  163. const router = new TrieRouter();
  164. const resBody = 'Lorem Ipsum is simply dummy text.';
  165. router.defineRoute({
  166. method: HttpMethod.GET,
  167. path: '/',
  168. handler: () => resBody,
  169. });
  170. const req = createRequestMock();
  171. const res = createResponseMock();
  172. router.setService(DataSender, {
  173. send(response, data) {
  174. expect(response).to.be.eq(res);
  175. expect(data).to.be.eq(resBody);
  176. done();
  177. },
  178. });
  179. router.requestListener(req, res);
  180. });
  181. it('uses the ErrorSender to send the server response', function (done) {
  182. const router = new TrieRouter();
  183. const error = new Error();
  184. router.defineRoute({
  185. method: HttpMethod.GET,
  186. path: '/',
  187. handler: () => {
  188. throw error;
  189. },
  190. });
  191. const req = createRequestMock();
  192. const res = createResponseMock();
  193. router.setService(ErrorSender, {
  194. send(request, response, err) {
  195. expect(request).to.be.eq(req);
  196. expect(response).to.be.eq(res);
  197. expect(err).to.be.eq(error);
  198. done();
  199. },
  200. });
  201. router.requestListener(req, res);
  202. });
  203. describe('hooks', function () {
  204. it('invokes entire "preHandler" hooks before the route handler', async function () {
  205. const router = new TrieRouter();
  206. const order = [];
  207. const body = 'OK';
  208. router.defineRoute({
  209. method: HttpMethod.GET,
  210. path: '/',
  211. preHandler: [
  212. () => {
  213. order.push('preHandler1');
  214. },
  215. () => {
  216. order.push('preHandler2');
  217. },
  218. ],
  219. handler: () => {
  220. order.push('handler');
  221. return body;
  222. },
  223. });
  224. const req = createRequestMock();
  225. const res = createResponseMock();
  226. router.requestListener(req, res);
  227. const result = await res.getBody();
  228. expect(result).to.be.eq(body);
  229. expect(order).to.be.eql(['preHandler1', 'preHandler2', 'handler']);
  230. });
  231. it('invokes entire "preHandler" hooks after the route handler', async function () {
  232. const router = new TrieRouter();
  233. const order = [];
  234. const body = 'OK';
  235. router.defineRoute({
  236. method: HttpMethod.GET,
  237. path: '/',
  238. handler: () => {
  239. order.push('handler');
  240. return body;
  241. },
  242. postHandler: [
  243. () => {
  244. order.push('postHandler1');
  245. },
  246. () => {
  247. order.push('postHandler2');
  248. },
  249. ],
  250. });
  251. const req = createRequestMock();
  252. const res = createResponseMock();
  253. router.requestListener(req, res);
  254. const result = await res.getBody();
  255. expect(result).to.be.eq(body);
  256. expect(order).to.be.eql(['handler', 'postHandler1', 'postHandler2']);
  257. });
  258. it('provides the request context to the "preHandler" hooks', async function () {
  259. const router = new TrieRouter();
  260. const order = [];
  261. const body = 'OK';
  262. router.defineRoute({
  263. method: HttpMethod.GET,
  264. path: '/',
  265. preHandler: [
  266. ctx => {
  267. order.push('preHandler1');
  268. expect(ctx).to.be.instanceof(RequestContext);
  269. },
  270. ctx => {
  271. order.push('preHandler2');
  272. expect(ctx).to.be.instanceof(RequestContext);
  273. },
  274. ],
  275. handler: ctx => {
  276. order.push('handler');
  277. expect(ctx).to.be.instanceof(RequestContext);
  278. return body;
  279. },
  280. });
  281. const req = createRequestMock();
  282. const res = createResponseMock();
  283. router.requestListener(req, res);
  284. const result = await res.getBody();
  285. expect(result).to.be.eq(body);
  286. expect(order).to.be.eql(['preHandler1', 'preHandler2', 'handler']);
  287. });
  288. it('provides the request context and return value from the route handler to the "postHandler" hooks', async function () {
  289. const router = new TrieRouter();
  290. const order = [];
  291. const body = 'OK';
  292. let requestContext;
  293. router.defineRoute({
  294. method: HttpMethod.GET,
  295. path: '/',
  296. handler: ctx => {
  297. order.push('handler');
  298. expect(ctx).to.be.instanceof(RequestContext);
  299. requestContext = ctx;
  300. return body;
  301. },
  302. postHandler: [
  303. (ctx, data) => {
  304. order.push('postHandler1');
  305. expect(ctx).to.be.eq(requestContext);
  306. expect(data).to.be.eq(body);
  307. },
  308. (ctx, data) => {
  309. order.push('postHandler2');
  310. expect(ctx).to.be.eq(requestContext);
  311. expect(data).to.be.eq(body);
  312. },
  313. ],
  314. });
  315. const req = createRequestMock();
  316. const res = createResponseMock();
  317. router.requestListener(req, res);
  318. const result = await res.getBody();
  319. expect(result).to.be.eq(body);
  320. expect(order).to.be.eql(['handler', 'postHandler1', 'postHandler2']);
  321. });
  322. it('invokes the route handler if entire "preHandler" hooks returns undefined or null', async function () {
  323. const router = new TrieRouter();
  324. const order = [];
  325. const body = 'OK';
  326. router.defineRoute({
  327. method: HttpMethod.GET,
  328. path: '/',
  329. preHandler: [
  330. () => {
  331. order.push('preHandler1');
  332. return undefined;
  333. },
  334. () => {
  335. order.push('preHandler2');
  336. return null;
  337. },
  338. ],
  339. handler: () => {
  340. order.push('handler');
  341. return body;
  342. },
  343. });
  344. const req = createRequestMock();
  345. const res = createResponseMock();
  346. router.requestListener(req, res);
  347. const result = await res.getBody();
  348. expect(result).to.be.eq(body);
  349. expect(order).to.be.eql(['preHandler1', 'preHandler2', 'handler']);
  350. });
  351. it('sends a returns value from the route handler if entire "postHandler" hooks returns undefined or null', async function () {
  352. const router = new TrieRouter();
  353. const order = [];
  354. const body = 'OK';
  355. router.defineRoute({
  356. method: HttpMethod.GET,
  357. path: '/',
  358. handler: () => {
  359. order.push('handler');
  360. return body;
  361. },
  362. postHandler: [
  363. () => {
  364. order.push('postHandler1');
  365. return undefined;
  366. },
  367. () => {
  368. order.push('postHandler2');
  369. return null;
  370. },
  371. ],
  372. });
  373. const req = createRequestMock();
  374. const res = createResponseMock();
  375. router.requestListener(req, res);
  376. const result = await res.getBody();
  377. expect(result).to.be.eq(body);
  378. expect(order).to.be.eql(['handler', 'postHandler1', 'postHandler2']);
  379. });
  380. it('sends a return value from the "preHandler" hook in the first priority', async function () {
  381. const router = new TrieRouter();
  382. const order = [];
  383. const preHandlerBody = 'foo';
  384. const handlerBody = 'bar';
  385. const postHandlerBody = 'baz';
  386. router.defineRoute({
  387. method: HttpMethod.GET,
  388. path: '/',
  389. preHandler() {
  390. order.push('preHandler');
  391. return preHandlerBody;
  392. },
  393. handler: () => {
  394. order.push('handler');
  395. return handlerBody;
  396. },
  397. postHandler() {
  398. order.push('postHandler');
  399. return postHandlerBody;
  400. },
  401. });
  402. const req = createRequestMock();
  403. const res = createResponseMock();
  404. router.requestListener(req, res);
  405. const result = await res.getBody();
  406. expect(result).to.be.eq(preHandlerBody);
  407. expect(result).not.to.be.eq(handlerBody);
  408. expect(result).not.to.be.eq(postHandlerBody);
  409. expect(order).to.be.eql(['preHandler']);
  410. });
  411. it('sends a return value from the "postHandler" hook in the second priority', async function () {
  412. const router = new TrieRouter();
  413. const order = [];
  414. const handlerBody = 'foo';
  415. const postHandlerBody = 'bar';
  416. router.defineRoute({
  417. method: HttpMethod.GET,
  418. path: '/',
  419. preHandler() {
  420. order.push('preHandler');
  421. },
  422. handler: () => {
  423. order.push('handler');
  424. return handlerBody;
  425. },
  426. postHandler() {
  427. order.push('postHandler');
  428. return postHandlerBody;
  429. },
  430. });
  431. const req = createRequestMock();
  432. const res = createResponseMock();
  433. router.requestListener(req, res);
  434. const result = await res.getBody();
  435. expect(result).not.to.be.eq(handlerBody);
  436. expect(result).to.be.eq(postHandlerBody);
  437. expect(order).to.be.eql(['preHandler', 'handler', 'postHandler']);
  438. });
  439. it('sends a return value from the root handler in the third priority', async function () {
  440. const router = new TrieRouter();
  441. const order = [];
  442. const body = 'OK';
  443. router.defineRoute({
  444. method: HttpMethod.GET,
  445. path: '/',
  446. preHandler() {
  447. order.push('preHandler');
  448. },
  449. handler: () => {
  450. order.push('handler');
  451. return body;
  452. },
  453. postHandler() {
  454. order.push('postHandler');
  455. },
  456. });
  457. const req = createRequestMock();
  458. const res = createResponseMock();
  459. router.requestListener(req, res);
  460. const result = await res.getBody();
  461. expect(result).to.be.eq(body);
  462. expect(order).to.be.eql(['preHandler', 'handler', 'postHandler']);
  463. });
  464. });
  465. });
  466. describe('_handleRequest', function () {
  467. it('should register the request context in the request-scope ServiceContainer', function (done) {
  468. const router = new TrieRouter();
  469. router.defineRoute({
  470. method: HttpMethod.GET,
  471. path: '/',
  472. handler(ctx) {
  473. const res = ctx.container.getRegistered(RequestContext);
  474. expect(res).to.be.eq(ctx);
  475. expect(res).to.be.not.eq(router.container);
  476. done();
  477. },
  478. });
  479. const req = createRequestMock();
  480. const res = createResponseMock();
  481. router.requestListener(req, res);
  482. });
  483. it('should register the IncomingMessage in the request-scope ServiceContainer', function (done) {
  484. const router = new TrieRouter();
  485. const req = createRequestMock();
  486. const res = createResponseMock();
  487. router.defineRoute({
  488. method: HttpMethod.GET,
  489. path: '/',
  490. handler(ctx) {
  491. const result = ctx.container.getRegistered(IncomingMessage);
  492. expect(result).to.be.eq(req);
  493. done();
  494. },
  495. });
  496. router.requestListener(req, res);
  497. });
  498. it('should register the ServerResponse in the request-scope ServiceContainer', function (done) {
  499. const router = new TrieRouter();
  500. const req = createRequestMock();
  501. const res = createResponseMock();
  502. router.defineRoute({
  503. method: HttpMethod.GET,
  504. path: '/',
  505. handler(ctx) {
  506. const result = ctx.container.getRegistered(ServerResponse);
  507. expect(result).to.be.eq(res);
  508. done();
  509. },
  510. });
  511. router.requestListener(req, res);
  512. });
  513. it('should send parsing error response instead of throwing error', async function () {
  514. const router = new TrieRouter();
  515. router.defineRoute({
  516. method: HttpMethod.POST,
  517. path: '/',
  518. handler() {},
  519. });
  520. const req = createRequestMock({
  521. method: HttpMethod.POST,
  522. headers: {'content-type': 'application/json'},
  523. body: 'invalid',
  524. });
  525. const res = createResponseMock();
  526. router.requestListener(req, res);
  527. const body = await res.getBody();
  528. expect(res.statusCode).to.be.eq(400);
  529. expect(JSON.parse(body)).to.be.eql({
  530. error: {
  531. message: `Unexpected token 'i', "invalid" is not valid JSON`,
  532. },
  533. });
  534. });
  535. it('should not invoke the main handler if a preHandler sends the response asynchronously', async function () {
  536. let handlerCalled = false;
  537. const router = new TrieRouter();
  538. router.defineRoute({
  539. method: 'GET',
  540. path: '/',
  541. preHandler(ctx) {
  542. return new Promise(resolve => {
  543. setTimeout(() => {
  544. ctx.response.setHeader('Content-Type', 'text/plain');
  545. ctx.response.end('Response from preHandler');
  546. resolve(undefined);
  547. }, 10);
  548. });
  549. },
  550. handler() {
  551. handlerCalled = true;
  552. return 'Response from main handler';
  553. },
  554. });
  555. const req = createRequestMock({method: 'GET', path: '/'});
  556. const res = createResponseMock();
  557. await router._handleRequest(req, res);
  558. const responseBody = await res.getBody();
  559. expect(responseBody).to.equal('Response from preHandler');
  560. expect(handlerCalled).to.be.false;
  561. });
  562. });
  563. describe('addHook', function () {
  564. it('adds the given hook to the HookRegistry and returns itself', function () {
  565. const router = new TrieRouter();
  566. const reg = router.getService(HookRegistry);
  567. const type = RouterHookType.PRE_HANDLER;
  568. const hook = () => undefined;
  569. expect(reg.hasHook(type, hook)).to.be.false;
  570. const res = router.addHook(type, hook);
  571. expect(res).to.be.eq(router);
  572. expect(reg.hasHook(type, hook)).to.be.true;
  573. });
  574. });
  575. describe('addPreHandler', function () {
  576. it('adds the given pre-handler hook to the HookRegistry and returns itself', function () {
  577. const router = new TrieRouter();
  578. const reg = router.getService(HookRegistry);
  579. const hook = () => undefined;
  580. expect(reg.hasHook(RouterHookType.PRE_HANDLER, hook)).to.be.false;
  581. const res = router.addPreHandler(hook);
  582. expect(res).to.be.eq(router);
  583. expect(reg.hasHook(RouterHookType.PRE_HANDLER, hook)).to.be.true;
  584. });
  585. });
  586. describe('addPostHandler', function () {
  587. it('adds the given post-handler hook to the HookRegistry and returns itself', function () {
  588. const router = new TrieRouter();
  589. const reg = router.getService(HookRegistry);
  590. const hook = () => undefined;
  591. expect(reg.hasHook(RouterHookType.POST_HANDLER, hook)).to.be.false;
  592. const res = router.addPostHandler(hook);
  593. expect(res).to.be.eq(router);
  594. expect(reg.hasHook(RouterHookType.POST_HANDLER, hook)).to.be.true;
  595. });
  596. });
  597. });