Rails 5 is now out and I am upgrading my basic login/CRUD app to use the new framework. Things were pretty straightforward, except for one rspec test that is failing. I tried stepping through the code with pry, but finding the problem was beyond me. Any pointers would be appreciated.
This is the error:
Failures:
1) User email rejects email addresses with improper format
Failure/Error: expect(user).not_to be_valid
ArgumentError:
wrong number of arguments (given 0, expected 1)
# ./spec/models/problem_test_user_spec.rb:13:in `block (4 levels) in <top (required)>'
# ./spec/models/problem_test_user_spec.rb:11:in `each'
# ./spec/models/problem_test_user_spec.rb:11:in `block (3 levels) in <top (required)>'
Here is the pared-down test:
1 require 'rails_helper'
2 require 'devise'
3
4 RSpec.describe User, type: :model do
5 let (:user) { FactoryGirl.create :user }
6
7 describe "email" do
8 it "rejects email addresses with improper format" do
9 invalid_addresses = %w[ user@example,com user_at_foo.org user.name@example. ]
10
11 invalid_addresses.each do |addr|
12 user.email = addr
13 expect(user).not_to be_valid
14 end
15 end
16 end
17 end
Here is my model:
class User < ApplicationRecord
devise :database_authenticatable, :validatable
validates :email, length: { maximum: 254 } # max length per RFC 3696, errata ID 1690
end
Here is my factory:
FactoryGirl.define do
factory :user do
name { Faker::Name.name }
tone_name { "#{Faker::Company.name} Tone" }
email { Faker::Internet.email }
password SOME_PASSWORD
password_confirmation SOME_PASSWORD
admin false
end
end
While the test was failing, the app under Rails 5 was running fine in my development environment, but it allowed an invalid password of the form name@example,com where the Rails 4.2 version both passes the rspec test and catches this invalid password with an error.
While researching this, I learned that Devise changed the default email_regexp to a less strict expression in v4.1. My test is actually failing because an email address that used to be identified as illegal now is accepted. The ArgumentError is not in my code. I believe it is an error either in Devise or in Rspec. If I put a binding.pry in my test, then a simple #inspect on my model causes the following:
12: invalid_addresses.each do |addr|
13: user.email = addr
14: binding.pry
=> 15: expect(user).not_to be_valid
16: end
17: end
18: end
19: end
[1] pry(#<RSpec::ExampleGroups::User::Email>)> user.inspect
ArgumentError: wrong number of arguments (given 0, expected 1)
from /Users/tarsa/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/devise-4.2.0/lib/devise/models/database_authenticatable.rb:146:in `password_digest'
The Devise code may not be at fault, I suspect some interaction with ActiveRecord is going on, but I am at the limit of my debugging skills at this point.
I can workaround the problem by configuring Devise to use the 4.0.3 version of email_regexp. If all the password strings are recognized as invalid, then the bug, wherever it is, does not get tickled.
I am still interested in a root cause, but I am no longer blocked.
Can you please provide a sample application that reproduces the error?
I stripped my app down to the bare essentials. Clone this branch, install postgresql, run bundle install, rake db:setup and then rspec should show the problem described in my issue report.
https://github.com/gltarsa/Motleytones/tree/minimal-test-app-for-devise-rails5-problem
Thanks!
@gltarsa Thanks!
I dug through the whole stack trace (using RSpec -b switch) and found the root cause here:
$ bundle exec rspec spec/models/problem_test_user_spec.rb -b
F
Failures:
1) User email rejects email addresses with improper format
Failure/Error: expect(user).not_to be_valid
ArgumentError:
wrong number of arguments (given 0, expected 1)
# /opt/rubies/2.3.0/lib/ruby/gems/2.3.0/gems/devise-4.2.0/lib/devise/models/database_authenticatable.rb:146:in `password_digest'
# /opt/rubies/2.3.0/lib/ruby/gems/2.3.0/gems/activemodel-5.0.0/lib/active_model/serialization.rb:135:in `block in serializable_hash'
# /opt/rubies/2.3.0/lib/ruby/gems/2.3.0/gems/activemodel-5.0.0/lib/active_model/serialization.rb:135:in `each'
# /opt/rubies/2.3.0/lib/ruby/gems/2.3.0/gems/activemodel-5.0.0/lib/active_model/serialization.rb:135:in `serializable_hash'
# /opt/rubies/2.3.0/lib/ruby/gems/2.3.0/gems/activerecord-5.0.0/lib/active_record/serialization.rb:17:in `serializable_hash'
# /opt/rubies/2.3.0/lib/ruby/gems/2.3.0/gems/devise-4.2.0/lib/devise/models/authenticatable.rb:114:in `serializable_hash'
# /opt/rubies/2.3.0/lib/ruby/gems/2.3.0/gems/devise-4.2.0/lib/devise/models/authenticatable.rb:120:in `inspect'
The users table has a password_digest column that is conflicting with Devise's password_digest(password) method, and Rails serializable_hash is trying to call the method as an attribute instead (thus making the (given 0, expected 1) bad method invocation).
This is happening now because Devise 4.2.0 now uses serializable_hash on inspect to avoid serializing sensitive data, so when the RSpec matcher tries to do user.inspect it fails. But even without upgrading Devise, calling user.as_json / user.serializable_hash on your app fails because of the attribute/method conflict.
I think in your case the best option is to remove the password_digest column from the users table as Devise uses the encrypted_password instead.
Hope this helps - I'm closing this issue as it isn't an issue with Devise, but let me know if you have any other questions.
Thank you, this was illuminating in a couple of dimensions. I was on a complete tangent for this. Much obliged.
Most helpful comment
@gltarsa Thanks!
I dug through the whole stack trace (using RSpec
-bswitch) and found the root cause here:The
userstable has apassword_digestcolumn that is conflicting with Devise'spassword_digest(password)method, and Railsserializable_hashis trying to call the method as an attribute instead (thus making the(given 0, expected 1)bad method invocation).This is happening now because Devise
4.2.0now usesserializable_hashoninspectto avoid serializing sensitive data, so when the RSpec matcher tries to douser.inspectit fails. But even without upgrading Devise, callinguser.as_json/user.serializable_hashon your app fails because of the attribute/method conflict.I think in your case the best option is to remove the
password_digestcolumn from theuserstable as Devise uses theencrypted_passwordinstead.Hope this helps - I'm closing this issue as it isn't an issue with Devise, but let me know if you have any other questions.