Skip to content

Commit 80e4584

Browse files
bpleslievbrazo
authored andcommitted
[1673] Ensure mix_case returns at least one lower and one upper case letter (faker-ruby#1694)
1 parent 77b3c1e commit 80e4584

File tree

8 files changed

+131
-22
lines changed

8 files changed

+131
-22
lines changed

doc/default/alphanumeric.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# Keyword arguments: number
55
Faker::Alphanumeric.alpha(number: 10) #=> "zlvubkrwga"
66

7-
# Keyword arguments: number
7+
# Keyword arguments: number, min_alpha, min_numeric
88
Faker::Alphanumeric.alphanumeric(number: 10) #=> "3yfq2phxtb"
9+
Faker::Alphanumeric.alphanumeric(number: 10, min_alpha: 3) #=> "3yfq2phxtb"
10+
Faker::Alphanumeric.alphanumeric(number: 10, min_alpha: 3, min_numeric: 3) #=> "3yfq2phx8b"
911
```

doc/default/lorem.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ Faker::Lorem.words(number: 4, supplemental: true) #=> ["colloco", "qui", "vergo"
1010

1111
Faker::Lorem.multibyte #=> 😀
1212

13-
# Keyword arguments: number
13+
# Keyword arguments: number, min_alpha, min_numeric
1414
Faker::Lorem.characters #=> "uw1ep04lhs0c4d931n1jmrspprf5wrj85fefue0y7y6m56b6omquh7br7dhqijwlawejpl765nb1716idmp3xnfo85v349pzy2o9rir23y2qhflwr71c1585fnynguiphkjm8p0vktwitcsm16lny7jzp9t4drwav3qmhz4yjq4k04x14gl6p148hulyqioo72tf8nwrxxcclfypz2lc58lsibgfe5w5p0xv95peafjjmm2frkhdc6duoky0aha"
1515
Faker::Lorem.characters(number: 10) #=> "ang9cbhoa8"
16+
Faker::Lorem.characters(number: 10, min_alpha: 4) #=> "ang9cbhoa8"
17+
Faker::Lorem.characters(number: 10, min_alpha: 4, min_numeric: 1) #=> "ang9cbhoa8"
1618

1719
# Keyword arguments: word_count, supplemental, random_words_to_add
1820
# The 'random_words_to_add' argument increases the sentence's word count by a random value within (0..random_words_to_add).

lib/faker.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ def random
4040
class Base
4141
Numbers = Array(0..9)
4242
ULetters = Array('A'..'Z')
43-
Letters = ULetters + Array('a'..'z')
43+
LLetters = Array('a'..'z')
44+
Letters = ULetters + LLetters
4445

4546
class << self
4647
NOT_GIVEN = Object.new

lib/faker/default/alphanumeric.rb

+36-8
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,60 @@
22

33
module Faker
44
class Alphanumeric < Base
5-
class << self
6-
ALPHABET = ('a'..'z').to_a
7-
ALPHANUMS = ALPHABET + (0..9).to_a
5+
ALPHANUMS = LLetters + Numbers
86

7+
class << self
98
def alpha(legacy_number = NOT_GIVEN, number: 32)
109
if legacy_number != NOT_GIVEN
1110
warn_with_uplevel 'Passing `number` with the 1st argument of `Alphanumeric.alpha` is deprecated. Use keyword argument like `Alphanumeric.alpha(number: ...)` instead.', uplevel: 1
1211
number = legacy_number
1312
end
14-
1513
char_count = resolve(number)
1614
return '' if char_count.to_i < 1
1715

18-
Array.new(char_count) { sample(ALPHABET) }.join
16+
Array.new(char_count) { sample(self::LLetters) }.join
1917
end
2018

21-
def alphanumeric(legacy_number = NOT_GIVEN, number: 32)
19+
##
20+
# Produces a random string of alphanumeric characters
21+
#
22+
# @param [Integer] number
23+
# @param [Integer] min_alpha
24+
# @param [Integer] min_numeric
25+
#
26+
# @return [String]
27+
#
28+
# @example Faker::Alphanumeric.alphanumeric(number: 10) #=> "3yfq2phxtb"
29+
# @example Faker::Alphanumeric.alphanumeric(number: 10, min_alpha: 3) #=> "3yfq2phxtb"
30+
# @example Faker::Alphanumeric.alphanumeric(number: 10, min_alpha: 3, min_numeric: 3) #=> "3yfq2phx8b"
31+
#
32+
# @faker.version 2.1.3
33+
def alphanumeric(legacy_number = NOT_GIVEN, number: 32, min_alpha: 0, min_numeric: 0)
2234
if legacy_number != NOT_GIVEN
2335
warn_with_uplevel 'Passing `number` with the 1st argument of `Alphanumeric.alphanumeric` is deprecated. Use keyword argument like `Alphanumeric.alphanumeric(number: ...)` instead.', uplevel: 1
2436
number = legacy_number
2537
end
26-
2738
char_count = resolve(number)
2839
return '' if char_count.to_i < 1
40+
raise ArgumentError, 'min_alpha must be greater than or equal to 0' if min_alpha&.negative?
41+
raise ArgumentError, 'min_numeric must be greater than or equal to 0' if min_numeric&.negative?
42+
43+
if min_alpha.zero? && min_numeric.zero?
44+
return Array.new(char_count) { sample(ALPHANUMS) }.join
45+
end
46+
47+
if min_alpha + min_numeric > char_count
48+
raise ArgumentError, 'min_alpha + min_numeric must be <= number'
49+
end
50+
51+
random_count = char_count - min_alpha - min_numeric
52+
53+
alphas = Array.new(min_alpha) { sample(self::LLetters) }
54+
numbers = Array.new(min_numeric) { sample(self::Numbers) }
55+
randoms = Array.new(random_count) { sample(ALPHANUMS) }
2956

30-
Array.new(char_count) { sample(ALPHANUMS) }.join
57+
combined = alphas + numbers + randoms
58+
combined.shuffle.join
3159
end
3260
end
3361
end

lib/faker/default/internet.rb

+24-4
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,25 @@ def username(legacy_specifier = NOT_GIVEN, legacy_separators = NOT_GIVEN, specif
8383
end
8484
end
8585

86+
##
87+
# Produces a randomized string of characters
88+
#
89+
# @param [Integer] min_length
90+
# @param [Integer] max_length
91+
# @param [Boolean] mix_case
92+
# @param [Boolean] special_characters
93+
#
94+
# @return [String]
95+
#
96+
# @example Faker::Internet.password #=> "Vg5mSvY1UeRg7"
97+
# @example Faker::Internet.password(min_length: 8) #=> "YfGjIk0hGzDqS0"
98+
# @example Faker::Internet.password(min_length: 10, max_length: 20) #=> "EoC9ShWd1hWq4vBgFw"
99+
# @example Faker::Internet.password(min_length: 10, max_length: 20, mix_case: true) #=> "3k5qS15aNmG"
100+
# @example Faker::Internet.password(min_length: 10, max_length: 20, mix_case: true, special_characters: true) #=> "*%NkOnJsH4"
101+
#
102+
# @faker.version 2.1.3
86103
# rubocop:disable Metrics/ParameterLists
87104
def password(legacy_min_length = NOT_GIVEN, legacy_max_length = NOT_GIVEN, legacy_mix_case = NOT_GIVEN, legacy_special_characters = NOT_GIVEN, min_length: 8, max_length: 16, mix_case: true, special_characters: false)
88-
# rubocop:enable Metrics/ParameterLists
89105
if legacy_min_length != NOT_GIVEN
90106
warn_with_uplevel 'Passing `min_length` with the 1st argument of `Internet.password` is deprecated. Use keyword argument like `Internet.password(min_length: ...)` instead.', uplevel: 1
91107
min_length = legacy_min_length
@@ -103,7 +119,8 @@ def password(legacy_min_length = NOT_GIVEN, legacy_max_length = NOT_GIVEN, legac
103119
special_characters = legacy_special_characters
104120
end
105121

106-
temp = Lorem.characters(number: min_length)
122+
min_alpha = mix_case ? 2 : 0
123+
temp = Lorem.characters(number: min_length, min_alpha: min_alpha)
107124
diff_length = max_length - min_length
108125

109126
if diff_length.positive?
@@ -112,8 +129,12 @@ def password(legacy_min_length = NOT_GIVEN, legacy_max_length = NOT_GIVEN, legac
112129
end
113130

114131
if mix_case
132+
alpha_count = 0
115133
temp.chars.each_with_index do |char, index|
116-
temp[index] = char.upcase if index.even?
134+
if char =~ /[[:alpha:]]/
135+
temp[index] = char.upcase if alpha_count.even?
136+
alpha_count += 1
137+
end
117138
end
118139
end
119140

@@ -236,7 +257,6 @@ def ip_v6_cidr
236257
"#{ip_v6_address}/#{rand(1..127)}"
237258
end
238259

239-
# rubocop:disable Metrics/ParameterLists
240260
def url(legacy_host = NOT_GIVEN, legacy_path = NOT_GIVEN, legacy_scheme = NOT_GIVEN, host: domain_name, path: "/#{username}", scheme: 'http')
241261
# rubocop:enable Metrics/ParameterLists
242262
if legacy_host != NOT_GIVEN

lib/faker/default/lorem.rb

+17-2
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,28 @@ def character
3131
sample(Types::CHARACTERS)
3232
end
3333

34-
def characters(legacy_number = NOT_GIVEN, number: 255)
34+
##
35+
# Produces a random string of alphanumeric characters
36+
#
37+
# @param [Integer] number
38+
# @param [Integer] min_alpha
39+
# @param [Integer] min_numeric
40+
#
41+
# @return [String]
42+
#
43+
# @example Faker::Lorem.characters #=> "uw1ep04lhs0c4d931n1jmrspprf5wrj85fefue0y7y6m56b6omquh7br7dhqijwlawejpl765nb1716idmp3xnfo85v349pzy2o9rir23y2qhflwr71c1585fnynguiphkjm8p0vktwitcsm16lny7jzp9t4drwav3qmhz4yjq4k04x14gl6p148hulyqioo72tf8nwrxxcclfypz2lc58lsibgfe5w5p0xv95peafjjmm2frkhdc6duoky0aha"
44+
# @example Faker::Lorem.characters(number: 10) #=> "ang9cbhoa8"
45+
# @example Faker::Lorem.characters(number: 10, min_alpha: 4) #=> "ang9cbhoa8"
46+
# @example Faker::Lorem.characters(number: 10, min_alpha: 4, min_numeric: 1) #=> "ang9cbhoa8"
47+
#
48+
# @faker.version 2.1.3
49+
def characters(legacy_number = NOT_GIVEN, number: 255, min_alpha: 0, min_numeric: 0)
3550
if legacy_number != NOT_GIVEN
3651
warn_with_uplevel 'Passing `number` with the 1st argument of `Lorem.characters` is deprecated. Use keyword argument like `Lorem.characters(number: ...)` instead.', uplevel: 1
3752
number = legacy_number
3853
end
3954

40-
Alphanumeric.alphanumeric(number: number)
55+
Alphanumeric.alphanumeric(number: number, min_alpha: min_alpha, min_numeric: min_numeric)
4156
end
4257

4358
def multibyte

test/faker/default/test_alphanum.rb

+34-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,42 @@ def setup
88
end
99

1010
def alpha
11-
assert @tester.alpha(characters: 5).match(/[a-z]{5}/)
11+
assert @tester.alpha(number: 5).match(/[a-z]{5}/)
1212
end
1313

1414
def alphanum
15-
assert @tester.alphanumeric(characters: 5).match(/[a-z0-9]{5}/)
15+
assert @tester.alphanumeric(number: 5).match(/[a-z0-9]{5}/)
16+
end
17+
18+
def test_alphanumeric_invalid_min_alpha
19+
assert_raise ArgumentError do
20+
@tester.alphanumeric(number: 5, min_alpha: -1)
21+
end
22+
end
23+
24+
def test_alphanumeric_invalid_min_numeric
25+
assert_raise ArgumentError do
26+
@tester.alphanumeric(number: 5, min_numeric: -1)
27+
end
28+
end
29+
30+
def test_alphanumeric_with_invalid_mins
31+
assert_raise ArgumentError do
32+
@tester.alphanumeric(number: 5, min_numeric: 4, min_alpha: 3)
33+
end
34+
end
35+
36+
def test_alphanumeric_with_min_alpha
37+
letters = @tester.alphanumeric(number: 5, min_alpha: 2).split('').map do |char|
38+
char =~ /[[:alpha:]]/
39+
end
40+
assert letters.compact.size >= 2
41+
end
42+
43+
def test_alphanumeric_with_min_numeric
44+
numbers = @tester.alphanumeric(number: 5, min_numeric: 4).split('').map do |char|
45+
char =~ /[[:digit:]]/
46+
end
47+
assert numbers.compact.size >= 4
1648
end
1749
end

test/faker/default/test_faker_internet.rb

+12-3
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ def test_password
9898

9999
def test_password_with_integer_arg
100100
(1..32).each do |min_length|
101-
assert @tester.password(min_length: min_length).length >= min_length
101+
assert @tester.password(min_length: min_length, mix_case: false).length >= min_length
102102
end
103103
end
104104

105105
def test_password_max_with_integer_arg
106106
(1..32).each do |min_length|
107107
max_length = min_length + 4
108-
assert @tester.password(min_length: min_length, max_length: max_length).length <= max_length
108+
assert @tester.password(min_length: min_length, max_length: max_length, mix_case: false).length <= max_length
109109
end
110110
end
111111

@@ -118,7 +118,16 @@ def test_password_could_achieve_max_length
118118
end
119119

120120
def test_password_with_mixed_case
121-
assert @tester.password.match(/[A-Z]+/)
121+
password = @tester.password
122+
upcase_count = 0
123+
downcase_count = 0
124+
password.chars.each do |char|
125+
if char =~ /[[:alpha:]]/
126+
char.capitalize == char ? upcase_count += 1 : downcase_count += 1
127+
end
128+
end
129+
assert upcase_count >= 1
130+
assert downcase_count >= 1
122131
end
123132

124133
def test_password_without_mixed_case

0 commit comments

Comments
 (0)