trie-router.spec.js 20 KB

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