hook-invoker.spec.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import {expect} from 'chai';
  2. import {format} from '@e22m4u/js-format';
  3. import {ROOT_PATH} from '../constants.js';
  4. import {HookInvoker} from './hook-invoker.js';
  5. import {Route, HttpMethod} from '../route/index.js';
  6. import {createResponseMock} from '../utils/index.js';
  7. import {HookRegistry, RouterHookType} from './hook-registry.js';
  8. describe('HookInvoker', function () {
  9. describe('invokeAndContinueUntilValueReceived', function () {
  10. it('requires the parameter "route" to be a Route instance', function () {
  11. const s = new HookInvoker();
  12. const res = createResponseMock();
  13. const throwable = v => () =>
  14. s.invokeAndContinueUntilValueReceived(
  15. v,
  16. RouterHookType.PRE_HANDLER,
  17. res,
  18. );
  19. const error = v =>
  20. format(
  21. 'The parameter "route" of ' +
  22. 'the HookInvoker.invokeAndContinueUntilValueReceived ' +
  23. 'must be a Route instance, but %s was given.',
  24. v,
  25. );
  26. expect(throwable('str')).to.throw(error('"str"'));
  27. expect(throwable('')).to.throw(error('""'));
  28. expect(throwable(10)).to.throw(error('10'));
  29. expect(throwable(0)).to.throw(error('0'));
  30. expect(throwable(true)).to.throw(error('true'));
  31. expect(throwable(false)).to.throw(error('false'));
  32. expect(throwable(null)).to.throw(error('null'));
  33. expect(throwable({})).to.throw(error('Object'));
  34. expect(throwable([])).to.throw(error('Array'));
  35. expect(throwable(undefined)).to.throw(error('undefined'));
  36. throwable(
  37. new Route({
  38. method: HttpMethod.GET,
  39. path: ROOT_PATH,
  40. handler: () => undefined,
  41. }),
  42. )();
  43. });
  44. it('requires the parameter "hookType" to be a non-empty String', function () {
  45. const s = new HookInvoker();
  46. const route = new Route({
  47. method: HttpMethod.GET,
  48. path: ROOT_PATH,
  49. handler: () => undefined,
  50. });
  51. const res = createResponseMock();
  52. const throwable = v => () =>
  53. s.invokeAndContinueUntilValueReceived(route, v, res);
  54. const error = v =>
  55. format(
  56. 'The parameter "hookType" of ' +
  57. 'the HookInvoker.invokeAndContinueUntilValueReceived ' +
  58. 'must be a non-empty String, but %s was given.',
  59. v,
  60. );
  61. expect(throwable('')).to.throw(error('""'));
  62. expect(throwable(10)).to.throw(error('10'));
  63. expect(throwable(0)).to.throw(error('0'));
  64. expect(throwable(true)).to.throw(error('true'));
  65. expect(throwable(false)).to.throw(error('false'));
  66. expect(throwable(null)).to.throw(error('null'));
  67. expect(throwable({})).to.throw(error('Object'));
  68. expect(throwable([])).to.throw(error('Array'));
  69. expect(throwable(undefined)).to.throw(error('undefined'));
  70. throwable(RouterHookType.PRE_HANDLER)();
  71. });
  72. it('requires the parameter "hookType" to be a supported hook', function () {
  73. const s = new HookInvoker();
  74. const route = new Route({
  75. method: HttpMethod.GET,
  76. path: ROOT_PATH,
  77. handler: () => undefined,
  78. });
  79. const res = createResponseMock();
  80. Object.values(RouterHookType).forEach(type =>
  81. s.invokeAndContinueUntilValueReceived(route, type, res),
  82. );
  83. const throwable = () =>
  84. s.invokeAndContinueUntilValueReceived(route, 'unknown', res);
  85. expect(throwable).to.throw('The hook type "unknown" is not supported.');
  86. });
  87. it('requires the parameter "response" to be an instance of ServerResponse', function () {
  88. const s = new HookInvoker();
  89. const route = new Route({
  90. method: HttpMethod.GET,
  91. path: ROOT_PATH,
  92. handler: () => undefined,
  93. });
  94. const throwable = v => () =>
  95. s.invokeAndContinueUntilValueReceived(
  96. route,
  97. RouterHookType.PRE_HANDLER,
  98. v,
  99. );
  100. const error = v =>
  101. format(
  102. 'The parameter "response" of ' +
  103. 'the HookInvoker.invokeAndContinueUntilValueReceived ' +
  104. 'must be a ServerResponse instance, but %s was given.',
  105. v,
  106. );
  107. expect(throwable('str')).to.throw(error('"str"'));
  108. expect(throwable('')).to.throw(error('""'));
  109. expect(throwable(10)).to.throw(error('10'));
  110. expect(throwable(0)).to.throw(error('0'));
  111. expect(throwable(true)).to.throw(error('true'));
  112. expect(throwable(false)).to.throw(error('false'));
  113. expect(throwable(null)).to.throw(error('null'));
  114. expect(throwable({})).to.throw(error('Object'));
  115. expect(throwable([])).to.throw(error('Array'));
  116. expect(throwable(undefined)).to.throw(error('undefined'));
  117. throwable(createResponseMock())();
  118. });
  119. it('invokes global hooks in priority', function () {
  120. const s = new HookInvoker();
  121. const order = [];
  122. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  123. order.push('globalHook1');
  124. });
  125. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  126. order.push('globalHook2');
  127. });
  128. const route = new Route({
  129. method: HttpMethod.GET,
  130. path: ROOT_PATH,
  131. preHandler: [
  132. () => {
  133. order.push('routeHook1');
  134. },
  135. () => {
  136. order.push('routeHook2');
  137. },
  138. ],
  139. handler: () => undefined,
  140. });
  141. s.invokeAndContinueUntilValueReceived(
  142. route,
  143. RouterHookType.PRE_HANDLER,
  144. createResponseMock(),
  145. );
  146. expect(order).to.be.eql([
  147. 'globalHook1',
  148. 'globalHook2',
  149. 'routeHook1',
  150. 'routeHook2',
  151. ]);
  152. });
  153. it('stops global hooks invocation if any of them returns a value', function () {
  154. const s = new HookInvoker();
  155. const order = [];
  156. const ret = 'OK';
  157. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  158. order.push('globalHook1');
  159. });
  160. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  161. order.push('globalHook2');
  162. return ret;
  163. });
  164. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  165. order.push('globalHook3');
  166. });
  167. const route = new Route({
  168. method: HttpMethod.GET,
  169. path: ROOT_PATH,
  170. preHandler: [
  171. () => {
  172. order.push('routeHook1');
  173. },
  174. () => {
  175. order.push('routeHook2');
  176. },
  177. ],
  178. handler: () => undefined,
  179. });
  180. const result = s.invokeAndContinueUntilValueReceived(
  181. route,
  182. RouterHookType.PRE_HANDLER,
  183. createResponseMock(),
  184. );
  185. expect(result).to.be.eq(ret);
  186. expect(order).to.be.eql(['globalHook1', 'globalHook2']);
  187. });
  188. it('stops route hooks invocation if any of them returns a value', function () {
  189. const s = new HookInvoker();
  190. const order = [];
  191. const ret = 'OK';
  192. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  193. order.push('globalHook1');
  194. });
  195. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  196. order.push('globalHook2');
  197. });
  198. const route = new Route({
  199. method: HttpMethod.GET,
  200. path: ROOT_PATH,
  201. preHandler: [
  202. () => {
  203. order.push('routeHook1');
  204. },
  205. () => {
  206. order.push('routeHook2');
  207. return ret;
  208. },
  209. () => {
  210. order.push('routeHook3');
  211. },
  212. ],
  213. handler: () => undefined,
  214. });
  215. const result = s.invokeAndContinueUntilValueReceived(
  216. route,
  217. RouterHookType.PRE_HANDLER,
  218. createResponseMock(),
  219. );
  220. expect(result).to.be.eq(ret);
  221. expect(order).to.be.eql([
  222. 'globalHook1',
  223. 'globalHook2',
  224. 'routeHook1',
  225. 'routeHook2',
  226. ]);
  227. });
  228. it('returns the given response and should not call hooks if the response is already sent', function () {
  229. const s = new HookInvoker();
  230. const res = createResponseMock();
  231. res._headersSent = true;
  232. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  233. throw new Error('Should not be called');
  234. });
  235. const route = new Route({
  236. method: HttpMethod.GET,
  237. path: ROOT_PATH,
  238. preHandler: [
  239. () => {
  240. throw new Error('Should not be called');
  241. },
  242. ],
  243. handler: () => undefined,
  244. });
  245. const result = s.invokeAndContinueUntilValueReceived(
  246. route,
  247. RouterHookType.PRE_HANDLER,
  248. res,
  249. );
  250. expect(result).to.be.eq(res);
  251. });
  252. it('stops global hooks invocation and returns the given response if it is already sent', function () {
  253. const s = new HookInvoker();
  254. const order = [];
  255. const res = createResponseMock();
  256. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  257. order.push('globalHook1');
  258. });
  259. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  260. order.push('globalHook2');
  261. res._headersSent = true;
  262. });
  263. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  264. order.push('globalHook3');
  265. });
  266. const route = new Route({
  267. method: HttpMethod.GET,
  268. path: ROOT_PATH,
  269. preHandler: [
  270. () => {
  271. order.push('routeHook1');
  272. },
  273. () => {
  274. order.push('routeHook2');
  275. },
  276. ],
  277. handler: () => undefined,
  278. });
  279. const result = s.invokeAndContinueUntilValueReceived(
  280. route,
  281. RouterHookType.PRE_HANDLER,
  282. res,
  283. );
  284. expect(result).to.be.eq(res);
  285. expect(order).to.be.eql(['globalHook1', 'globalHook2']);
  286. });
  287. it('stops route hooks invocation and returns the given response if it is already sent', function () {
  288. const s = new HookInvoker();
  289. const order = [];
  290. const res = createResponseMock();
  291. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  292. order.push('globalHook1');
  293. });
  294. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  295. order.push('globalHook2');
  296. });
  297. const route = new Route({
  298. method: HttpMethod.GET,
  299. path: ROOT_PATH,
  300. preHandler: [
  301. () => {
  302. order.push('routeHook1');
  303. },
  304. () => {
  305. order.push('routeHook2');
  306. res._headersSent = true;
  307. },
  308. () => {
  309. order.push('routeHook3');
  310. },
  311. ],
  312. handler: () => undefined,
  313. });
  314. const result = s.invokeAndContinueUntilValueReceived(
  315. route,
  316. RouterHookType.PRE_HANDLER,
  317. res,
  318. );
  319. expect(result).to.be.eq(res);
  320. expect(order).to.be.eql([
  321. 'globalHook1',
  322. 'globalHook2',
  323. 'routeHook1',
  324. 'routeHook2',
  325. ]);
  326. });
  327. it('returns a Promise if any global hook is asynchronous', async function () {
  328. const s = new HookInvoker();
  329. const order = [];
  330. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  331. order.push('globalHook1');
  332. });
  333. s.getService(HookRegistry).addHook(
  334. RouterHookType.PRE_HANDLER,
  335. async () => {
  336. order.push('globalHook2');
  337. },
  338. );
  339. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  340. order.push('globalHook3');
  341. });
  342. const route = new Route({
  343. method: HttpMethod.GET,
  344. path: ROOT_PATH,
  345. preHandler: [
  346. () => {
  347. order.push('routeHook1');
  348. },
  349. () => {
  350. order.push('routeHook2');
  351. },
  352. ],
  353. handler: () => undefined,
  354. });
  355. const promise = s.invokeAndContinueUntilValueReceived(
  356. route,
  357. RouterHookType.PRE_HANDLER,
  358. createResponseMock(),
  359. );
  360. expect(promise).to.be.instanceof(Promise);
  361. await expect(promise).to.eventually.be.undefined;
  362. expect(order).to.be.eql([
  363. 'globalHook1',
  364. 'globalHook2',
  365. 'globalHook3',
  366. 'routeHook1',
  367. 'routeHook2',
  368. ]);
  369. });
  370. it('returns a Promise if entire global hooks are asynchronous', async function () {
  371. const s = new HookInvoker();
  372. const order = [];
  373. s.getService(HookRegistry).addHook(
  374. RouterHookType.PRE_HANDLER,
  375. async () => {
  376. order.push('globalHook1');
  377. },
  378. );
  379. s.getService(HookRegistry).addHook(
  380. RouterHookType.PRE_HANDLER,
  381. async () => {
  382. order.push('globalHook2');
  383. },
  384. );
  385. const route = new Route({
  386. method: HttpMethod.GET,
  387. path: ROOT_PATH,
  388. preHandler: [
  389. () => {
  390. order.push('routeHook1');
  391. },
  392. () => {
  393. order.push('routeHook2');
  394. },
  395. ],
  396. handler: () => undefined,
  397. });
  398. const promise = s.invokeAndContinueUntilValueReceived(
  399. route,
  400. RouterHookType.PRE_HANDLER,
  401. createResponseMock(),
  402. );
  403. expect(promise).to.be.instanceof(Promise);
  404. await expect(promise).to.eventually.be.undefined;
  405. expect(order).to.be.eql([
  406. 'globalHook1',
  407. 'globalHook2',
  408. 'routeHook1',
  409. 'routeHook2',
  410. ]);
  411. });
  412. it('returns a Promise if any route hook is asynchronous', async function () {
  413. const s = new HookInvoker();
  414. const order = [];
  415. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  416. order.push('globalHook1');
  417. });
  418. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  419. order.push('globalHook2');
  420. });
  421. const route = new Route({
  422. method: HttpMethod.GET,
  423. path: ROOT_PATH,
  424. preHandler: [
  425. () => {
  426. order.push('routeHook1');
  427. },
  428. async () => {
  429. order.push('routeHook2');
  430. },
  431. () => {
  432. order.push('routeHook3');
  433. },
  434. ],
  435. handler: () => undefined,
  436. });
  437. const promise = s.invokeAndContinueUntilValueReceived(
  438. route,
  439. RouterHookType.PRE_HANDLER,
  440. createResponseMock(),
  441. );
  442. expect(promise).to.be.instanceof(Promise);
  443. await expect(promise).to.eventually.be.undefined;
  444. expect(order).to.be.eql([
  445. 'globalHook1',
  446. 'globalHook2',
  447. 'routeHook1',
  448. 'routeHook2',
  449. 'routeHook3',
  450. ]);
  451. });
  452. it('returns a Promise if entire route hooks are asynchronous', async function () {
  453. const s = new HookInvoker();
  454. const order = [];
  455. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  456. order.push('globalHook1');
  457. });
  458. s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
  459. order.push('globalHook2');
  460. });
  461. const route = new Route({
  462. method: HttpMethod.GET,
  463. path: ROOT_PATH,
  464. preHandler: [
  465. async () => {
  466. order.push('routeHook1');
  467. },
  468. async () => {
  469. order.push('routeHook2');
  470. },
  471. ],
  472. handler: () => undefined,
  473. });
  474. const promise = s.invokeAndContinueUntilValueReceived(
  475. route,
  476. RouterHookType.PRE_HANDLER,
  477. createResponseMock(),
  478. );
  479. expect(promise).to.be.instanceof(Promise);
  480. await expect(promise).to.eventually.be.undefined;
  481. expect(order).to.be.eql([
  482. 'globalHook1',
  483. 'globalHook2',
  484. 'routeHook1',
  485. 'routeHook2',
  486. ]);
  487. });
  488. });
  489. });