Как предотвратить обновление пароля, если он пуст в модели Rails, и протестировать его с помощью RSpec?

Я использую драгоценный камень Bcrypt для шифрования паролей.

Модель пользователя:

class User < ApplicationRecord
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, confirmation: true
  # ...
end

Как сделать так, чтобы пароль был пустым при обновлении пользователя?:

expect(user.update(name: 'Joana', password: '')).to be(true)

Но не обновить пароль?

expect { user.update(name: 'Joana', password: '') }
.not_to(change { user.reload.password_digest })

Если вы хотите протестировать его, не забудьте найти пользователя перед обновлением, чтобы не получить ложных срабатываний:

user = User.find(user.id)

🤔 А знаете ли вы, что...
Фреймворк предоставляет средства для масштабирования приложений, включая работу с множеством серверов и балансировку нагрузки.


105
3

Ответы:

Я справляюсь с этим в своем приложении следующим образом. Этой части моего кода уже несколько лет, поэтому в более поздних версиях Rails могут быть некоторые улучшения, но она работает нормально.

Конечно, вы шифруете пароль перед сохранением в базе данных. Допустим, поле базы данных, в котором хранится зашифрованный пароль, — crypted_password.

Теперь в вашей модели User у вас есть:

class User < ApplicationRecord
  attr_accessor :password # this is the name of the parameter that comes in from the form on the front end

  before_save :encrypt_password

  def encrypt_password
    return if password.blank? # return early and don't assign any value to crypted_password, and it will not be changed
    self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if salt.blank?
    self.crypted_password = encrypt(password) # encrypt, here, is a method provided by my included authentication module
  end

end

ОБНОВЛЕНИЕ Теперь, когда мы увидели реальную модель пользователя...

has_secure_password уже работает так, как вы описываете. Он добавляет кучу собственных проверок и игнорирует пустой пароль.

class User < ApplicationRecord
  has_secure_password
  validates :password,
    presence: true,
    length: { minimum: 6 },
    confirmation: true
end

И несколько быстрых проверок в консоли Rails.

3.2.2 :001 > user = User.create!(name: "Foo", password: "abc123")
  TRANSACTION (0.1ms)  begin transaction
  User Create (0.7ms)  INSERT INTO "users" ("name", "password", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id"  [["name", "Foo"], ["password", "[FILTERED]"], ["password_digest", "[FILTERED]"], ["created_at", "2024-06-20 19:09:14.107560"], ["updated_at", "2024-06-20 19:09:14.107560"]]
  TRANSACTION (1.1ms)  commit transaction
 => 
#<User:0x00000001096d28d8
... 
3.2.2 :002 > user.update!(name: 'Joana', password: '')
  TRANSACTION (0.1ms)  begin transaction
  User Update (0.4ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Joana"], ["updated_at", "2024-06-20 19:09:17.852613"], ["id", 6]]
  TRANSACTION (0.1ms)  commit transaction
 => true 
3.2.2 :003 > user.update!(name: 'Joana', password: '')
 => true 
3.2.2 :004 > user.update!(name: 'Joana', password: nil)
(irb):4:in `<main>': Validation failed: Password can't be blank, Password can't be blank, Password is too short (minimum is 6 characters) (ActiveRecord::RecordInvalid)

user.update!(name: 'Joana', password: '') обновляет только имя, а не пароль. Это функция, предоставленная has_secure_password.


You could add a `before_update` [callback](https://guides.rubyonrails.org/active_record_callbacks.html) to your model to strip blanks.
class User
  before_update -> { self.restore_password! if self.password.blank? }

  ...
end

Однако обратные вызовы моделей могут вызвать проблемы, особенно когда они обеспечивают соблюдение «бизнес-правил», которые могут измениться или не быть универсально применимыми. А иногда обратные вызовы пропускаются и могут усложнить массовое обновление базы данных. Это проблема «толстой модели».

Вместо этого сделайте это как проверку модели.

class User
  validates :password, presence: true

  ...
end

Обычно вы оставляете все как есть и используете проверку, чтобы сообщить пользователю о проблеме.

Если вы хотите обновить другие атрибуты пользователя, даже если один из них недействителен, это сомнительная практика, вам следует удалить пустой параметр пароля в вашем контроллере. Лучшая причина — если пользователь случайно вставит пробел в поле пароля или что-то в этом роде.

class UserController
  ...

  def update
    # or `params.compact_blank!` to do this for all parameters
    params.delete(:password) if params[:password].blank?
    ...
  end
end

Но не изобретайте заново учетные записи пользователей. Используйте придумать . Он безопасен, хорошо документирован и работает со многими другими драгоценными камнями.


Решено
class User < ApplicationRecord
  has_secure_password
  validates(
    :password, 
    allow_nil: { on: :update }, # add this
    length: { minimum: 6 }
  )
  # ...
end

Таким образом, эти ожидания оправдываются:

user = User.find(user.id) # Refresh object as if you were in controller

expect(user.update(name: 'Joana')).to be(true)
expect(user.update(name: 'Joana', password: '')).to be(true)

expect { user.update(name: 'Joana', password: '') }
.not_to(change { user.reload.password_digest })

Однако имейте в виду, что он не будет обновляться с фактическим паролем nil:

expect(user.update(name: 'Joana', password: nil)).to be(false)

Итак, это противоречит здравому смыслу, но это работает.

Подтверждение и проверка присутствия не требуются, поскольку они предоставляются Rails.