trie-router.spec.js 20 KB

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