I have the following Rspec test with the following output for my User model in a Rails API I'm building:
RSpec.describe User, type: :model do
let(:michael) { User.new(email: "michael#email.com", password: "Password1", password_confirmation: "Password1") }
it 'is not valid without a password' do
michael.password = ''
expect(michael).not_to be_valid, "Expect User to have a Password"
end
end
1) User basic checks is not valid without a password
Failure/Error: expect(michael).not_to be_valid, "Expect User to have a Password"
Expect User to have a Password
# ./spec/models/user_spec.rb:19:in `block (3 levels) in <main>'
However, for some reason this test just fails, even with the following on my User model
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
validates :password, presence: true
validates :password,
format: { with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{8,}\Z/, message: "Password must be at least 8 characters long, contain at least 1 uppercase and 1 lowercase letter and 1 number" },
confirmation: true,
on: :create
end
The above validation works fine, as you can see here:
➜ rails c
Running via Spring preloader in process 2774
Loading development environment (Rails 6.0.3.1)
irb(main):001:0> new = User.new
irb(main):002:0> new.email = "testing#testing.com"
irb(main):003:0> new.save
(0.2ms) BEGIN
User Exists? (0.6ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "testing#testing.com"], ["LIMIT", 1]]
(0.2ms) ROLLBACK
=> false
irb(main):004:0> new.errors
=> #<ActiveModel::Errors:0x00007f743848dfb0 #base=#<User id: nil, email: "testing#testing.com", password_digest: nil, created_at: nil, updated_at: nil, admin: false>, #messages={:password=>["can't be blank", "can't be blank", "Password must be at least 8 characters long, contain at least 1 uppercase and 1 lowercase letter and 1 number"]}, #details={:password=>[{:error=>:blank}, {:error=>:blank}, {:error=>:invalid, :value=>nil}]}>
What exactly am I doing wrong here for this test to fail? It's a painfully simple one, and all the other ones work as expected except for the password test.
First, in your tests password = '' is a valid one because for Ruby, '' is something. You have to add this to your validation:
validates :password, presence: true, allow_blank: false
Also, take a look, your password validation is for on: :create and in your spec you are not creating it.
Related
The following test:
test "create with mobile" do
assert #user_mobile.valid?, #user_mobile.errors.full_messages.inspect
puts assert #user_mobile.valid?
assert_difference('User.count') do
post users_url, params: { user: { mobile: #user_mobile.mobile, name_first: #user_mobile.name_first, name_last: #user_mobile.name_last, nation_id: #user_mobile.nation_id, mobile_nation_id: #user_mobile.mobile_nation_id, password: '11111111', password_confirmation: '11111111', idiom_id: #user_mobile.idiom_id } }
end
end
passes the first assertion and is returning to the console
true
F
Failure:
UsersControllerTest#test_create_with_mobile [/Volumes/SanJuanT/r/market/test/controllers/users_controller_test.rb:29]:
"User.count" didn't change by 1.
Thus, the non creation of the record is not due to a validation error.
How can one trace the point in the process where devise fails to create the record?
Update
Logs indicate a series of unpermitted parameters (all of the above, save password related ones).
Processing by Users::RegistrationsController#create as HTML
Parameters: {"user"=>{"mobile"=>"3331112200", "name_first"=>"MyString", "name_last"=>"MyString", "nation_id"=>"1", "mobile_nation_id"=>"1",
yet the registrations_controller, has a protected block for sign_up (and update) strong parameters:
def configure_sign_up_params
devise_parameter_sanitizer.permit({ roleshopusers: [:role_id, :shop_id, :user_id] }, :email, :password, :password_confirmation, :mobile, :mobile_nation_id, [...])
adding those parameters to the alternate lazy way, also suggested in devise ReadMe
before_action :configure_permitted_parameters, if: :devise_controller?
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:email, :password, :password_confirmation, :mobile [...]
initiates a process, but rolls back
User Exists? [...]
ROLLBACK TO SAVEPOINT active_record_1
as this is a test, I do not imaging that the test would consider a fixture as already existing.
** Solution (possible) **
Changing the reference to a string that was not referenced in the fixtures.
post users_url, params: { user: { mobile: ' 3331 91 220 0 ',
log
User Create (0.5ms)[0m [1m[32mINSERT INTO "users" ("encrypted_password", "mobile", "mobile_nation_id", "login_name", "nation_id"[...]
[1m[36mTRANSACTION (0.1ms)[0m [1m[35mRELEASE SAVEPOINT active_record_1
As the model had
validates :mobile, uniqueness: { scope: :nation_id, message: (I18n.t('user.define_one_mobile_per_nation')) }, if: -> { mobile.present? }
Given devise has authentication_keys that must be unique, recalling one of the fixtures is forcing the rollback.
I am reading Michael Hartl's Ruby On Rails Tutorial (3rd). In chapter 9, there's an example showing us how to update the user info. I got confused by the allow_nil attached here. I simplified the code as below:
class User < ActiveRecord::Base
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
end
myuser=User.new(name: "Foo_bar",email: "test#example.com", password: "123456", password_confirmation: "123456")
myuser.save
myuser=User.find(myuser.id)
myuser.update_attributes(name: "Bar_Foo",email: "test#example.com", password: "", password_confirmation: "") # It will succeed! But WHY?
I understand the allow_nil: true skips the validation when the value being validated is nil. Nevertheless obviously, password: "" is not a nil value. How can allow_nil: true allows an empty string?
This behaviour is due to has_secure_password magic. In short, it check whether given password is blank or not and does not assign anything to #password if it is. Hence:
user = User.new
user.password = '' # This is not an assignment, but invocation of `password=` method.
user.password #=> nil
You can see the source code behind password= method here: https://github.com/rails/rails/blob/869a90512f36b04914d73cbf58317d953caea7c5/activemodel/lib/active_model/secure_password.rb#L122
Also note, that has_secure_password already defines default password validation, so if you want to create your own, you need to call it with has_secure_password validation: false
You can try this in irb:
user = User.new
user.password = '123'
user.password_confirmation = '123'
user.password_digest.nil? => false
user = User.new
user.password = ''
user.password_confirmation = ''
user.password_digest.nil? => true
I created a user and then went user.save and got this => false message... I see it says "user exists" but when I go users = User.all theres only 1 user I created a little while back that appears. So why is it saying this user exists?
irb(main):003:0> user = User.new(:screen_name => "jeff holmes", :email => "jeffholmes#gmail.com", :password => "123456")
/Users/coreyholmes/RubymineProjects/worklink/app/models/user.rb:22: warning: regular expression has ']' without escape: /\A[A-Z0-9._%-]+#(A-Z0-9-]+\.)+[A-Z]{2,4}\z/
=> #<User id: nil, screen_name: "jeff holmes", email: "jeffholmes#gmail.com", password: "123456">
irb(main):004:0> user.save
(0.2ms) BEGIN
User Exists (0.3ms) SELECT 1 AS one FROM `users` WHERE `users`.`screen_name` = BINARY 'jeff holmes' LIMIT 1
User Exists (0.2ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'jeffholmes#gmail.com' LIMIT 1
/Users/coreyholmes/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/activemodel-4.2.1/lib/active_model/validations/format.rb:8: warning: regular expression has ']' without escape: /\A[A-Z0-9._%-]+#(A-Z0-9-]+\.)+[A-Z]{2,4}\z/
(0.1ms) ROLLBACK
=> false
UPDATE
Here is the validation code
class User < ActiveRecord::Base
# Max and min lengths for all fields
SCREEN_NAME_MIN_LENGTH = 4
SCREEN_NAME_MAX_LENGTH = 20
PASSWORD_MIN_LENGTH = 4
PASSWORD_MAX_LENGTH = 20
EMAIL_MAX_LENGTH = 50
SCREEN_NAME_RANGE = SCREEN_NAME_MIN_LENGTH...SCREEN_NAME_MAX_LENGTH
PASSWORD_RANGE = PASSWORD_MIN_LENGTH...PASSWORD_MAX_LENGTH
validates_uniqueness_of :screen_name, :email
validates_length_of :screen_name, :within => SCREEN_NAME_RANGE
validates_length_of :password, :within => PASSWORD_RANGE
validates_length_of :email, :maximum => EMAIL_MAX_LENGTH
validates_format_of :screen_name,
:with => /\A[A-Z0-9_]*\z/,
:message => 'must contain only letters, ' + 'numbers, and underscores'
validates_format_of :email,
:with => /\A[A-Z0-9._%-]+#(A-Z0-9-+\.)+[A-Z]{2,4}\z/,
:message => 'common... enter a real email address'
end
I suspect this is part of the problem:
/\A[A-Z0-9._%-]+#(A-Z0-9-]+\.)+[A-Z]{2,4}\z/
# ↑
# Is this supposed to be "(["?
P.S. This is also a bad regex for validating an email address. For one thing, there are tons of TLDs with more than four characters now.
In seeds.rb I have a single record for my Messages model:
Message.create!(email: "example#example.com",
name: "Example User",
content: "This is my message")
If I run rake db:seed I get the error message:
rake aborted!
ActiveRecord::RecordInvalid: Validation failed: Email has already been taken, Username has already been taken
/usr/local/rvm/gems/ruby-2.1.5/gems/activerecord-4.2.1/lib/active_record/validations.rb:79:in `raise_record_invalid'
...
If I run bundle exec rake db:reset I get the error:
-- initialize_schema_migrations_table()
-> 0.0734s
rake aborted!
ActiveRecord::StatementInvalid: SQLite3::ConstraintException: NOT NULL constraint failed: messages.name: INSERT INTO "messages" ("created_at", "updated_at") VALUES (?, ?)
/usr/local/rvm/gems/ruby-2.1.5/gems/sqlite3-1.3.10/lib/sqlite3/statement.rb:108:in `step'
...
Email in the Message model is indeed required. But still when I reset the db, I don't see how this record could be invalid. And email does not need to be unique, so why does the seed command give such an error?
The model file for messages:
attr_accessor :name, :email, :content
validates :name, presence: true,
length: { maximum: 255 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true,
length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
validates :content, presence: true,
length: { maximum: 600 }
Messages migration file:
def change
create_table :messages do |t|
t.string :name, null: false, limit: 255
t.string :email, null: false, limit: 255
t.string :content, null: false, limit: 600
t.timestamps null: false
end
end
I'm not sure what is causing this problem...
Update: I'm lost, I now also get an error message when seeding even without the Message.create! lines in the seeds file. The error says rake aborted! ActiveRecord::RecordInvalid: Validation failed: Email has already been taken, Username has already been taken. But how can this be since I first run bundle exec rake db:reset? Doesn't this remove all the data from the db and wouldn't this by definition mean they can't already be taken?
My seeds file is:
User.create!(fullname: "Example User",
username: "fakename0",
email: "example#railstutorial.org",
admin: true,
activated: true,
activated_at: Time.zone.now,
password: "foobar",
password_confirmation: "foobar")
User.create!(fullname: "Example User 2",
username: "fawwkename0",
email: "exaaample#railstutorial.org",
admin: false,
activated: true,
activated_at: Time.zone.now,
organization: "organization",
password: "foobar",
password_confirmation: "foobar")
99.times do |n|
username = "fakename#{n+1}"
email = "example-#{n+1}#railstutorial.org"
password = "password"
User.create!(username: username,
email: email,
password: password,
password_confirmation: password,
activated: true,
activated_at: Time.zone.now)
Update 2: I found out that running rake db:reset already seeds the db (or at least the db wasn't empty after this command). Still this doesn't explain why I can't seed it with
Message.create!(email: "example#example.com",
name: "Example User",
content: "This is my message")
Seeding this data generates the error: rake aborted!
ActiveRecord::StatementInvalid: SQLite3::ConstraintException: NOT NULL constraint failed: messages.name: INSERT INTO "messages" ("created_at", "updated_at") VALUES (?, ?) /usr/local/rvm/gems/ruby-2.1.5/gems/sqlite3-1.3.10/lib/sqlite3/statement.rb:108:in 'step' ...
Removing attr_accessor :name, :email, :content from the model file solved it. Don't really know why. I added this line because it is included in the tutorial: http://matharvard.ca/posts/2014/jan/11/contact-form-in-rails-4/ But also without that line the messages form still seems to be working.
If you haven't seen it yet, have a look at the latest railscast authentication in rails 3.1. He uses password_digest:string when generating the model and adds has_secret_password to the User model. He also adds some accessible_attributes, :password and :password_confirmation.
By using validates_confirmation_of :password we can make sure the password is confirmed before creating an instance. If I leave the password fields empty in my form I get this error message:
ruby-1.9.2-p180 :024 > u = User.new
=> #<User id: nil, name: nil, email: nil, password_digest: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :027 > u.save
(0.4ms) SELECT 1 FROM "users" WHERE "users"."email" = '' LIMIT 1
=> false
ruby-1.9.2-p180 :028 > u.errors.full_messages
=> ["Password digest can't be blank"]
Of course we don't want to call the password field "Password digest" when communicating with our users. How do I change this error message?
Bonus question
When using validates_confirmation_of and using mismatching passwords I get two error messages telling me about it, and only the :password label and input tags are surrounded with fields_with_errors divs. If the passwords don't match I want to also highlight the password_confirmation input, if possible remove it altogether from the password part.
Should I write my own method for checking password confirmation? If so, could you provide some small guidelines?
The official way to solve this is to add the following to your locale file:
en:
activerecord:
attributes:
user:
password_digest: 'Password'
The English locale file is located at config/locales/en.yml. You may have to restart your server for the changes to be loaded in your environment.
To get rid of password_digest message, you can modify the view:
<% #user.errors.messages.delete(:password_digest) -%>
You could override the human_attribute_name method and set your own humanized version of the password_digest attribute. Try something like this:
HUMANIZED_ATTRIBUTES = {
:password_digest => "Password"
}
def self.human_attribute_name(attr, options={})
HUMANIZED_ATTRIBUTES[attr.to_sym] || super
end
Then your error should look like this: "Password can't be blank"