1 | import {expect} from 'chai';
|
2 | import * as sinon from 'sinon';
|
3 | import {delayedPromise, retryPromise, RetryPromiseOptions, timeoutPromise} from '../src/promise-utils';
|
4 |
|
5 | const accuracyFactor = 0.9;
|
6 | describe('Promise utilities', () => {
|
7 | describe('delayedPromise', () => {
|
8 | it('resolves after provided the ms', async () => {
|
9 | const startTime = Date.now(), delay = 50;
|
10 |
|
11 | await delayedPromise(delay);
|
12 |
|
13 | expect(Date.now(), 'verify delay').to.be.gte(startTime + (delay * accuracyFactor));
|
14 | })
|
15 | });
|
16 |
|
17 | describe('timeoutPromise', () => {
|
18 | it('resolves with original value if original promise resolves within time frame', async () => {
|
19 | await expect(timeoutPromise(Promise.resolve('test'), 100)).to.eventually.become('test');
|
20 | });
|
21 |
|
22 | it('rejects with original value if original promise rejects within time frame', async () => {
|
23 | await expect(timeoutPromise(Promise.reject('an error'), 100)).to.eventually.be.rejectedWith('an error');
|
24 | });
|
25 |
|
26 | it('rejects with a timeout message if time is up and original promise is pending', async () => {
|
27 | await expect(timeoutPromise(delayedPromise(200), 50)).to.eventually.be.rejectedWith('timed out after 50ms');
|
28 | });
|
29 |
|
30 | it('allows providing a custom timeout message', async () => {
|
31 | await expect(timeoutPromise(delayedPromise(200), 50, 'FAILED!')).to.eventually.be.rejectedWith('FAILED!');
|
32 | });
|
33 | });
|
34 |
|
35 | describe('retryPromise', () => {
|
36 | async function verifyCallCount(spy: sinon.SinonSpy, count: number, noExtraEventsGrace: number): Promise<void> {
|
37 | expect(spy).to.have.callCount(count);
|
38 | await delayedPromise(noExtraEventsGrace);
|
39 | expect(spy).to.have.callCount(count);
|
40 | }
|
41 |
|
42 | it('resolves if first run was a success', async () => {
|
43 | const retryOptions: RetryPromiseOptions = {retries: 2, interval: 5};
|
44 | const promiseProvider = sinon.stub().resolves('value');
|
45 |
|
46 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.become('value');
|
47 | await verifyCallCount(promiseProvider, 1, retryOptions.interval + 1);
|
48 | });
|
49 |
|
50 | it('rejects if first run failed, and no retries', async () => {
|
51 | const retryOptions: RetryPromiseOptions = {retries: 0, interval: 10};
|
52 | const promiseProvider = sinon.stub().rejects(new Error('failed'));
|
53 |
|
54 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.rejectedWith('failed');
|
55 | await verifyCallCount(promiseProvider, 1, retryOptions.interval + 1);
|
56 | });
|
57 |
|
58 | it('resolves if a success run was achieved during a retry', async () => {
|
59 | const retryOptions: RetryPromiseOptions = {retries: 2, interval: 10};
|
60 | const promiseProvider = sinon.stub()
|
61 | .onFirstCall().rejects(new Error('first failure'))
|
62 | .onSecondCall().rejects(new Error('second failure'))
|
63 | .resolves('success');
|
64 |
|
65 | const startTime = Date.now();
|
66 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.become('success');
|
67 | expect(Date.now(), 'verify interval').to.be.gte(startTime + (retryOptions.interval * 2 * accuracyFactor));
|
68 | await verifyCallCount(promiseProvider, 3, retryOptions.interval + 1);
|
69 | });
|
70 |
|
71 | it('rejects if all tries failed', async () => {
|
72 | const retryOptions: RetryPromiseOptions = {retries: 5, interval: 5};
|
73 | const promiseProvider = sinon.stub().rejects(new Error('failed'));
|
74 |
|
75 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.rejectedWith('failed');
|
76 | await verifyCallCount(promiseProvider, 6, retryOptions.interval + 1);
|
77 | });
|
78 |
|
79 | it('rejects with error of last failed attempt', async () => {
|
80 | const retryOptions: RetryPromiseOptions = {retries: 1, interval: 5};
|
81 | const promiseProvider = sinon.stub()
|
82 | .onFirstCall().rejects(new Error('first failure'))
|
83 | .onSecondCall().rejects(new Error('second failure'))
|
84 | .rejects(new Error('other failures'));
|
85 |
|
86 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.rejectedWith('second failure');
|
87 | await verifyCallCount(promiseProvider, 2, retryOptions.interval + 1);
|
88 |
|
89 | });
|
90 |
|
91 | describe('when provided with a timeout', () => {
|
92 | it('verifies timeout is greater than retries*interval', async () => {
|
93 | const retryOptions: RetryPromiseOptions = {retries: 10, interval: 10, timeout: 90};
|
94 | const promiseProvider = sinon.stub();
|
95 |
|
96 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually
|
97 | .be.rejectedWith('timeout (90ms) must be greater than retries (10) times interval (10ms)');
|
98 | await verifyCallCount(promiseProvider, 0, retryOptions.interval + 1);
|
99 | });
|
100 |
|
101 | it('resolves if a success run was achieved during timeout', async () => {
|
102 | const retryOptions: RetryPromiseOptions = {retries: 1, interval: 5, timeout: 1500};
|
103 | const promiseProvider = sinon.stub()
|
104 | .onFirstCall().rejects(new Error('first failure'))
|
105 | .resolves('success');
|
106 |
|
107 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.become('success');
|
108 | await verifyCallCount(promiseProvider, 2, retryOptions.interval + 1);
|
109 | });
|
110 |
|
111 | it('rejects with error of last failed attempt if timeout expires', async () => {
|
112 | const retryOptions: RetryPromiseOptions = {retries: 1, interval: 10, timeout: 400};
|
113 | const promiseProvider = sinon.stub()
|
114 | .onFirstCall().rejects(new Error('first failure'))
|
115 | .onSecondCall().returns(delayedPromise(2000));
|
116 |
|
117 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.be.rejectedWith('first failure');
|
118 | await verifyCallCount(promiseProvider, 2, retryOptions.interval + 1);
|
119 |
|
120 | });
|
121 |
|
122 | it('rejects with default timeout message, if no last failed attempt and timeout expires', async () => {
|
123 | const retryOptions: RetryPromiseOptions = {retries: 0, interval: 20, timeout: 50};
|
124 | const promiseProvider = sinon.stub().returns(delayedPromise(1000));
|
125 |
|
126 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.be.rejectedWith('timed out after 50ms');
|
127 | await verifyCallCount(promiseProvider, 1, retryOptions.interval + 1);
|
128 | });
|
129 |
|
130 | it('rejects with provided timeout message, if no last failed attempt and timeout expires', async () => {
|
131 | const retryOptions: RetryPromiseOptions = {
|
132 | retries: 0,
|
133 | interval: 20,
|
134 | timeout: 50,
|
135 | timeoutMessage: 'FAILED'
|
136 | };
|
137 | const promiseProvider = sinon.stub().returns(delayedPromise(1000));
|
138 |
|
139 | await expect(retryPromise(promiseProvider, retryOptions)).to.eventually.be.rejectedWith('FAILED');
|
140 | await verifyCallCount(promiseProvider, 1, retryOptions.interval + 1);
|
141 | });
|
142 | });
|
143 | });
|
144 | });
|