Как предотвратить обновление пароля, если он пуст в модели 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)

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

dbugger 18.06.2024 13:48

Рассматривали ли вы проверку, которая гарантирует наличие password?

spickermann 18.06.2024 14:46

Да, я это сделал, но это не позволяет мне сохранять другие атрибуты в операторе обновления, если пароль отсутствует. @spickermann

Chiara Ani 18.06.2024 15:01

Хорошо, я могу очистить его в контроллере или во внешнем интерфейсе. Тогда мне следует добавить allow_nil: { on: :update } к проверке пароля? @Стефан

Chiara Ani 18.06.2024 15:07

Если пользователь не хочет устанавливать новый пароль, интерфейсная часть, вероятно, не должна отправлять пустое значение. Кроме того, вызов params.compact_blank! в вашем контроллере приведет к удалению пустых значений.

Stefan 18.06.2024 15:09

это чисто стилистическая проблема, но я бы удалил поле пароля, если оно пустое, в обратном вызове before_update в модели, а не при манипуляциях в контроллере. Это парадигма «толстой модели и тощего контроллера». На мой взгляд, контроллеры слишком перегружены.

Les Nightingill 18.06.2024 19:31

Да, я думал об этом. Однако я не знаю, как это сделать. Вот почему я задал вопрос. @LesNightingill

Chiara Ani 19.06.2024 10:31
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
105
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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

Я использую драгоценный камень Bcrypt, поэтому, возможно, для меня это будет совсем по-другому.

Chiara Ani 20.06.2024 14:04

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

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

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

Что ж, я делаю API с JSON Web Token. Девайс очень хорошо работает в монолите. Однако все API-решения с devise, которые я видел до сих пор, либо не поддерживаются, либо требуют столько же работы, как и отсутствие использования devise вообще.

Chiara Ani 20.06.2024 13:50

С другой стороны, если я удалю пароль из оператора обновления user.update(name: 'Joana'), проверка присутствия все равно завершится неудачей. Поэтому мне также приходится разрешить ноль при проверке пароля при обновлении, что кажется нелогичным.

Chiara Ani 20.06.2024 13:56

@ChiaraAni Ты пробовал? user.update!(password: nil), user.update!(password: "") и user.update!(password: " ") не пройдут проверку, но user.update(name: 'Joana') работает нормально, поскольку пароль не обновляется. Вам не нужно разрешать ноль. Возможно, недоразумение здесь в том, что user.update(name: "Foo") и user.update(name: "Foo", password: nil) разные. Существует очень важная разница между передачей ключа со значением nil и отсутствием передачи ключа вообще.

Schwern 20.06.2024 20:43

@ChiaraAni Я обновил свой ответ теперь, когда вы опубликовали свою модель пользователя. Это уже работает, как вы описали. Вы изменили свою модель пользователя?

Schwern 20.06.2024 21:19

Прежде чем обновлять его, не забудьте найти пользователя так, как будто вы находитесь в контроллере: User.find(user.id). Ваш подход является ложным срабатыванием. Похоже, он получает пароль из рубиновой памяти, поэтому он не дает сбоя, как должен.

Chiara Ani 21.06.2024 11:57

@ChiaraAni Ты прав! Это странно. Между объектами есть внутренние различия, но для проверки это не имеет значения. Нет состояний «создать публикацию» или «найти публикацию». Возможно, вы нашли ошибку. Еще одна причина не полагаться на этот метод и вместо этого очистить свои входные данные.

Schwern 21.06.2024 19:53

@ChiaraAni Я думаю, что это ошибка или недоработка has_secure_password. ‼Я задала по этому поводу фокусированный вопрос.

Schwern 21.06.2024 21:13
Ответ принят как подходящий
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.

Вы получите тот же результат с allow_nil или без него. Демонстрация. И ваш вопрос был не о нуле, обновите свой вопрос, пожалуйста.

Schwern 20.06.2024 21:09

Я думаю, у вас ложноположительный результат. Если вы действительно найдете пользователя User.find(user.id), а затем обновите его, ничего не получится. Поэтому он терпит неудачу, когда вы обновляете его в контроллере. Правда, мой вопрос был не о nil, и я не хочу, чтобы он был о nil, я просто имею в виду, что это нелогично, что с nil это не работает. @Шверн

Chiara Ani 21.06.2024 11:44

Лучшим способом протестировать это было бы попытаться обновить пользователя, а затем вместо этого использовать expect(user.authenicate(old_password)).to be_truthy. Столбец дайджеста пароля — это внутренняя деталь реализации, о которой ваши тесты не обязательно должны знать.

max 21.06.2024 13:03

Другие вопросы по теме