hook-invoker.spec.js 14 KB

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