466 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| var chai = require('chai'),
 | |
|   expect = chai.expect,
 | |
|   moment = require('moment'),
 | |
|   sinon = require('sinon');
 | |
| 
 | |
| var delay = ms => new Promise(_ => setTimeout(_, ms));
 | |
| 
 | |
| chai.use(require('chai-as-promised'));
 | |
| sinon.usingPromise(Promise);
 | |
| 
 | |
| describe('Global Promise', function() {
 | |
|   var retry = require('../').default;
 | |
|   var applyJitter = require('../').applyJitter;
 | |
| 
 | |
| 
 | |
|   beforeEach(function() {
 | |
|     this.count = 0;
 | |
|     this.soRejected = new Error(Math.random().toString());
 | |
|     this.soResolved = new Error(Math.random().toString());
 | |
|   });
 | |
| 
 | |
|   it('should reject immediately if max is 1 (using options)', function() {
 | |
|     var callback = sinon.stub();
 | |
| 
 | |
|     callback.resolves(this.soResolved);
 | |
|     callback.onCall(0).rejects(this.soRejected);
 | |
| 
 | |
|     return expect(retry(callback, { max: 1, backoffBase: 0 }))
 | |
|       .to.eventually.be.rejectedWith(this.soRejected)
 | |
|       .then(function() {
 | |
|         expect(callback.callCount).to.equal(1);
 | |
|       });
 | |
|   });
 | |
| 
 | |
|   it('should reject immediately if max is 1 (using integer)', function() {
 | |
|     var callback = sinon.stub();
 | |
| 
 | |
|     callback.resolves(this.soResolved);
 | |
|     callback.onCall(0).rejects(this.soRejected);
 | |
| 
 | |
|     return expect(retry(callback, 1))
 | |
|       .to.eventually.be.rejectedWith(this.soRejected)
 | |
|       .then(function() {
 | |
|         expect(callback.callCount).to.equal(1);
 | |
|       });
 | |
|   });
 | |
| 
 | |
|   it('should reject after all tries if still rejected', function() {
 | |
|     var callback = sinon.stub();
 | |
| 
 | |
|     callback.rejects(this.soRejected);
 | |
| 
 | |
|     return expect(retry(callback, { max: 3, backoffBase: 0 }))
 | |
|       .to.eventually.be.rejectedWith(this.soRejected)
 | |
|       .then(function() {
 | |
|         expect(callback.firstCall.args).to.deep.equal([{ current: 1 }]);
 | |
|         expect(callback.secondCall.args).to.deep.equal([{ current: 2 }]);
 | |
|         expect(callback.thirdCall.args).to.deep.equal([{ current: 3 }]);
 | |
|         expect(callback.callCount).to.equal(3);
 | |
|       });
 | |
|   });
 | |
| 
 | |
|   it('should resolve immediately if resolved on first try', function() {
 | |
|     var callback = sinon.stub();
 | |
| 
 | |
|     callback.resolves(this.soResolved);
 | |
|     callback.onCall(0).resolves(this.soResolved);
 | |
| 
 | |
|     return expect(retry(callback, { max: 10, backoffBase: 0 }))
 | |
|       .to.eventually.equal(this.soResolved)
 | |
|       .then(function() {
 | |
|         expect(callback.callCount).to.equal(1);
 | |
|       });
 | |
|   });
 | |
| 
 | |
|   it('should resolve if resolved before hitting max', function() {
 | |
|     var callback = sinon.stub();
 | |
| 
 | |
|     callback.rejects(this.soRejected);
 | |
|     callback.onCall(3).resolves(this.soResolved);
 | |
| 
 | |
|     return expect(retry(callback, { max: 10, backoffBase: 0 }))
 | |
|       .to.eventually.equal(this.soResolved)
 | |
|       .then(function() {
 | |
|         expect(callback.firstCall.args).to.deep.equal([{ current: 1 }]);
 | |
|         expect(callback.secondCall.args).to.deep.equal([{ current: 2 }]);
 | |
|         expect(callback.thirdCall.args).to.deep.equal([{ current: 3 }]);
 | |
|         expect(callback.callCount).to.equal(4);
 | |
|       });
 | |
|   });
 | |
| 
 | |
|   describe('options.timeout', function() {
 | |
|     it('should throw if reject on first attempt', function() {
 | |
|       return expect(
 | |
|         retry(
 | |
|           function() {
 | |
|             return delay(2000);
 | |
|           },
 | |
|           {
 | |
|             max: 1,
 | |
|             backoffBase: 0,
 | |
|             timeout: 1000
 | |
|           }
 | |
|         )
 | |
|       ).to.eventually.be.rejectedWith(retry.TimeoutError);
 | |
|     });
 | |
| 
 | |
|     it('should throw if reject on last attempt', function() {
 | |
|       return expect(
 | |
|         retry(
 | |
|           function() {
 | |
|             this.count++;
 | |
|             if (this.count === 3) {
 | |
|               return delay(3500);
 | |
|             }
 | |
|             return Promise.reject();
 | |
|           }.bind(this),
 | |
|           {
 | |
|             max: 3,
 | |
|             backoffBase: 0,
 | |
|             timeout: 1500
 | |
|           }
 | |
|         )
 | |
|       )
 | |
|         .to.eventually.be.rejectedWith(retry.TimeoutError)
 | |
|         .then(function() {
 | |
|           expect(this.count).to.equal(3);
 | |
|         }.bind(this));
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('options.match', function() {
 | |
|     it('should continue retry while error is equal to match string', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(3).resolves(this.soResolved);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {
 | |
|           max: 15,
 | |
|           backoffBase: 0,
 | |
|           match: 'Error: ' + this.soRejected.message
 | |
|         })
 | |
|       )
 | |
|         .to.eventually.equal(this.soResolved)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(4);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should reject immediately if error is not equal to match string', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {
 | |
|           max: 15,
 | |
|           backoffBase: 0,
 | |
|           match: 'A custom error string'
 | |
|         })
 | |
|       )
 | |
|         .to.eventually.be.rejectedWith(this.soRejected)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(1);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should continue retry while error is instanceof match', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(4).resolves(this.soResolved);
 | |
| 
 | |
|       return expect(retry(callback, { max: 15, backoffBase: 0, match: Error }))
 | |
|         .to.eventually.equal(this.soResolved)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(5);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should reject immediately if error is not instanceof match', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, { max: 15, backoffBase: 0, match: function foo() {} })
 | |
|       )
 | |
|         .to.eventually.be.rejectedWith(Error)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(1);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should continue retry while error is equal to match string in array', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(4).resolves(this.soResolved);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {
 | |
|           max: 15,
 | |
|           backoffBase: 0,
 | |
|           match: [
 | |
|             'Error: ' + (this.soRejected.message + 1),
 | |
|             'Error: ' + this.soRejected.message
 | |
|           ]
 | |
|         })
 | |
|       )
 | |
|         .to.eventually.equal(this.soResolved)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(5);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should reject immediately if error is not equal to match string in array', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {
 | |
|           max: 15,
 | |
|           backoffBase: 0,
 | |
|           match: [
 | |
|             'Error: ' + (this.soRejected + 1),
 | |
|             'Error: ' + (this.soRejected + 2)
 | |
|           ]
 | |
|         })
 | |
|       )
 | |
|         .to.eventually.be.rejectedWith(Error)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(1);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should reject immediately if error is not instanceof match in array', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {
 | |
|           max: 15,
 | |
|           backoffBase: 0,
 | |
|           match: ['Error: ' + (this.soRejected + 1), function foo() {}]
 | |
|         })
 | |
|       )
 | |
|         .to.eventually.be.rejectedWith(Error)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(1);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should continue retry while error is instanceof match in array', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(4).resolves(this.soResolved);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {
 | |
|           max: 15,
 | |
|           backoffBase: 0,
 | |
|           match: ['Error: ' + (this.soRejected + 1), Error]
 | |
|         })
 | |
|       )
 | |
|         .to.eventually.equal(this.soResolved)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(5);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should continue retry while error is matched by function', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(4).resolves(this.soResolved);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {
 | |
|           max: 15,
 | |
|           backoffBase: 0,
 | |
|           match: (err) => err instanceof Error
 | |
|         })
 | |
|       )
 | |
|         .to.eventually.equal(this.soResolved)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(5);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should continue retry while error is matched by a function in array', function() {
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(4).resolves(this.soResolved);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {
 | |
|           max: 15,
 | |
|           backoffBase: 0,
 | |
|           match: [
 | |
|             (err) => err instanceof Error
 | |
|           ]
 | |
|         })
 | |
|       )
 | |
|         .to.eventually.equal(this.soResolved)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(5);
 | |
|         });
 | |
|     });
 | |
| });
 | |
| 
 | |
|   describe('options.backoff', function() {
 | |
|     it('should resolve after 5 retries and an eventual delay over 611ms using default backoff', async function() {
 | |
|       // Given
 | |
|       var callback = sinon.stub();
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(5).resolves(this.soResolved);
 | |
| 
 | |
|       // When
 | |
|       var startTime = moment();
 | |
|       const result = await retry(callback, { max: 15 });
 | |
|       var endTime = moment();
 | |
| 
 | |
|       // Then
 | |
|       expect(result).to.equal(this.soResolved);
 | |
|       expect(callback.callCount).to.equal(6);
 | |
|       expect(endTime.diff(startTime)).to.be.within(600, 650);
 | |
|     });
 | |
| 
 | |
|     it('should resolve after 1 retry and initial delay equal to the backoffBase', async function() {
 | |
|       var initialDelay = 100;
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.onCall(0).rejects(this.soRejected);
 | |
|       callback.onCall(1).resolves(this.soResolved);
 | |
| 
 | |
|       var startTime = moment();
 | |
|       const result = await retry(callback, {
 | |
|           max: 2,
 | |
|           backoffBase: initialDelay,
 | |
|           backoffExponent: 3
 | |
|         });
 | |
|       var endTime = moment();
 | |
| 
 | |
|       expect(result).to.equal(this.soResolved);
 | |
|       expect(callback.callCount).to.equal(2);
 | |
|       // allow for some overhead
 | |
|       expect(endTime.diff(startTime)).to.be.within(initialDelay, initialDelay + 50);
 | |
|     });
 | |
| 
 | |
|     it('should throw TimeoutError and cancel backoff delay if timeout is reached', function() {
 | |
|       return expect(
 | |
|         retry(
 | |
|           function() {
 | |
|             return delay(2000);
 | |
|           },
 | |
|           {
 | |
|             max: 15,
 | |
|             timeout: 1000
 | |
|           }
 | |
|         )
 | |
|       ).to.eventually.be.rejectedWith(retry.TimeoutError);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('options.report', function() {
 | |
|     it('should receive the error that triggered a retry', function() {
 | |
|       var report = sinon.stub();
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(1).resolves(this.soResolved);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {max: 3, report})
 | |
|       )
 | |
|         .to.eventually.equal(this.soResolved)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(2);
 | |
| 
 | |
|           // messages sent to report are:
 | |
|           // Trying functionStub #1 at <timestamp>
 | |
|           // Error: <random number>                 <--- This is the report call we want to test
 | |
|           // Retrying functionStub (2)
 | |
|           // Delaying retry of functionStub by 100
 | |
|           // Trying functionStub #2 at <timestamp>
 | |
|           expect(report.callCount).to.equal(5);
 | |
|           expect(report.getCall(1).args[2]).to.be.instanceOf(Error);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it('should receive the error that exceeded max', function() {
 | |
|       var report = sinon.stub();
 | |
|       var callback = sinon.stub();
 | |
| 
 | |
|       callback.rejects(this.soRejected);
 | |
| 
 | |
|       return expect(
 | |
|         retry(callback, {max: 3, report})
 | |
|       )
 | |
|         .to.eventually.be.rejectedWith(Error)
 | |
|         .then(function() {
 | |
|           expect(callback.callCount).to.equal(3);
 | |
| 
 | |
|           // Trying functionStub #1 at <timestamp>
 | |
|           // Error: <random number>
 | |
|           // Retrying functionStub (2)
 | |
|           // Delaying retry of functionStub by 100
 | |
|           // Trying functionStub #2 at <timestamp>
 | |
|           // Error: <random number>
 | |
|           // Retrying functionStub (3)
 | |
|           // Delaying retry of functionStub by 110.00000000000001
 | |
|           // Trying functionStub #3 at <timestamp>
 | |
|           // Error: <random number>                 <--- This is the report call we want to test
 | |
|           expect(report.callCount).to.equal(10);
 | |
|           expect(report.lastCall.args[2]).to.be.instanceOf(Error);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|   });
 | |
| 
 | |
|   describe('options.backoffJitter', function() {
 | |
| 
 | |
|     describe('fn:applyJitter', function() {
 | |
|       it('applies randomized offsets to base delay', function() {
 | |
|         for (let i = 0; i < 10; i++) {
 | |
|           const withJitter = applyJitter(1000, 100);
 | |
|           expect((withJitter >= 900 && withJitter <= 1100)).to.equal(true);
 | |
|         }
 | |
|       });
 | |
|       
 | |
|       it('never returns values less than zero', function() {
 | |
|         for (let i = 0; i < 10; i++) {
 | |
|           expect(applyJitter(10, 1000) >= 0).to.equal(true);
 | |
|         }
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('should resolve after 1 retries and an eventual delay in range of 80-120 ms', async function() {
 | |
|       var initialDelay = 100;
 | |
|       var delayJitter = 20;
 | |
| 
 | |
|       // Given
 | |
|       var callback = sinon.stub();
 | |
|       callback.rejects(this.soRejected);
 | |
|       callback.onCall(1).resolves(this.soResolved);
 | |
| 
 | |
|       // When
 | |
|       var startTime = moment();
 | |
|       const result = await retry(callback, { max: 5, backoffBase: initialDelay, backoffJitter: delayJitter });
 | |
|       var endTime = moment();
 | |
| 
 | |
|       // Then
 | |
|       expect(result).to.equal(this.soResolved);
 | |
|       expect(callback.callCount).to.equal(2);
 | |
|       expect(endTime.diff(startTime)).to.be.within(75, 125);
 | |
|     });
 | |
|   });
 | |
| });
 |