How can allow_nil let an empty string pass the validation - ruby-on-rails

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

Related

Existence of ActiveModel::SecurePassword authenticate method (Rails 6)

The "authenticate" method can only be found here: https://apidock.com/rails/ActiveModel/SecurePassword/InstanceMethodsOnActivation/authenticate
, with version 6.0.0 being grayed out. So this seems to be outdated.
I have searched the Rails 6 documentation for the authenticate method, and found no record of it under https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html.
Yet in the code snippet on the same page
# Schema: User(name:string, password_digest:string, recovery_password_digest:string)
class User < ActiveRecord::Base
has_secure_password
has_secure_password :recovery_password, validations: false
end
user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
user.save # => false, password required
user.password = 'mUc3m00RsqyRe'
user.save # => false, confirmation doesn't match
user.password_confirmation = 'mUc3m00RsqyRe'
user.save # => true
user.recovery_password = "42password"
user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
user.save # => true
user.authenticate('notright') # => false
user.authenticate('mUc3m00RsqyRe') # => user
user.authenticate_recovery_password('42password') # => user
User.find_by(name: 'david')&.authenticate('notright') # => false
User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
The authenticate method is still used (user.authenticate). Where does this method come from if I can't find it in the latest documentation?
Edit:
A related question regarding differences in documentation: I am able to find ActionDispatch::Request::Session on rubydocs but not on api.rubyonrails.
https://www.rubydoc.info/docs/rails/ActionDispatch/Request/Session
https://api.rubyonrails.org/classes/ActionDispatch/Request.html
Now I am not certain where I should be looking when searching for methods. Is api.rubyonrails not the "definitive" place to look for documentation?
It looks like they forgot to mention it in the documentation for has_secure_password. If you look into source code of ActiveModel::SecurePassword. You will find
# Returns +self+ if the password is correct, otherwise +false+.
#
# class User < ActiveRecord::Base
# has_secure_password validations: false
# end
#
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
# user.save
# user.authenticate_password('notright') # => false
# user.authenticate_password('mUc3m00RsqyRe') # => user
define_method("authenticate_#{attribute}") do |unencrypted_password|
attribute_digest = public_send("#{attribute}_digest")
BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
end
alias_method :authenticate, :authenticate_password if attribute == :password
You can se it is now defined as dynamic method based on the parametr name provided to has_secure_password method. So they implemented it in more general way. And to be more friendly with backwards compatibility the implemented the alias authenticate for authenticate_password which was the original implementation.
Unfortunately these dynamic methods are not very well documented in the rails API docs.

Rspec presence: true check fails even though it works as intended

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.

How do I allow uppercase usernames in Refinery CMS?

I believe Refinery uses Devise, and I found this guide to allow uppercase usernames in Devise
https://github.com/plataformatec/devise/wiki/How-To%3a-Allow-users-to-sign-in-using-their-username-or-email-address
However, even with
config.authentication_keys = [ :login ]
config.case_insensitive_keys = [:email]
it still forced the username to lowercase.
> u = User.create username: "UsErNaMe", password: "secret", email: "email#com.com"
=> #<Refinery::User id: 60, username: "username", email: "email#com.com",
I saw this question, but it did not help
Devise: Allow users to register as "UsErNaMe" but login with "username"
Refinery 2.1.1, Devise 2.2.8, Rails 3.2.14
It is in the Refinery::User model. There's a before_validation filter that downcases usernames:
...
before_validation :downcase_username, :strip_username
...
private
def downcase_username
self.username = self.username.downcase if self.username?
end
You could decorate the Refinery::User model:
Refinery::User.class_eval do
private
def downcase_username
self.username if self.username?
end
end
Found it
intended_username = user_params[:username] # save the username because Refinery converts it to lowercase! https://github.com/refinery/refinerycms/blob/master/authentication/app/models/refinery/user.rb#L28
intended_username.strip! # trim spaces
if current_refinery_user.update_without_password(user_params)
current_refinery_user.username = intended_username # restore the username as the user intended with mixed case
current_refinery_user.save(validate: false) # skip validations

Rails 3.1. Create one user in console with secure password

I want to create one user (admin) and I want to use console (without user registration model). I use solution from RailsCasts (http://railscasts.com/episodes/270-authentication-in-rails-3-1).
But I have one problem: when I do User.create(..., :password => "pass") in console my password stored in database without encription (like "pass"). And I can't login with my data.
How can I create user from console? :)
Straight from the Rails API
# Schema: User(name:string, password_digest:string)
class User < ActiveRecord::Base
has_secure_password
end
user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch")
user.save # => false, password required
user.password = "mUc3m00RsqyRe"
user.save # => false, confirmation doesn't match
user.password_confirmation = "mUc3m00RsqyRe"
user.save # => true
user.authenticate("notright") # => false
user.authenticate("mUc3m00RsqyRe") # => user
You need to include :password_confirmation => "pass in your hash!
Right, so taking a look at has_secure_password you want to perform BCrypt::Password.create(unencrypted_password) to obtain it. You'll need the bcrypt-ruby gem to do the above.

Create a devise user from Ruby console

Any idea on how to create and save a new User object with devise from the ruby console?
When I tried to save it, I'm getting always false. I guess I'm missing something but I'm unable to find any related info.
You can add false to the save method to skip the validations if you want.
User.new({:email => "guy#gmail.com", :roles => ["admin"], :password => "111111", :password_confirmation => "111111" }).save(false)
Otherwise I'd do this
User.create!({:email => "guy#gmail.com", :roles => ["admin"], :password => "111111", :password_confirmation => "111111" })
If you have confirmable module enabled for devise, make sure you are setting the confirmed_at value to something like Time.now while creating.
You should be able to do this using
u = User.new(:email => "user#name.com", :password => 'password', :password_confirmation => 'password')
u.save
if this returns false, you can call
u.errors
to see what's gone wrong.
When on your model has :confirmable option this mean the object user should be confirm first. You can do two ways to save user.
a. first is skip confirmation:
newuser = User.new({email: 'superadmin1#testing.com', password: 'password', password_confirmation: 'password'})
newuser.skip_confirmation!
newuser.save
b. or use confirm! :
newuser = User.new({email: 'superadmin2#testing.com', password: 'password', password_confirmation: 'password'})
newuser.confirm!
newuser.save
If you want to avoid sending confirmation emails, the best choice is:
u = User.new({
email: 'demo#greenant.com.br',
password: '12feijaocomarroz',
password_confirmation: '12feijaocomarroz'
})
u.confirm
u.save
So if you're using a fake email or have no internet connection, that'll avoid errors.
None of the above answers worked for me.
This is what I did:
User.create(email: "a#a.com", password: "asdasd", password_confirmation: "asdasd")
Keep in mind that the password must be bigger than 6 characters.

Resources