diff --git a/Appraisals b/Appraisals index 48021174..8e976665 100644 --- a/Appraisals +++ b/Appraisals @@ -6,6 +6,7 @@ if RUBY_VERSION < '2.0' appraise '3.1' do gem 'rails', '~> 3.1.8' + gem 'bcrypt-ruby', '~> 3.0.0' gem 'jquery-rails' gem 'sass-rails' gem 'strong_parameters' @@ -14,6 +15,7 @@ end appraise '3.2' do gem 'rails', '~> 3.2.13' + gem 'bcrypt-ruby', '~> 3.0.0' gem 'jquery-rails' gem 'sass-rails' gem 'strong_parameters' @@ -21,6 +23,7 @@ end appraise '4.0' do gem 'rails', '4.0.0' + gem 'bcrypt-ruby', '~> 3.0.0' #FIXME: This should be ~> 3.1.0 for Rails 4.0 gem 'jquery-rails' gem 'sass-rails', '~> 4.0.0' diff --git a/README.md b/README.md index c9abfbce..81e2fdb5 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ describe User do it { should allow_value("a@b.com").for(:email) } it { should ensure_inclusion_of(:age).in_range(1..100) } it { should_not allow_mass_assignment_of(:password) } + it { should have_secure_password } end ``` diff --git a/gemfiles/3.1.gemfile b/gemfiles/3.1.gemfile index 39af8c39..531aca24 100644 --- a/gemfiles/3.1.gemfile +++ b/gemfiles/3.1.gemfile @@ -10,6 +10,7 @@ gem "jdbc-sqlite3", :platform=>:jruby gem "jruby-openssl", :platform=>:jruby gem "therubyrhino", :platform=>:jruby gem "rails", "~> 3.1.8" +gem "bcrypt-ruby", "~> 3.0.0" gem "jquery-rails" gem "sass-rails" gem "strong_parameters" diff --git a/gemfiles/3.1.gemfile.lock b/gemfiles/3.1.gemfile.lock index e9c0ab66..4c5ce9ca 100644 --- a/gemfiles/3.1.gemfile.lock +++ b/gemfiles/3.1.gemfile.lock @@ -43,6 +43,7 @@ GEM childprocess (~> 0.3.6) cucumber (>= 1.1.1) rspec-expectations (>= 2.7.0) + bcrypt-ruby (3.0.1) bourne (1.4.0) mocha (~> 0.13.2) builder (3.0.4) @@ -143,6 +144,7 @@ DEPENDENCIES activerecord-jdbcsqlite3-adapter appraisal (~> 0.4) aruba + bcrypt-ruby (~> 3.0.0) bourne (~> 1.3) bundler (~> 1.1) cucumber (~> 1.1) diff --git a/gemfiles/3.2.gemfile b/gemfiles/3.2.gemfile index 37dbc69e..be4a32c6 100644 --- a/gemfiles/3.2.gemfile +++ b/gemfiles/3.2.gemfile @@ -10,6 +10,7 @@ gem "jdbc-sqlite3", :platform=>:jruby gem "jruby-openssl", :platform=>:jruby gem "therubyrhino", :platform=>:jruby gem "rails", "~> 3.2.13" +gem "bcrypt-ruby", "~> 3.0.0" gem "jquery-rails" gem "sass-rails" gem "strong_parameters" diff --git a/gemfiles/3.2.gemfile.lock b/gemfiles/3.2.gemfile.lock index bfdf7ec1..754ad4f2 100644 --- a/gemfiles/3.2.gemfile.lock +++ b/gemfiles/3.2.gemfile.lock @@ -42,6 +42,7 @@ GEM childprocess (~> 0.3.6) cucumber (>= 1.1.1) rspec-expectations (>= 2.7.0) + bcrypt-ruby (3.0.1) bourne (1.4.0) mocha (~> 0.13.2) builder (3.0.4) @@ -141,6 +142,7 @@ DEPENDENCIES activerecord-jdbcsqlite3-adapter appraisal (~> 0.4) aruba + bcrypt-ruby (~> 3.0.0) bourne (~> 1.3) bundler (~> 1.1) cucumber (~> 1.1) diff --git a/gemfiles/4.0.gemfile b/gemfiles/4.0.gemfile index 154bfbe3..9fefc4a9 100644 --- a/gemfiles/4.0.gemfile +++ b/gemfiles/4.0.gemfile @@ -10,6 +10,7 @@ gem "jdbc-sqlite3", :platform=>:jruby gem "jruby-openssl", :platform=>:jruby gem "therubyrhino", :platform=>:jruby gem "rails", "4.0.0" +gem "bcrypt-ruby", "~> 3.0.0" gem "jquery-rails" gem "sass-rails", "~> 4.0.0" gem "protected_attributes" diff --git a/gemfiles/4.0.gemfile.lock b/gemfiles/4.0.gemfile.lock index 8ab74400..7219b102 100644 --- a/gemfiles/4.0.gemfile.lock +++ b/gemfiles/4.0.gemfile.lock @@ -40,6 +40,7 @@ GEM cucumber (>= 1.1.1) rspec-expectations (>= 2.7.0) atomic (1.1.13) + bcrypt-ruby (3.0.1) bourne (1.5.0) mocha (>= 0.13.2, < 0.15) builder (3.1.4) @@ -135,6 +136,7 @@ DEPENDENCIES activerecord-jdbcsqlite3-adapter appraisal (~> 0.4) aruba + bcrypt-ruby (~> 3.0.0) bourne (~> 1.3) bundler (~> 1.1) cucumber (~> 1.1) diff --git a/lib/shoulda/matchers/active_model.rb b/lib/shoulda/matchers/active_model.rb index f3ccdf02..fec6ed04 100644 --- a/lib/shoulda/matchers/active_model.rb +++ b/lib/shoulda/matchers/active_model.rb @@ -17,6 +17,7 @@ require 'shoulda/matchers/active_model/validate_confirmation_of_matcher' require 'shoulda/matchers/active_model/validate_numericality_of_matcher' require 'shoulda/matchers/active_model/allow_mass_assignment_of_matcher' require 'shoulda/matchers/active_model/errors' +require 'shoulda/matchers/active_model/have_secure_password_matcher' module Shoulda diff --git a/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb b/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb new file mode 100644 index 00000000..0a59c195 --- /dev/null +++ b/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb @@ -0,0 +1,71 @@ +module Shoulda # :nodoc: + module Matchers + module ActiveModel # :nodoc: + + # Ensures that the model exhibits behavior added by has_secure_password. + # + # Example: + # it { should have_secure_password } + def have_secure_password + HaveSecurePasswordMatcher.new + end + + class HaveSecurePasswordMatcher # :nodoc: + attr_reader :failure_message_for_should + + CORRECT_PASSWORD = "aBcDe12345" + INCORRECT_PASSWORD = "password" + + EXPECTED_METHODS = [ + :authenticate, + :password=, + :password_confirmation=, + :password_digest, + :password_digest=, + ] + + MESSAGES = { + authenticated_incorrect_password: "expected %{subject} to not authenticate an incorrect password", + did_not_authenticate_correct_password: "expected %{subject} to authenticate the correct password", + method_not_found: "expected %{subject} to respond to %{methods}" + } + + def description + "have a secure password" + end + + def matches?(subject) + @subject = subject + + if failure = validate + key, params = failure + @failure_message_for_should = MESSAGES[key] % { subject: subject.class }.merge(params) + end + + failure.nil? + end + + private + + attr_reader :subject + + def validate + missing_methods = EXPECTED_METHODS.select {|m| !subject.respond_to?(m) } + + if missing_methods.present? + [:method_not_found, { methods: missing_methods.to_sentence }] + else + subject.password = CORRECT_PASSWORD + subject.password_confirmation = CORRECT_PASSWORD + + if not subject.authenticate(CORRECT_PASSWORD) + [:did_not_authenticate_correct_password, {}] + elsif subject.authenticate(INCORRECT_PASSWORD) + [:authenticated_incorrect_password, {}] + end + end + end + end + end + end +end diff --git a/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb b/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb new file mode 100644 index 00000000..d9d187db --- /dev/null +++ b/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Shoulda::Matchers::ActiveModel::HaveSecurePasswordMatcher do + if active_model_3_1? + it 'matches when the subject configures has_secure_password with default options' do + working_model = define_model(:example, password_digest: :string) { has_secure_password } + expect(working_model.new).to have_secure_password + end + + it 'does not match when the subject does not authenticate a password' do + no_secure_password = define_model(:example) + expect(no_secure_password.new).not_to have_secure_password + end + + it 'does not match when the subject is missing the password_digest attribute' do + no_digest_column = define_model(:example) { has_secure_password } + expect(no_digest_column.new).not_to have_secure_password + end + end +end