diff --git a/README.md b/README.md index dd09937..2a34178 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,17 @@ __Note__: ambiguous characters are only removed if there is more than one ambigu 'WG86SAH22SWB' // output will never contain an 'O' (or a '0' for that matter) ``` +#### Random Length + +In case you want the exact `length` to be random, you can pass in a range: + +```javascript +> password.randomPassword({ length: [6, 8], characters: password.digits }) +'6324' +``` + +The generated password will have at least 6 characters, but no more than 8 characters. In other words, both the lower and the upper bounds are inclusive. + #### Predicate If you need the password to meet some arbitrary complexity requirement, you can pass in a `predicate` function. diff --git a/index.js b/index.js index 8040e30..d8ceb75 100644 --- a/index.js +++ b/index.js @@ -29,13 +29,23 @@ function randomPassword(opts) { var characterRules = translateRules(opts); - if (!util.isInteger(opts.length)) { + if (util.isInteger(opts.length)) { + opts.length = [opts.length, opts.length]; + } + + var lowerLength = opts.length[0]; + var upperLength = opts.length[1]; + + if (!util.isInteger(lowerLength) || !util.isInteger(upperLength)) { throw new Error('length must be an integer'); } - if (opts.length < 1) { + if (upperLength < lowerLength) { + throw new Error('length upper bound must be greater than the lower bound'); + } + if (lowerLength < 1) { throw new Error('length must be > 0'); } - if (opts.length < characterRules.length) { + if (upperLength < characterRules.length) { throw new Error('length must be >= # of character sets passed'); } if (characterRules.some(function (rule) { return !rule.characters })) { @@ -51,9 +61,10 @@ function randomPassword(opts) { var minimumLength = characterRules .map(function (rule) { return rule.exactly || 1 }) .reduce(function (l, r) { return l + r }, 0); - if (opts.length < minimumLength) { + if (upperLength < minimumLength) { throw new Error('length is too short for character set rules'); } + lowerLength = Math.max(lowerLength, minimumLength); var allExactly = characterRules.every(function (rule) { return rule.exactly }); if (allExactly) { @@ -63,9 +74,11 @@ function randomPassword(opts) { } } + var length = opts.random.between(lowerLength, upperLength); + var result; do { - result = generatePassword(characterRules, opts.length, opts.random); + result = generatePassword(characterRules, length, opts.random); } while (!opts.predicate(result)); return result; } diff --git a/index.spec.js b/index.spec.js index a8b1aad..a36b3d4 100644 --- a/index.spec.js +++ b/index.spec.js @@ -5,6 +5,7 @@ describe('passwordGenerator', () => { beforeEach(() => { random = { + between: jest.fn((x, y) => y), choose: jest.fn(), shuffle: jest.fn(x => x), }; @@ -21,6 +22,10 @@ describe('passwordGenerator', () => { expect(() => randomPassword({ length: 'not-an-integer' })).toThrow('length must be an integer'); }); + it('throws an error when passed an upper length that is not an integer', () => { + expect(() => randomPassword({ length: [123, 'not-an-integer'] })).toThrow('length must be an integer'); + }); + it('throws an error when passed a length less than 1', () => { expect(() => randomPassword({ length: 0 })).toThrow('length must be > 0'); }); @@ -82,6 +87,49 @@ describe('passwordGenerator', () => { expect(result).toBe('cbaabc'); }); + describe('when given a range for length', () => { + + it('throws an error when upper bound is less than the lower bound', () => { + expect(() => randomPassword({ + length: [42, 41], + })).toThrow('length upper bound must be greater than the lower bound'); + }); + + it('returns sequence with length between the passed range', () => { + random.between.mockReturnValueOnce(5); + random.choose.mockImplementation(x => x); + + expect(randomPassword({ + length: [2, 10], + characters: 'a', + random + })).toBe('aaaaa'); + expect(random.between).toHaveBeenCalledWith(2, 10); + }); + + it('throws an error when upper bound is less than the totaled number of exactly character sets + required sets', () => { + expect(() => randomPassword({ + length: [1, 42], + characters: [ + 'abc', + { characters: '123', exactly: 42 } + ] + })).toThrow('length is too short for character set rules'); + }); + + it('uses the effective minimum length as the lower bound when it is greater than the passed lower bound', () => { + const password = randomPassword({ + length: [1, 42], + characters: [ + 'abc', + { characters: '123', exactly: 41 } + ] + }); + expect(password.length).toBe(42); + }); + + }); + describe('when passed multiple character sets', () => { it('throws an error if the passed length is fewer than the number of sets passed', () => { expect(() => randomPassword({ diff --git a/lib/random.js b/lib/random.js index a5ceb94..19c75fd 100644 --- a/lib/random.js +++ b/lib/random.js @@ -19,6 +19,10 @@ Random.prototype.choose = function (choices) { return choices[this._getInt(choices.length)]; }; +Random.prototype.between = function (lowerBoundInclusive, upperBoundInclusive) { + return lowerBoundInclusive + this.getInt(upperBoundInclusive - lowerBoundInclusive + 1); +}; + Random.prototype.getInt = function (upperBoundExclusive) { if (upperBoundExclusive === undefined) { throw new Error('Must pass an upper bound'); diff --git a/lib/random.spec.js b/lib/random.spec.js index fb055b1..be06c59 100644 --- a/lib/random.spec.js +++ b/lib/random.spec.js @@ -13,6 +13,26 @@ describe('Random', () => { expect(() => new Random(123)).toThrow('Must pass a randomSource function'); }); + describe('numberBetween', () => { + + it('uses the randomSource to pick a number in between the given range', () => { + const lower = 2; + const upper = 5; + + testCase(0, 2); + testCase(1, 3); + testCase(2, 4); + testCase(3, 5); + + function testCase(randomValue, expected) { + randomSource.mockReturnValueOnce([randomValue]); + + expect(subject.between(lower, upper)).toBe(expected); + } + }); + + }); + describe('getInt', () => { it('throws an error when not passed an upper bound', () => { expect(() => subject.getInt()).toThrow('Must pass an upper bound');