hooks.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. 'use strict';
  2. const noop = require('lodash/noop');
  3. const chai = require('chai');
  4. const sinon = require('sinon');
  5. const expect = chai.expect;
  6. const Support = require('../support');
  7. const { DataTypes, Sequelize } = require('@sequelize/core');
  8. const sequelize = Support.sequelize;
  9. describe(Support.getTestDialectTeaser('Hooks'), () => {
  10. beforeEach(function () {
  11. this.Model = sequelize.define('m');
  12. });
  13. it('does not expose non-model hooks', function () {
  14. for (const badHook of [
  15. 'beforeDefine',
  16. 'afterDefine',
  17. 'beforeConnect',
  18. 'afterConnect',
  19. 'beforePoolAcquire',
  20. 'afterPoolAcquire',
  21. 'beforeDisconnect',
  22. 'afterDisconnect',
  23. 'beforeInit',
  24. 'afterInit',
  25. ]) {
  26. expect(this.Model).to.not.have.property(badHook);
  27. }
  28. });
  29. describe('arguments', () => {
  30. it('hooks can modify passed arguments', async function () {
  31. this.Model.addHook('beforeCreate', options => {
  32. options.answer = 41;
  33. });
  34. const options = {};
  35. await this.Model.runHooks('beforeCreate', options);
  36. expect(options.answer).to.equal(41);
  37. });
  38. });
  39. describe('proxies', () => {
  40. beforeEach(() => {
  41. sinon.stub(sequelize, 'queryRaw').resolves([
  42. {
  43. _previousDataValues: {},
  44. dataValues: { id: 1, name: 'abc' },
  45. },
  46. ]);
  47. });
  48. afterEach(() => {
  49. sequelize.queryRaw.restore();
  50. });
  51. describe('defined by options.hooks', () => {
  52. beforeEach(function () {
  53. this.beforeSaveHook = sinon.spy();
  54. this.afterSaveHook = sinon.spy();
  55. this.afterCreateHook = sinon.spy();
  56. this.Model = sequelize.define(
  57. 'm',
  58. {
  59. name: DataTypes.STRING,
  60. },
  61. {
  62. hooks: {
  63. beforeSave: this.beforeSaveHook,
  64. afterSave: this.afterSaveHook,
  65. afterCreate: this.afterCreateHook,
  66. },
  67. },
  68. );
  69. });
  70. it('calls beforeSave/afterSave', async function () {
  71. await this.Model.create({});
  72. expect(this.afterCreateHook).to.have.been.calledOnce;
  73. expect(this.beforeSaveHook).to.have.been.calledOnce;
  74. expect(this.afterSaveHook).to.have.been.calledOnce;
  75. });
  76. });
  77. describe('defined by addHook method', () => {
  78. beforeEach(function () {
  79. this.beforeSaveHook = sinon.spy();
  80. this.afterSaveHook = sinon.spy();
  81. this.Model = sequelize.define('m', {
  82. name: DataTypes.STRING,
  83. });
  84. this.Model.addHook('beforeSave', this.beforeSaveHook);
  85. this.Model.addHook('afterSave', this.afterSaveHook);
  86. });
  87. it('calls beforeSave/afterSave', async function () {
  88. await this.Model.create({});
  89. expect(this.beforeSaveHook).to.have.been.calledOnce;
  90. expect(this.afterSaveHook).to.have.been.calledOnce;
  91. });
  92. });
  93. describe('defined by hook method', () => {
  94. beforeEach(function () {
  95. this.beforeSaveHook = sinon.spy();
  96. this.afterSaveHook = sinon.spy();
  97. this.Model = sequelize.define('m', {
  98. name: DataTypes.STRING,
  99. });
  100. this.Model.addHook('beforeSave', this.beforeSaveHook);
  101. this.Model.addHook('afterSave', this.afterSaveHook);
  102. });
  103. it('calls beforeSave/afterSave', async function () {
  104. await this.Model.create({});
  105. expect(this.beforeSaveHook).to.have.been.calledOnce;
  106. expect(this.afterSaveHook).to.have.been.calledOnce;
  107. });
  108. });
  109. });
  110. describe('multiple hooks', () => {
  111. beforeEach(function () {
  112. this.hook1 = sinon.spy();
  113. this.hook2 = sinon.spy();
  114. this.hook3 = sinon.spy();
  115. });
  116. describe('runs all hooks on success', () => {
  117. afterEach(function () {
  118. expect(this.hook1).to.have.been.calledOnce;
  119. expect(this.hook2).to.have.been.calledOnce;
  120. expect(this.hook3).to.have.been.calledOnce;
  121. });
  122. it('using addHook', async function () {
  123. this.Model.addHook('beforeCreate', this.hook1);
  124. this.Model.addHook('beforeCreate', this.hook2);
  125. this.Model.addHook('beforeCreate', this.hook3);
  126. await this.Model.runHooks('beforeCreate');
  127. });
  128. it('using function', async function () {
  129. this.Model.beforeCreate(this.hook1);
  130. this.Model.beforeCreate(this.hook2);
  131. this.Model.beforeCreate(this.hook3);
  132. await this.Model.runHooks('beforeCreate');
  133. });
  134. it('using define', async function () {
  135. await sequelize
  136. .define(
  137. 'M',
  138. {},
  139. {
  140. hooks: {
  141. beforeCreate: [this.hook1, this.hook2, this.hook3],
  142. },
  143. },
  144. )
  145. .runHooks('beforeCreate');
  146. });
  147. it('using a mixture', async function () {
  148. const Model = sequelize.define(
  149. 'M',
  150. {},
  151. {
  152. hooks: {
  153. beforeCreate: this.hook1,
  154. },
  155. },
  156. );
  157. Model.beforeCreate(this.hook2);
  158. Model.addHook('beforeCreate', this.hook3);
  159. await Model.runHooks('beforeCreate');
  160. });
  161. });
  162. it('stops execution when a hook throws', async function () {
  163. this.Model.beforeCreate(() => {
  164. this.hook1();
  165. throw new Error('No!');
  166. });
  167. this.Model.beforeCreate(this.hook2);
  168. await expect(this.Model.runHooks('beforeCreate')).to.be.rejected;
  169. expect(this.hook1).to.have.been.calledOnce;
  170. expect(this.hook2).not.to.have.been.called;
  171. });
  172. it('stops execution when a hook rejects', async function () {
  173. this.Model.beforeCreate(async () => {
  174. this.hook1();
  175. throw new Error('No!');
  176. });
  177. this.Model.beforeCreate(this.hook2);
  178. await expect(this.Model.runHooks('beforeCreate')).to.be.rejected;
  179. expect(this.hook1).to.have.been.calledOnce;
  180. expect(this.hook2).not.to.have.been.called;
  181. });
  182. });
  183. describe('global hooks', () => {
  184. describe('using addHook', () => {
  185. it('invokes the global hook', async function () {
  186. const globalHook = sinon.spy();
  187. sequelize.addHook('beforeUpdate', globalHook);
  188. await this.Model.runHooks('beforeUpdate');
  189. expect(globalHook).to.have.been.calledOnce;
  190. });
  191. it('invokes the global hook, when the model also has a hook', async () => {
  192. const globalHookBefore = sinon.spy();
  193. const globalHookAfter = sinon.spy();
  194. const localHook = sinon.spy();
  195. sequelize.addHook('beforeUpdate', globalHookBefore);
  196. const Model = sequelize.define(
  197. 'm',
  198. {},
  199. {
  200. hooks: {
  201. beforeUpdate: localHook,
  202. },
  203. },
  204. );
  205. sequelize.addHook('beforeUpdate', globalHookAfter);
  206. await Model.runHooks('beforeUpdate');
  207. expect(globalHookBefore).to.have.been.calledOnce;
  208. expect(globalHookAfter).to.have.been.calledOnce;
  209. expect(localHook).to.have.been.calledOnce;
  210. expect(localHook).to.have.been.calledBefore(globalHookBefore);
  211. expect(localHook).to.have.been.calledBefore(globalHookAfter);
  212. });
  213. });
  214. it('registers both the global define hook, and the local hook', async () => {
  215. const globalHook = sinon.spy();
  216. const sequelize = Support.createSequelizeInstance({
  217. define: {
  218. hooks: {
  219. beforeCreate: globalHook,
  220. },
  221. },
  222. });
  223. const localHook = sinon.spy();
  224. const Model = sequelize.define(
  225. 'M',
  226. {},
  227. {
  228. hooks: {
  229. beforeUpdate: noop, // Just to make sure we can define other hooks without overwriting the global one
  230. beforeCreate: localHook,
  231. },
  232. },
  233. );
  234. await Model.runHooks('beforeCreate');
  235. expect(globalHook).to.have.been.calledOnce;
  236. expect(localHook).to.have.been.calledOnce;
  237. });
  238. });
  239. describe('#removeHook', () => {
  240. it('should remove hook', async function () {
  241. const hook1 = sinon.spy();
  242. const hook2 = sinon.spy();
  243. this.Model.addHook('beforeCreate', 'myHook', hook1);
  244. this.Model.beforeCreate('myHook2', hook2);
  245. await this.Model.runHooks('beforeCreate');
  246. expect(hook1).to.have.been.calledOnce;
  247. expect(hook2).to.have.been.calledOnce;
  248. hook1.resetHistory();
  249. hook2.resetHistory();
  250. this.Model.removeHook('beforeCreate', 'myHook');
  251. this.Model.removeHook('beforeCreate', 'myHook2');
  252. await this.Model.runHooks('beforeCreate');
  253. expect(hook1).not.to.have.been.called;
  254. expect(hook2).not.to.have.been.called;
  255. });
  256. it('should not remove other hooks', async function () {
  257. const hook1 = sinon.spy();
  258. const hook2 = sinon.spy();
  259. const hook3 = sinon.spy();
  260. const hook4 = sinon.spy();
  261. this.Model.addHook('beforeCreate', hook1);
  262. this.Model.addHook('beforeCreate', 'myHook', hook2);
  263. this.Model.beforeCreate('myHook2', hook3);
  264. this.Model.beforeCreate(hook4);
  265. await this.Model.runHooks('beforeCreate');
  266. expect(hook1).to.have.been.calledOnce;
  267. expect(hook2).to.have.been.calledOnce;
  268. expect(hook3).to.have.been.calledOnce;
  269. expect(hook4).to.have.been.calledOnce;
  270. hook1.resetHistory();
  271. hook2.resetHistory();
  272. hook3.resetHistory();
  273. hook4.resetHistory();
  274. this.Model.removeHook('beforeCreate', 'myHook');
  275. await this.Model.runHooks('beforeCreate');
  276. expect(hook1).to.have.been.calledOnce;
  277. expect(hook2).not.to.have.been.called;
  278. expect(hook3).to.have.been.calledOnce;
  279. expect(hook4).to.have.been.calledOnce;
  280. });
  281. });
  282. describe('#addHook', () => {
  283. it('should add additional hook when previous exists', async function () {
  284. const hook1 = sinon.spy();
  285. const hook2 = sinon.spy();
  286. const Model = this.sequelize.define(
  287. 'Model',
  288. {},
  289. {
  290. hooks: { beforeCreate: hook1 },
  291. },
  292. );
  293. Model.addHook('beforeCreate', hook2);
  294. await Model.runHooks('beforeCreate');
  295. expect(hook1).to.have.been.calledOnce;
  296. expect(hook2).to.have.been.calledOnce;
  297. });
  298. });
  299. describe('promises', () => {
  300. it('can return a promise', async function () {
  301. this.Model.beforeBulkCreate(async () => {
  302. // This space intentionally left blank
  303. });
  304. await expect(this.Model.runHooks('beforeBulkCreate')).to.be.fulfilled;
  305. });
  306. it('can return undefined', async function () {
  307. this.Model.beforeBulkCreate(() => {
  308. // This space intentionally left blank
  309. });
  310. await expect(this.Model.runHooks('beforeBulkCreate')).to.be.fulfilled;
  311. });
  312. it('can return an error by rejecting', async function () {
  313. this.Model.beforeCreate(async () => {
  314. throw new Error('Forbidden');
  315. });
  316. await expect(this.Model.runHooks('beforeCreate')).to.be.rejectedWith('Forbidden');
  317. });
  318. it('can return an error by throwing', async function () {
  319. this.Model.beforeCreate(() => {
  320. throw new Error('Forbidden');
  321. });
  322. await expect(this.Model.runHooks('beforeCreate')).to.be.rejectedWith('Forbidden');
  323. });
  324. });
  325. describe('sync hooks', () => {
  326. beforeEach(function () {
  327. this.hook1 = sinon.spy();
  328. this.hook2 = sinon.spy();
  329. this.hook3 = sinon.spy();
  330. this.hook4 = sinon.spy();
  331. });
  332. it('runs all beforInit/afterInit hooks', function () {
  333. Sequelize.addHook('beforeInit', 'h1', this.hook1);
  334. Sequelize.addHook('beforeInit', 'h2', this.hook2);
  335. Sequelize.addHook('afterInit', 'h3', this.hook3);
  336. Sequelize.addHook('afterInit', 'h4', this.hook4);
  337. Support.createSequelizeInstance();
  338. expect(this.hook1).to.have.been.calledOnce;
  339. expect(this.hook2).to.have.been.calledOnce;
  340. expect(this.hook3).to.have.been.calledOnce;
  341. expect(this.hook4).to.have.been.calledOnce;
  342. // cleanup hooks on Sequelize
  343. Sequelize.removeHook('beforeInit', 'h1');
  344. Sequelize.removeHook('beforeInit', 'h2');
  345. Sequelize.removeHook('afterInit', 'h3');
  346. Sequelize.removeHook('afterInit', 'h4');
  347. Support.createSequelizeInstance();
  348. // check if hooks were removed
  349. expect(this.hook1).to.have.been.calledOnce;
  350. expect(this.hook2).to.have.been.calledOnce;
  351. expect(this.hook3).to.have.been.calledOnce;
  352. expect(this.hook4).to.have.been.calledOnce;
  353. });
  354. it('runs all beforDefine/afterDefine hooks', function () {
  355. const sequelize = Support.createSequelizeInstance();
  356. sequelize.addHook('beforeDefine', this.hook1);
  357. sequelize.addHook('beforeDefine', this.hook2);
  358. sequelize.addHook('afterDefine', this.hook3);
  359. sequelize.addHook('afterDefine', this.hook4);
  360. sequelize.define('Test', {});
  361. expect(this.hook1).to.have.been.calledOnce;
  362. expect(this.hook2).to.have.been.calledOnce;
  363. expect(this.hook3).to.have.been.calledOnce;
  364. expect(this.hook4).to.have.been.calledOnce;
  365. });
  366. });
  367. describe('#removal', () => {
  368. before(() => {
  369. sinon.stub(sequelize, 'queryRaw').resolves([
  370. {
  371. _previousDataValues: {},
  372. dataValues: { id: 1, name: 'abc' },
  373. },
  374. ]);
  375. });
  376. after(() => {
  377. sequelize.queryRaw.restore();
  378. });
  379. it('should be able to remove by name', async () => {
  380. const User = sequelize.define('User');
  381. const hook1 = sinon.spy();
  382. const hook2 = sinon.spy();
  383. User.addHook('beforeCreate', 'sasuke', hook1);
  384. User.addHook('beforeCreate', 'naruto', hook2);
  385. await User.create({ username: 'makunouchi' });
  386. expect(hook1).to.have.been.calledOnce;
  387. expect(hook2).to.have.been.calledOnce;
  388. User.removeHook('beforeCreate', 'sasuke');
  389. await User.create({ username: 'sendo' });
  390. expect(hook1).to.have.been.calledOnce;
  391. expect(hook2).to.have.been.calledTwice;
  392. });
  393. it('should be able to remove by reference', async () => {
  394. const User = sequelize.define('User');
  395. const hook1 = sinon.spy();
  396. const hook2 = sinon.spy();
  397. User.addHook('beforeCreate', hook1);
  398. User.addHook('beforeCreate', hook2);
  399. await User.create({ username: 'makunouchi' });
  400. expect(hook1).to.have.been.calledOnce;
  401. expect(hook2).to.have.been.calledOnce;
  402. User.removeHook('beforeCreate', hook1);
  403. await User.create({ username: 'sendo' });
  404. expect(hook1).to.have.been.calledOnce;
  405. expect(hook2).to.have.been.calledTwice;
  406. });
  407. });
  408. });