Strip out more inline javascript code. Encrypt backup codes and treat the backup codes attribute as a json array
This commit is contained in:
@@ -230,4 +230,132 @@ class UserTest < ActiveSupport::TestCase
|
||||
assert_not user.valid?
|
||||
assert_includes user.errors[:password], "is too short (minimum is 8 characters)"
|
||||
end
|
||||
|
||||
# Backup codes tests
|
||||
test "generate_backup_codes returns 10 plain codes and stores BCrypt hashes" do
|
||||
user = User.create!(
|
||||
email_address: "test@example.com",
|
||||
password: "password123"
|
||||
)
|
||||
|
||||
# Generate backup codes
|
||||
plain_codes = user.send(:generate_backup_codes)
|
||||
|
||||
# Should return 10 plain codes
|
||||
assert_equal 10, plain_codes.length
|
||||
assert_kind_of Array, plain_codes
|
||||
|
||||
# All codes should be 8 characters, alphanumeric, uppercase
|
||||
plain_codes.each do |code|
|
||||
assert_equal 8, code.length
|
||||
assert_match(/\A[A-Z0-9]+\z/, code)
|
||||
end
|
||||
|
||||
# Save user to persist the backup codes
|
||||
user.save!
|
||||
|
||||
# Reload user from database to check stored values
|
||||
user.reload
|
||||
stored_hashes = user.backup_codes || []
|
||||
|
||||
# Should store 10 BCrypt hashes
|
||||
assert_equal 10, stored_hashes.length
|
||||
stored_hashes.each do |hash|
|
||||
assert hash.start_with?('$2a$'), "Should be BCrypt hash"
|
||||
end
|
||||
|
||||
# Verify each plain code matches its corresponding hash
|
||||
plain_codes.each_with_index do |code, index|
|
||||
assert BCrypt::Password.new(stored_hashes[index]) == code, "Plain code should match stored hash"
|
||||
end
|
||||
end
|
||||
|
||||
test "verify_backup_code works with BCrypt hashes" do
|
||||
user = User.create!(
|
||||
email_address: "test@example.com",
|
||||
password: "password123"
|
||||
)
|
||||
|
||||
# Generate backup codes using the new flow (simulate what happens in controller)
|
||||
plain_codes = user.send(:generate_backup_codes)
|
||||
user.save!
|
||||
user.reload
|
||||
|
||||
# Should successfully verify a valid backup code
|
||||
assert user.verify_backup_code(plain_codes.first), "Should verify first backup code"
|
||||
|
||||
# Code should be deleted after use (single-use property)
|
||||
user.reload
|
||||
assert user.verify_backup_code(plain_codes.first) == false, "Used code should not be verifiable again"
|
||||
|
||||
# Should still verify other unused codes
|
||||
assert user.verify_backup_code(plain_codes.second), "Should verify second backup code"
|
||||
end
|
||||
|
||||
test "verify_backup_code returns false for invalid codes" do
|
||||
user = User.create!(
|
||||
email_address: "test@example.com",
|
||||
password: "password123"
|
||||
)
|
||||
|
||||
# Generate backup codes
|
||||
plain_codes = user.send(:generate_backup_codes)
|
||||
user.save!
|
||||
user.reload
|
||||
|
||||
# Should fail for invalid codes
|
||||
assert_not user.verify_backup_code("INVALID123"), "Should fail for invalid code"
|
||||
assert_not user.verify_backup_code(""), "Should fail for empty code"
|
||||
assert_not user.verify_backup_code(plain_codes.first + "X"), "Should fail for modified valid code"
|
||||
end
|
||||
|
||||
test "verify_backup_code returns false when no backup codes exist" do
|
||||
user = User.create!(
|
||||
email_address: "test@example.com",
|
||||
password: "password123"
|
||||
)
|
||||
|
||||
# Should return false when user has no backup codes
|
||||
assert_not user.verify_backup_code("ANYCODE123"), "Should fail when no backup codes exist"
|
||||
end
|
||||
|
||||
test "verify_backup_code respects rate limiting" do
|
||||
# Temporarily use memory store for this test
|
||||
original_cache_store = Rails.cache
|
||||
Rails.cache = ActiveSupport::Cache::MemoryStore.new
|
||||
|
||||
user = User.create!(
|
||||
email_address: "test@example.com",
|
||||
password: "password123"
|
||||
)
|
||||
|
||||
# Generate backup codes
|
||||
plain_codes = user.send(:generate_backup_codes)
|
||||
user.save!
|
||||
user.reload
|
||||
|
||||
# Make 5 failed attempts to trigger rate limit
|
||||
5.times do |i|
|
||||
result = user.verify_backup_code("INVALID123")
|
||||
assert_not result, "Failed attempt #{i+1} should return false"
|
||||
end
|
||||
|
||||
# Check that the cache is tracking attempts
|
||||
attempts = Rails.cache.read("backup_code_failed_attempts_#{user.id}") || 0
|
||||
assert_equal 5, attempts, "Should have 5 failed attempts tracked"
|
||||
|
||||
# 6th attempt should be rate limited (both valid and invalid codes should fail)
|
||||
assert_not user.verify_backup_code(plain_codes.first), "Valid code should be rate limited after 5 failed attempts"
|
||||
assert_not user.verify_backup_code("INVALID123"), "Invalid code should also be rate limited"
|
||||
|
||||
# Valid code should still work if we clear the rate limit
|
||||
Rails.cache.delete("backup_code_failed_attempts_#{user.id}")
|
||||
assert user.verify_backup_code(plain_codes.first), "Should work after clearing rate limit"
|
||||
|
||||
# Restore original cache store
|
||||
Rails.cache = original_cache_store
|
||||
end
|
||||
|
||||
# Note: parsed_backup_codes method and legacy tests removed
|
||||
# All users now use BCrypt hashes stored in JSON column
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user