Skip to content
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
6 changes: 4 additions & 2 deletions app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,12 @@ def handle_internal_error(exception)

# Format validation errors into structured array
def format_validation_errors(errors)
errors.messages.map do |field, messages|
errors.attribute_names.map do |field|
field_errors = errors.where(field)
{
field: field,
messages: messages,
codes: field_errors.filter_map { |e| "#{field}.#{e.type}" if e.type.is_a?(Symbol) },
messages: field_errors.map(&:message),
full_messages: errors.full_messages_for(field)
}
end
Expand Down
19 changes: 4 additions & 15 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -258,20 +258,9 @@ def generate_sso_link
def password_complexity
return if password.blank?

unless password.match?(/[a-z]/)
errors.add(:password, 'must include at least one lowercase letter')
end

unless password.match?(/[A-Z]/)
errors.add(:password, 'must include at least one uppercase letter')
end

unless password.match?(/\d/)
errors.add(:password, 'must include at least one number')
end

unless password.match?(PASSWORD_SPECIAL_CHAR_REGEX)
errors.add(:password, 'must include at least one special character')
end
errors.add(:password, :missing_lowercase, message: 'must include at least one lowercase letter') unless password.match?(/[a-z]/)
errors.add(:password, :missing_uppercase, message: 'must include at least one uppercase letter') unless password.match?(/[A-Z]/)
errors.add(:password, :missing_number, message: 'must include at least one number') unless password.match?(/\d/)
errors.add(:password, :missing_special_char, message: 'must include at least one special character') unless password.match?(PASSWORD_SPECIAL_CHAR_REGEX)
end
end
62 changes: 62 additions & 0 deletions spec/models/user_password_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe User, type: :model do
describe '#password_complexity' do
let(:base_attrs) { { email: 'test@example.com', name: 'Test User' } }

subject(:user) { User.new(base_attrs.merge(password: password)) }

context 'when password is missing a special character' do
let(:password) { 'NoSpecial1Abc' }

it 'adds :missing_special_char error code' do
user.valid?
error = user.errors.where(:password).find { |e| e.type == :missing_special_char }
expect(error).to be_present
end
end

context 'when password is missing an uppercase letter' do
let(:password) { 'nouppercase1!' }

it 'adds :missing_uppercase error code' do
user.valid?
error = user.errors.where(:password).find { |e| e.type == :missing_uppercase }
expect(error).to be_present
end
end

context 'when password is missing a lowercase letter' do
let(:password) { 'NOLOWER1!' }

it 'adds :missing_lowercase error code' do
user.valid?
error = user.errors.where(:password).find { |e| e.type == :missing_lowercase }
expect(error).to be_present
end
end

context 'when password is missing a number' do
let(:password) { 'NoNumbers!Ab' }

it 'adds :missing_number error code' do
user.valid?
error = user.errors.where(:password).find { |e| e.type == :missing_number }
expect(error).to be_present
end
end

context 'when password meets all complexity requirements' do
let(:password) { 'Valid1!Pass' }

it 'has no complexity error codes' do
user.valid?
complexity_types = %i[missing_lowercase missing_uppercase missing_number missing_special_char]
complexity_errors = user.errors.where(:password).select { |e| complexity_types.include?(e.type) }
expect(complexity_errors).to be_empty
end
end
end
end