|
| 1 | +import * as chai from 'chai'; |
| 2 | +import * as chaiAsPromised from 'chai-as-promised'; |
| 3 | +chai.use(chaiAsPromised); |
| 4 | + |
1 | 5 | import { assert, expect } from 'chai';
|
2 | 6 | import 'mocha';
|
3 |
| -import { ERRORS, hasNested, limitFn, range } from './utility-functions'; |
| 7 | +import { stdout } from 'test-console'; |
| 8 | +import { ERRORS, getFnName, hasNested, limitFn, range, retry, sleep } from './utility-functions'; |
4 | 9 |
|
5 | 10 | describe('Utility Functions', () => {
|
6 |
| - describe('hasNested', () => { |
| 11 | + describe('hasNested()', () => { |
7 | 12 | it('returns true if object has key', () => {
|
8 | 13 | const object = {
|
9 | 14 | key: 'value',
|
@@ -48,10 +53,25 @@ describe('Utility Functions', () => {
|
48 | 53 | it('returns false if passed empty path string', () => {
|
49 | 54 | expect(hasNested({}, '')).to.be.false;
|
50 | 55 | });
|
| 56 | + |
| 57 | + |
| 58 | + it('throws if first parameter is not an object', () => { |
| 59 | + assert.throws(() => hasNested(undefined, 'key'), ERRORS.HAS_NESTED_NOT_AN_OBJECT); |
| 60 | + }); |
| 61 | + |
| 62 | + |
| 63 | + it('throws if first parameter is an array', () => { |
| 64 | + assert.throws(() => hasNested(['value'], 'key'), ERRORS.HAS_NESTED_NOT_AN_OBJECT); |
| 65 | + }); |
| 66 | + |
| 67 | + |
| 68 | + it('throws if second parameter is not a string or an array', () => { |
| 69 | + assert.throws(() => hasNested({}, undefined), ERRORS.HAS_NESTED_NOT_A_STRING); |
| 70 | + }); |
51 | 71 | });
|
52 | 72 |
|
53 | 73 |
|
54 |
| - describe('limitFn', () => { |
| 74 | + describe('limitFn()', () => { |
55 | 75 | it('returns a function', () => {
|
56 | 76 | const fn = () => true;
|
57 | 77 | expect(limitFn(fn)).to.be.a('function');
|
@@ -102,7 +122,7 @@ describe('Utility Functions', () => {
|
102 | 122 | });
|
103 | 123 |
|
104 | 124 |
|
105 |
| - describe('range', () => { |
| 125 | + describe('range()', () => { |
106 | 126 | it('creates an array from 1 to 5', () => {
|
107 | 127 | const subject = range(1, 5);
|
108 | 128 | expect(subject).to.be.an('array');
|
@@ -149,4 +169,154 @@ describe('Utility Functions', () => {
|
149 | 169 | assert.throws(() => range(2, 2), ERRORS.RANGE_LIMITS_EQUAL);
|
150 | 170 | });
|
151 | 171 | });
|
| 172 | + |
| 173 | + |
| 174 | + describe('getFnName()', () => { |
| 175 | + it('returns function name', () => { |
| 176 | + const myFunction = () => true; |
| 177 | + expect(getFnName(myFunction)).to.equal('function \'myFunction\''); |
| 178 | + }); |
| 179 | + |
| 180 | + |
| 181 | + it('returns anonymous function name', () => { |
| 182 | + expect(getFnName(() => true)).to.equal('\'anonymous\' function'); |
| 183 | + }); |
| 184 | + }); |
| 185 | + |
| 186 | + |
| 187 | + describe('sleep()', () => { |
| 188 | + it('resolves after a 1000ms (default)', async () => { |
| 189 | + const startTime = Date.now(); |
| 190 | + await sleep(); |
| 191 | + expect((Date.now() - startTime)).to.be.gte(1000 * 0.9); |
| 192 | + }).timeout(5000); |
| 193 | + |
| 194 | + |
| 195 | + it('resolves after a 500ms', async () => { |
| 196 | + const startTime = Date.now(); |
| 197 | + await sleep(500); |
| 198 | + expect((Date.now() - startTime)).to.be.gte(500 * 0.9); |
| 199 | + }).timeout(5000); |
| 200 | + |
| 201 | + |
| 202 | + it('resolves after a 250ms', async () => { |
| 203 | + const startTime = Date.now(); |
| 204 | + await sleep(250); |
| 205 | + expect((Date.now() - startTime)).to.be.gte(250 * 0.9); |
| 206 | + }).timeout(5000); |
| 207 | + }); |
| 208 | + |
| 209 | + |
| 210 | + describe('retry()', () => { |
| 211 | + // Helper function to create error prone functions |
| 212 | + function badFn(fnName: string = 'errorProneFn', workOn?: number, errorNumber: number = 1) { |
| 213 | + const innerFn = function () { |
| 214 | + if (workOn && workOn === errorNumber - 1) { |
| 215 | + return true; |
| 216 | + } |
| 217 | + throw new Error(String(errorNumber++)); |
| 218 | + }; |
| 219 | + |
| 220 | + const api = { [fnName]: innerFn }; |
| 221 | + Object.defineProperty(api[fnName], 'name', { value: fnName }); |
| 222 | + return api; |
| 223 | + } |
| 224 | + |
| 225 | + it('calls passed in function', async () => { |
| 226 | + let wasCalled = false; |
| 227 | + await retry(() => wasCalled = true); |
| 228 | + expect(wasCalled).to.be.true; |
| 229 | + }); |
| 230 | + |
| 231 | + |
| 232 | + it('returns the function\'s return value', () => { |
| 233 | + const returnValue = 'It returns correctly!'; |
| 234 | + const fn = () => returnValue; |
| 235 | + return expect(retry(fn)).to.eventually.be.equal(returnValue); |
| 236 | + }); |
| 237 | + |
| 238 | + |
| 239 | + it('works with an empty options object', () => { |
| 240 | + return expect(retry(() => true, {})).to.be.eventually.true; |
| 241 | + }); |
| 242 | + |
| 243 | + |
| 244 | + it('ignores bad options', () => { |
| 245 | + return expect(retry(() => true, { badOption: true })).to.be.eventually.true; |
| 246 | + }); |
| 247 | + |
| 248 | + |
| 249 | + it('retries 3 times (by default) and returns the last error', () => { |
| 250 | + const { alwaysErrors } = badFn('alwaysErrors'); |
| 251 | + return expect(retry(alwaysErrors, { delay: 100 })).to.be.rejectedWith(/3/); |
| 252 | + }).timeout(5000); |
| 253 | + |
| 254 | + |
| 255 | + it('retries a custom number of times before it returns the error', () => { |
| 256 | + const { errorProneFn } = badFn(undefined, 2); |
| 257 | + return expect(retry(errorProneFn, { delay: 100, retries: 2 })).to.be.rejectedWith(/2/); |
| 258 | + }).timeout(5000); |
| 259 | + |
| 260 | + |
| 261 | + it('retries 1 time and returns function\'s return value', () => { |
| 262 | + const { fnErrorsOnce } = badFn('fnErrorsOnce', 1); |
| 263 | + return expect(retry(fnErrorsOnce)).to.be.fulfilled; |
| 264 | + }).timeout(5000); |
| 265 | + |
| 266 | + it('throws the error of a rejected promise', () => { |
| 267 | + const rejectPromiseFn = () => Promise.reject(new Error('Intentionally rejected promise')); |
| 268 | + return expect(retry(rejectPromiseFn, { delay: 100 })) |
| 269 | + .to.be.rejectedWith(RegExp('Intentionally rejected promise')); |
| 270 | + }).timeout(5000); |
| 271 | + |
| 272 | + |
| 273 | + it('throws if first parameter <fn> is not a function', () => { |
| 274 | + return expect(retry(undefined)).to.be.rejectedWith(ERRORS.RETRY_NEEDS_A_FUNCTION); |
| 275 | + }); |
| 276 | + |
| 277 | + |
| 278 | + it('throws if second parameter <options> is not an object', () => { |
| 279 | + return expect(retry(() => true, 1)).to.be.rejectedWith(ERRORS.RETRY_NEEDS_OPTIONS_TO_BE_OBJECT); |
| 280 | + }); |
| 281 | + |
| 282 | + |
| 283 | + it('throws if option <delay> is 0', () => { |
| 284 | + return expect(retry(() => true, { delay: 0 })).to.be.rejectedWith(ERRORS.RETRY_DELAY_CANNOT_BE_ZERO); |
| 285 | + }); |
| 286 | + |
| 287 | + |
| 288 | + it('throws if option <retries> is 0', () => { |
| 289 | + return expect(retry(() => true, { retries: 0 })).to.be.rejectedWith(ERRORS.RETRY_RETRIES_CANNOT_BE_ZERO); |
| 290 | + }); |
| 291 | + |
| 292 | + |
| 293 | + describe('retry() logging', () => { |
| 294 | + let inspect: any; |
| 295 | + |
| 296 | + beforeEach(() => inspect = stdout.inspect()); |
| 297 | + afterEach(() => { |
| 298 | + inspect.restore(); |
| 299 | + |
| 300 | + // Console out the output from the console.log stub |
| 301 | + for (const output of inspect.output) { |
| 302 | + // Clean up by removing new line characters |
| 303 | + console.log(output.replace('\n', '')); |
| 304 | + } |
| 305 | + }); |
| 306 | + |
| 307 | + |
| 308 | + it('accepts custom logger namespace', async () => { |
| 309 | + const { alwaysErrorsFn } = badFn('alwaysErrorsFn'); |
| 310 | + await retry(alwaysErrorsFn, { logNamespace: 'customNamespace', retries: 1 }).catch(ignore => undefined); |
| 311 | + expect(inspect.output.join(' ')).to.have.string('customNamespace'); |
| 312 | + }).timeout(5000); |
| 313 | + |
| 314 | + |
| 315 | + it('accepts custom logger channel', async () => { |
| 316 | + const { alwaysErrorsFn } = badFn('alwaysErrorsFn'); |
| 317 | + await retry(alwaysErrorsFn, { logChannel: 'customChannel', retries: 1 }).catch(ignore => undefined); |
| 318 | + expect(inspect.output.join(' ')).to.have.string('customChannel'); |
| 319 | + }).timeout(5000); |
| 320 | + }); |
| 321 | + }); |
152 | 322 | });
|
0 commit comments