diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 94950b0..172ddf6 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 7e07b1d..e3e95d8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/spec/models/user_password_spec.rb b/spec/models/user_password_spec.rb new file mode 100644 index 0000000..af0bef1 --- /dev/null +++ b/spec/models/user_password_spec.rb @@ -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