Search code examples
ruby-on-railsvalidationactiverecorddevise

find_or_create_by! inserting record when validation fail


In my Rails 4.1 app I'm using devise for user authentication.

I'm trying to create a user manually from a controller called transactions_controller and there is a class called CreateAdminService which I use for creating user from the parameters received in POST.

This is the code in CreateAdminService which does that

  def call (params, confirm = true, role = :user)
    user = User.find_or_create_by!(email: params[:email]) do |user|
        user.name     = params[:name] if params[:name]
        user.password = params[:password]
        user.password_confirmation = params[:password_confirmation]
        user.confirm! if confirm
        user.send("#{role}!") unless role == :user
      end
  end 

Everything works fine. When the passwords I sent don't match, the validations fail messages is show but the record is created. The new record doesn't have any passwords stored. I don't know if this a problem with devise or find_or_create_by! which shows this behaviour.

I tried to use find_or_create_by without the ! and this creates the record and doesn't check any validations or throws any errors which is quite bad.

How can I debug why this is happening or how I can create user whilst all validations can work.

Edit: The user model is as follows

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable

  devise :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable

  enum role: [:user, :vip, :admin]
  enum status: [:active, :suspended]

  has_many :accounts
  has_many :txns

  after_initialize :set_default_role, :if => :new_record?

  after_create :setup_account

  private

  def set_default_role
    self.role  ||= :user
    self.status ||= :active
  end

  def setup_account
    self.accounts.create!
  end
end

The log for the request and the error is this

Started POST "/start" for 127.0.0.1 at 2014-06-01 22:08:38 +0400
Processing by TransactionsController#start as JSON
  Parameters: {"user"=>{"name"=>"test", "email"=>"yavaidya4@test.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "transaction"=>{"user"=>{"name"=>"test", "email"=>"yavaidya4@test.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}}}
  User Load (0.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."email" = 'yavaidya4@test.com' LIMIT 1
   (0.1ms)  begin transaction
Binary data inserted for `string` type on column `encrypted_password`
  SQL (0.4ms)  INSERT INTO "users" ("confirmed_at", "created_at", "email", "encrypted_password", "name", "role", "status", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?)  [["confirmed_at", "2014-06-01 18:08:38.344688"], ["created_at", "2014-06-01 18:08:38.345141"], ["email", "yavaidya4@test.com"], ["encrypted_password", "$2a$10$nUiH/dLDrcNAOWpAPTBBYO.pA/mKKpHoCNPiM/0oX/jBiZy8cogWC"], ["name", "test"], ["role", 0], ["status", 0], ["updated_at", "2014-06-01 18:08:38.345141"]]
  Account Exists (0.2ms)  SELECT  1 AS one FROM "accounts"  WHERE "accounts"."user_id" = 14 LIMIT 1
  SQL (0.2ms)  INSERT INTO "accounts" ("balance", "created_at", "number", "status", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?, ?)  [["balance", 0.0], ["created_at", "2014-06-01 18:08:38.364984"], ["number", "22743176"], ["status", 1], ["updated_at", "2014-06-01 18:08:38.364984"], ["user_id", 14]]
   (0.7ms)  commit transaction
   (0.0ms)  begin transaction
   (0.2ms)  rollback transaction
Completed 422 Unprocessable Entity in 107ms



ActiveRecord::RecordInvalid - Validation failed: Password confirmation doesn't match Password:
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/validations.rb:57:in `save!'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/attribute_methods/dirty.rb:29:in `save!'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/transactions.rb:273:in `block in save!'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/transactions.rb:329:in `block in with_transaction_returning_status'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/connection_adapters/abstract/database_statements.rb:211:in `block in transaction'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/connection_adapters/abstract/database_statements.rb:219:in `within_new_transaction'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/connection_adapters/abstract/database_statements.rb:211:in `transaction'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/transactions.rb:208:in `transaction'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/transactions.rb:326:in `with_transaction_returning_status'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/transactions.rb:273:in `save!'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/validations.rb:41:in `create!'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/relation.rb:140:in `block in create!'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/relation.rb:286:in `scoping'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/relation.rb:140:in `create!'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/relation.rb:208:in `find_or_create_by!'
   () Users/test/.rvm/gems/ruby-2.1.1@global/gems/activerecord-4.1.0/lib/active_record/querying.rb:6:in `find_or_create_by!'
  app/services/create_admin_service.rb:11:in `call'
  app/controllers/transactions_controller.rb:8:in `start'

Solution

  • Ok, your logging seems pretty weird: it clearly saves the user, and only after that the validations fail. And something else that is weird: the confirmed_at is later then the created_at/updated_at. Wait. confirmed_at is already filled in? In your create block, you mix creation and action on the user.

    I am just guessing, but it seems to me the confirm! might somehow force the user to be saved already, and then later, at the end of the find_or_create_by the user is saved again (or attempted to be saved), and then the validations are checked.

    Checking the devise code my hunch is confirmed (pun! awwwww): it saves the user without any validations.

    So I would try the following:

    def call (params, confirm = true, role = :user)
      user = User.find_or_create_by!(email: params[:email]) do |user|
        user.name     = params[:name] if params[:name]
        user.password = params[:password]
        user.password_confirmation = params[:password_confirmation]
      end
      user.confirm! if confirm
      user.send("#{role}!") unless role == :user
    end 
    

    If the user was not created, an exception is thrown, and the last two lines are never executed anyway.

    On a side-note: after_initialize is not ideal, since it is called a lot.

    In your case, since you seem to just want to set some default values, I would do a

    before_create :set_default_role
    

    And this will only be called before saving a new user.