Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensure generated passwords have correct characters when mix_case & special_characters enabled #2533

Merged
merged 1 commit into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/default/internet.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ Faker::Internet.username(specifier: 5..8)
Faker::Internet.username(specifier: 8)

# Keyword arguments: min_length, max_length, mix_case, special_characters
# Default configuration is mix_case: true && special_characters: false
Zeragamba marked this conversation as resolved.
Show resolved Hide resolved
Faker::Internet.password #=> "Vg5mSvY1UeRg7"
Faker::Internet.password(min_length: 8) #=> "YfGjIk0hGzDqS0"
Faker::Internet.password(min_length: 10, max_length: 20) #=> "EoC9ShWd1hWq4vBgFw"
# min_length must be at least 1 if mix_case: false && special_characters: true
Faker::Internet.password(min_length: 10, max_length: 20, mix_case: false, special_characters: true) #=> "$1109mw31h8359jm0!oo"
# min_length must be at least 2 if mix_case: true && special_characters: false
Faker::Internet.password(min_length: 10, max_length: 20, mix_case: true) #=> "3k5qS15aNmG"
# min_length must be at least 3 if mix_case: true && special_characters: true
Faker::Internet.password(min_length: 10, max_length: 20, mix_case: true, special_characters: true) #=> "*%NkOnJsH4"

# Keyword arguments: subdomain, domain
Expand Down
55 changes: 33 additions & 22 deletions lib/faker/default/internet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,37 +147,48 @@ def username(specifier: nil, separators: %w[. _])
#
# @faker.version 2.1.3
def password(min_length: 8, max_length: 16, mix_case: true, special_characters: false)
raise ArgumentError, 'Password of length 1 can not have both mixed case and special characters' if min_length <= 1 && mix_case && special_characters
raise ArgumentError, 'max_length must be more than min_length' if max_length < min_length
stefannibrasil marked this conversation as resolved.
Show resolved Hide resolved

min_alpha = mix_case && min_length > 1 ? 2 : 0
temp = Lorem.characters(number: min_length, min_alpha: min_alpha)
diff_length = max_length - min_length

if diff_length.positive?
diff_rand = rand(diff_length + 1)
temp += Lorem.characters(number: diff_rand)
end
character_types = []
required_min_length = 0

if mix_case
alpha_count = 0
temp.chars.each_with_index do |char, index|
if char =~ /[[:alpha:]]/
temp[index] = char.upcase if alpha_count.even?
alpha_count += 1
end
end
character_types << :mix_case
required_min_length += 2
end

if special_characters
chars = %w[! @ # $ % ^ & *]
rand(1..min_length).times do |i|
temp[i] = chars[rand(chars.length)]
end
character_types << :special_characters
required_min_length += 1
end

raise ArgumentError, "min_length should be at least #{required_min_length} to enable #{character_types.join(', ')} configuration" if min_length < required_min_length

target_length = rand(min_length..max_length)

password = []
character_bag = []

# use lower_chars by default and add upper_chars if mix_case
lower_chars = ('a'..'z').to_a
password << lower_chars[rand(lower_chars.count - 1)]
character_bag += lower_chars

if character_types.include?(:mix_case)
upper_chars = ('A'..'Z').to_a
password << upper_chars[rand(upper_chars.count - 1)]
character_bag += upper_chars
end

if character_types.include?(:special_characters)
special_chars = %w[! @ # $ % ^ & *]
password << special_chars[rand(special_chars.count - 1)]
character_bag += special_chars
end

temp[rand(temp.size - 1)] = Lorem.characters(number: 1, min_alpha: 1).upcase if mix_case && special_characters && !temp.match(/[A-z]+/)
password << character_bag[rand(character_bag.count - 1)] while password.length < target_length

temp
shuffle(password).join
end

##
Expand Down
43 changes: 34 additions & 9 deletions test/faker/default/test_faker_internet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ def test_password

def test_password_with_integer_arg
(1..32).each do |min_length|
assert @tester.password(min_length: min_length, mix_case: false).length >= min_length
max_length = min_length + 1

assert_includes (min_length..max_length), @tester.password(min_length: min_length, max_length: max_length, mix_case: false).length
end
end

Expand Down Expand Up @@ -157,9 +159,8 @@ def test_password_with_mixed_case
assert downcase_count >= 1
end

def test_password_with_min_length_eq_1
min_length = 1
password = @tester.password(min_length: min_length)
def test_password_with_min_length_eq_1_without_mix_case
password = @tester.password(min_length: 1, mix_case: false)

assert_match(/\w+/, password)
end
Expand All @@ -173,6 +174,12 @@ def test_password_with_min_length_and_max_length
assert_includes (min_length..max_length), password.size, 'Password size is incorrect'
end

def test_password_with_max_length_less_than_min_length
assert_raise 'max_length must be more than min_length' do
@tester.password(min_length: 8, max_length: 4)
end
end

def test_password_without_mixed_case
assert_match(/[^A-Z]+/, @tester.password(min_length: 8, max_length: 12, mix_case: false))
end
Expand All @@ -194,25 +201,43 @@ def test_password_with_special_chars_and_mixed_case
end
end

def test_password_with_special_chars_and_mixed_case_on_2chars_password
def test_deterministic_password_with_special_chars_and_mixed_case
deterministically_verify -> { @tester.password(min_length: 4, max_length: 6, mix_case: true, special_characters: true) }, depth: 4 do |password|
assert_match(/[!@#$%\^&*]+/, password)
assert_match(/[A-z]+/, password)
end
end

def test_password_with_special_chars_and_mixed_case_on_3chars_password
16.times do
password = @tester.password(min_length: 2, max_length: 6, mix_case: true, special_characters: true)
password = @tester.password(min_length: 3, max_length: 6, mix_case: true, special_characters: true)

assert_match(/[!@#$%\^&*]+/, password)
assert_match(/[A-z]+/, password)
end
end

def test_password_with_incompatible_min_length_and_requirements
assert_raise ArgumentError do
def test_password_with_invalid_min_length_for_mix_case_and_special_characters
assert_raise_message 'min_length should be at least 3 to enable mix_case, special_characters configuration' do
@tester.password(min_length: 1, mix_case: true, special_characters: true)
end
end

def test_password_with_compatible_min_length_and_requirements
assert_nothing_raised do
[false, true].each do |value|
@tester.password(min_length: 1, mix_case: value, special_characters: !value)
min_length = value ? 2 : 1
@tester.password(min_length: min_length, mix_case: value, special_characters: !value)
end
end
end

def test_deterministic_password_with_compatible_min_length_and_requirements
[false, true].each do |value|
min_length = value ? 2 : 1

deterministically_verify -> { @tester.password(min_length: min_length, mix_case: value, special_characters: !value) }, depth: 4 do |subject|
assert_nothing_raised { subject }
end
end
end
Expand Down