promise.test.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. var chai = require('chai'),
  2. expect = chai.expect,
  3. moment = require('moment'),
  4. sinon = require('sinon');
  5. var delay = ms => new Promise(_ => setTimeout(_, ms));
  6. chai.use(require('chai-as-promised'));
  7. sinon.usingPromise(Promise);
  8. describe('Global Promise', function() {
  9. var retry = require('../').default;
  10. beforeEach(function() {
  11. this.count = 0;
  12. this.soRejected = new Error(Math.random().toString());
  13. this.soResolved = new Error(Math.random().toString());
  14. });
  15. it('should reject immediately if max is 1 (using options)', function() {
  16. var callback = sinon.stub();
  17. callback.resolves(this.soResolved);
  18. callback.onCall(0).rejects(this.soRejected);
  19. return expect(retry(callback, { max: 1, backoffBase: 0 }))
  20. .to.eventually.be.rejectedWith(this.soRejected)
  21. .then(function() {
  22. expect(callback.callCount).to.equal(1);
  23. });
  24. });
  25. it('should reject immediately if max is 1 (using integer)', function() {
  26. var callback = sinon.stub();
  27. callback.resolves(this.soResolved);
  28. callback.onCall(0).rejects(this.soRejected);
  29. return expect(retry(callback, 1))
  30. .to.eventually.be.rejectedWith(this.soRejected)
  31. .then(function() {
  32. expect(callback.callCount).to.equal(1);
  33. });
  34. });
  35. it('should reject after all tries if still rejected', function() {
  36. var callback = sinon.stub();
  37. callback.rejects(this.soRejected);
  38. return expect(retry(callback, { max: 3, backoffBase: 0 }))
  39. .to.eventually.be.rejectedWith(this.soRejected)
  40. .then(function() {
  41. expect(callback.firstCall.args).to.deep.equal([{ current: 1 }]);
  42. expect(callback.secondCall.args).to.deep.equal([{ current: 2 }]);
  43. expect(callback.thirdCall.args).to.deep.equal([{ current: 3 }]);
  44. expect(callback.callCount).to.equal(3);
  45. });
  46. });
  47. it('should resolve immediately if resolved on first try', function() {
  48. var callback = sinon.stub();
  49. callback.resolves(this.soResolved);
  50. callback.onCall(0).resolves(this.soResolved);
  51. return expect(retry(callback, { max: 10, backoffBase: 0 }))
  52. .to.eventually.equal(this.soResolved)
  53. .then(function() {
  54. expect(callback.callCount).to.equal(1);
  55. });
  56. });
  57. it('should resolve if resolved before hitting max', function() {
  58. var callback = sinon.stub();
  59. callback.rejects(this.soRejected);
  60. callback.onCall(3).resolves(this.soResolved);
  61. return expect(retry(callback, { max: 10, backoffBase: 0 }))
  62. .to.eventually.equal(this.soResolved)
  63. .then(function() {
  64. expect(callback.firstCall.args).to.deep.equal([{ current: 1 }]);
  65. expect(callback.secondCall.args).to.deep.equal([{ current: 2 }]);
  66. expect(callback.thirdCall.args).to.deep.equal([{ current: 3 }]);
  67. expect(callback.callCount).to.equal(4);
  68. });
  69. });
  70. describe('options.timeout', function() {
  71. it('should throw if reject on first attempt', function() {
  72. return expect(
  73. retry(
  74. function() {
  75. return delay(2000);
  76. },
  77. {
  78. max: 1,
  79. backoffBase: 0,
  80. timeout: 1000
  81. }
  82. )
  83. ).to.eventually.be.rejectedWith(retry.TimeoutError);
  84. });
  85. it('should throw if reject on last attempt', function() {
  86. return expect(
  87. retry(
  88. function() {
  89. this.count++;
  90. if (this.count === 3) {
  91. return delay(3500);
  92. }
  93. return Promise.reject();
  94. }.bind(this),
  95. {
  96. max: 3,
  97. backoffBase: 0,
  98. timeout: 1500
  99. }
  100. )
  101. )
  102. .to.eventually.be.rejectedWith(retry.TimeoutError)
  103. .then(function() {
  104. expect(this.count).to.equal(3);
  105. }.bind(this));
  106. });
  107. });
  108. describe('options.match', function() {
  109. it('should continue retry while error is equal to match string', function() {
  110. var callback = sinon.stub();
  111. callback.rejects(this.soRejected);
  112. callback.onCall(3).resolves(this.soResolved);
  113. return expect(
  114. retry(callback, {
  115. max: 15,
  116. backoffBase: 0,
  117. match: 'Error: ' + this.soRejected.message
  118. })
  119. )
  120. .to.eventually.equal(this.soResolved)
  121. .then(function() {
  122. expect(callback.callCount).to.equal(4);
  123. });
  124. });
  125. it('should reject immediately if error is not equal to match string', function() {
  126. var callback = sinon.stub();
  127. callback.rejects(this.soRejected);
  128. return expect(
  129. retry(callback, {
  130. max: 15,
  131. backoffBase: 0,
  132. match: 'A custom error string'
  133. })
  134. )
  135. .to.eventually.be.rejectedWith(this.soRejected)
  136. .then(function() {
  137. expect(callback.callCount).to.equal(1);
  138. });
  139. });
  140. it('should continue retry while error is instanceof match', function() {
  141. var callback = sinon.stub();
  142. callback.rejects(this.soRejected);
  143. callback.onCall(4).resolves(this.soResolved);
  144. return expect(retry(callback, { max: 15, backoffBase: 0, match: Error }))
  145. .to.eventually.equal(this.soResolved)
  146. .then(function() {
  147. expect(callback.callCount).to.equal(5);
  148. });
  149. });
  150. it('should reject immediately if error is not instanceof match', function() {
  151. var callback = sinon.stub();
  152. callback.rejects(this.soRejected);
  153. return expect(
  154. retry(callback, { max: 15, backoffBase: 0, match: function foo() {} })
  155. )
  156. .to.eventually.be.rejectedWith(Error)
  157. .then(function() {
  158. expect(callback.callCount).to.equal(1);
  159. });
  160. });
  161. it('should continue retry while error is equal to match string in array', function() {
  162. var callback = sinon.stub();
  163. callback.rejects(this.soRejected);
  164. callback.onCall(4).resolves(this.soResolved);
  165. return expect(
  166. retry(callback, {
  167. max: 15,
  168. backoffBase: 0,
  169. match: [
  170. 'Error: ' + (this.soRejected.message + 1),
  171. 'Error: ' + this.soRejected.message
  172. ]
  173. })
  174. )
  175. .to.eventually.equal(this.soResolved)
  176. .then(function() {
  177. expect(callback.callCount).to.equal(5);
  178. });
  179. });
  180. it('should reject immediately if error is not equal to match string in array', function() {
  181. var callback = sinon.stub();
  182. callback.rejects(this.soRejected);
  183. return expect(
  184. retry(callback, {
  185. max: 15,
  186. backoffBase: 0,
  187. match: [
  188. 'Error: ' + (this.soRejected + 1),
  189. 'Error: ' + (this.soRejected + 2)
  190. ]
  191. })
  192. )
  193. .to.eventually.be.rejectedWith(Error)
  194. .then(function() {
  195. expect(callback.callCount).to.equal(1);
  196. });
  197. });
  198. it('should reject immediately if error is not instanceof match in array', function() {
  199. var callback = sinon.stub();
  200. callback.rejects(this.soRejected);
  201. return expect(
  202. retry(callback, {
  203. max: 15,
  204. backoffBase: 0,
  205. match: ['Error: ' + (this.soRejected + 1), function foo() {}]
  206. })
  207. )
  208. .to.eventually.be.rejectedWith(Error)
  209. .then(function() {
  210. expect(callback.callCount).to.equal(1);
  211. });
  212. });
  213. it('should continue retry while error is instanceof match in array', function() {
  214. var callback = sinon.stub();
  215. callback.rejects(this.soRejected);
  216. callback.onCall(4).resolves(this.soResolved);
  217. return expect(
  218. retry(callback, {
  219. max: 15,
  220. backoffBase: 0,
  221. match: ['Error: ' + (this.soRejected + 1), Error]
  222. })
  223. )
  224. .to.eventually.equal(this.soResolved)
  225. .then(function() {
  226. expect(callback.callCount).to.equal(5);
  227. });
  228. });
  229. it('should continue retry while error is matched by function', function() {
  230. var callback = sinon.stub();
  231. callback.rejects(this.soRejected);
  232. callback.onCall(4).resolves(this.soResolved);
  233. return expect(
  234. retry(callback, {
  235. max: 15,
  236. backoffBase: 0,
  237. match: (err) => err instanceof Error
  238. })
  239. )
  240. .to.eventually.equal(this.soResolved)
  241. .then(function() {
  242. expect(callback.callCount).to.equal(5);
  243. });
  244. });
  245. it('should continue retry while error is matched by a function in array', function() {
  246. var callback = sinon.stub();
  247. callback.rejects(this.soRejected);
  248. callback.onCall(4).resolves(this.soResolved);
  249. return expect(
  250. retry(callback, {
  251. max: 15,
  252. backoffBase: 0,
  253. match: [
  254. (err) => err instanceof Error
  255. ]
  256. })
  257. )
  258. .to.eventually.equal(this.soResolved)
  259. .then(function() {
  260. expect(callback.callCount).to.equal(5);
  261. });
  262. });
  263. });
  264. describe('options.backoff', function() {
  265. it('should resolve after 5 retries and an eventual delay over 611ms using default backoff', async function() {
  266. // Given
  267. var callback = sinon.stub();
  268. callback.rejects(this.soRejected);
  269. callback.onCall(5).resolves(this.soResolved);
  270. // When
  271. var startTime = moment();
  272. const result = await retry(callback, { max: 15 });
  273. var endTime = moment();
  274. // Then
  275. expect(result).to.equal(this.soResolved);
  276. expect(callback.callCount).to.equal(6);
  277. expect(endTime.diff(startTime)).to.be.within(600, 650);
  278. });
  279. it('should resolve after 1 retry and initial delay equal to the backoffBase', async function() {
  280. var initialDelay = 100;
  281. var callback = sinon.stub();
  282. callback.onCall(0).rejects(this.soRejected);
  283. callback.onCall(1).resolves(this.soResolved);
  284. var startTime = moment();
  285. const result = await retry(callback, {
  286. max: 2,
  287. backoffBase: initialDelay,
  288. backoffExponent: 3
  289. });
  290. var endTime = moment();
  291. expect(result).to.equal(this.soResolved);
  292. expect(callback.callCount).to.equal(2);
  293. // allow for some overhead
  294. expect(endTime.diff(startTime)).to.be.within(initialDelay, initialDelay + 50);
  295. });
  296. it('should throw TimeoutError and cancel backoff delay if timeout is reached', function() {
  297. return expect(
  298. retry(
  299. function() {
  300. return delay(2000);
  301. },
  302. {
  303. max: 15,
  304. timeout: 1000
  305. }
  306. )
  307. ).to.eventually.be.rejectedWith(retry.TimeoutError);
  308. });
  309. });
  310. describe('options.report', function() {
  311. it('should receive the error that triggered a retry', function() {
  312. var report = sinon.stub();
  313. var callback = sinon.stub();
  314. callback.rejects(this.soRejected);
  315. callback.onCall(1).resolves(this.soResolved);
  316. return expect(
  317. retry(callback, {max: 3, report})
  318. )
  319. .to.eventually.equal(this.soResolved)
  320. .then(function() {
  321. expect(callback.callCount).to.equal(2);
  322. // messages sent to report are:
  323. // Trying functionStub #1 at <timestamp>
  324. // Error: <random number> <--- This is the report call we want to test
  325. // Retrying functionStub (2)
  326. // Delaying retry of functionStub by 100
  327. // Trying functionStub #2 at <timestamp>
  328. expect(report.callCount).to.equal(5);
  329. expect(report.getCall(1).args[2]).to.be.instanceOf(Error);
  330. });
  331. });
  332. it('should receive the error that exceeded max', function() {
  333. var report = sinon.stub();
  334. var callback = sinon.stub();
  335. callback.rejects(this.soRejected);
  336. return expect(
  337. retry(callback, {max: 3, report})
  338. )
  339. .to.eventually.be.rejectedWith(Error)
  340. .then(function() {
  341. expect(callback.callCount).to.equal(3);
  342. // Trying functionStub #1 at <timestamp>
  343. // Error: <random number>
  344. // Retrying functionStub (2)
  345. // Delaying retry of functionStub by 100
  346. // Trying functionStub #2 at <timestamp>
  347. // Error: <random number>
  348. // Retrying functionStub (3)
  349. // Delaying retry of functionStub by 110.00000000000001
  350. // Trying functionStub #3 at <timestamp>
  351. // Error: <random number> <--- This is the report call we want to test
  352. expect(report.callCount).to.equal(10);
  353. expect(report.lastCall.args[2]).to.be.instanceOf(Error);
  354. });
  355. });
  356. });
  357. });