Опубликован: 27.01.2016 | Уровень: для всех | Доступ: платный
Лекция 6:

Моделирование пользователей

Аутентификация пользователя

Последняя часть механики наших паролей это метод для получения пользователей по их email и паролям. Эта задача естественным образом разбивается на две части: первая из них это поиск пользователя по адресу электронной почты; вторая это аутентификация пользователя с данным паролем. Все тесты (кроме последнего) в этом разделе связаны с has_secure_password, так что в процессе реализации вы должны иметь возможность раскомментировать закомментированную строку из Листинге 6.27 для того чтобы дать тестам возможность пройти.

Первый шаг прост; как мы видели в Разделе 6.1.4, мы можем найти пользователя с данным адресом электронной почты с помощью метода find_by:

user = User.find_by(email: email)

Второй шаг заключается в применении метода authenticate для проверки того что у пользователя есть данный пароль. В Главе 8, мы будем получать текущего (вошедшего) пользователя используя код вроде этого:

current_user = user.authenticate(password)

Если данный пароль совпадает с паролем пользователя, он должен вернуть пользователя; в противном случае он должен вернуть false.

Как обычно, мы можем выразить требования для authenticate используя RSpec. Получившиеся в результате тесты являются немного более продвинутыми чем те что мы видели до этого, так что давайте разобьем их на части; если вы новичок в RSpec, вам возможно понадобится прочитать этот раздел несколько раз. Мы начнем с того, что объект User должен отвечать на authenticate:

it { should respond_to(:authenticate) }

Затем мы покрываем два случая - совпадение и несовпадения пароля:

describe "return value of authenticate method" do
  before { @user.save }
  let(:found_user) { User.find_by(email: @user.email) }

  describe "with valid password" do
    it { should eq found_user.authenticate(@user.password) }
  end

  describe "with invalid password" do
    let(:user_for_invalid_password) { found_user.authenticate("invalid") }

    it { should_not eq user_for_invalid_password }
    specify { expect(user_for_invalid_password).to be_false }
  end
end

Блок before сохраняет пользователя в базе данных, так что он может быть получен с помощью find_by, чего мы достигаем используя let method:

let(:found_user) { User.find_by(email: @user.email) }

Мы уже использовали let в нескольких упражнениях, это первый случай когда мы его видим в основном тексте учебника. Блок 6.3 рассказывает о let более подробно.

Два блока describe покрывают случаи когда @user и found_user должны быть одинаковыми (совпадение пароля) и разными (несовпадение пароля); они используют "равенство" eq для проверки эквивалентности объектов (который в свою очередь использует ==, как мы видели в Разделе 4.3.1). Обратите внимание что тесты в

describe "with invalid password" do
  let(:user_for_invalid_password) { found_user.authenticate("invalid") }

  it { should_not eq user_for_invalid_password }
  specify { expect(user_for_invalid_password).to be_false }
end

используют let второй раз, а также используют метод specify. Это просто синоним для it, который может быть использован когда it звучит ненатурально

___________________________________________

Блок 6.3.Применение let

RSpec метод let обеспечивает удобный способ для создания локальных переменных внутри тестов. Синтаксис может выглядеть немного странно, но его эффект аналогичен назначению переменной. Аргументом let является символ и он принимает блок, который возвращает значение назначаемое локальной переменной с именем символа. Другими словами,

let(:found_user) { User.find_by(email: @user.email) }

создает переменную found_user чье значение эквивалентно результату find_by. Затем мы можем использовать эту переменную в любом before или it блоке на протяжении всего оставшегося теста. Одно из достоинств let заключается в том, что он мемоизирует свое значение, это означает что он запоминает значение от одного вызова до другого. (Обратите внимание, что memoize это технический термин; в частности, его не надо путать с "memorize".) В данном случае, поскольку let мемоизирует переменную found_user, метод find_by будет вызван лишь единожды при запуске спеков модели User.

___________________________________________

Наконец, в качестве дополнительной меры предосторожности, мы протестируем на наличие валидации длины паролей, установив длину паролей не меньшей чем шесть знаков:

describe "with a password that's too short" do
  before { @user.password = @user.password_confirmation = "a" * 5 }
  it { should be_invalid }
end

Собрав вместе все тесты мы получаем Листинге 6.28.

require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }
  .
  .
  .
  it { should respond_to(:authenticate) }
  .
  .
  .
  describe "with a password that's too short" do
    before { @user.password = @user.password_confirmation = "a" * 5 }
    it { should be_invalid }
  end

  describe "return value of authenticate method" do
    before { @user.save }
    let(:found_user) { User.find_by(email: @user.email) }

    describe "with valid password" do
      it { should eq found_user.authenticate(@user.password) }
    end

    describe "with invalid password" do
      let(:user_for_invalid_password) { found_user.authenticate("invalid") }

      it { should_not eq user_for_invalid_password }
      specify { expect(user_for_invalid_password).to be_false }
    end
  end
end
Листинг 6.28. Тесты для метода authenticate. spec/models/user_spec.rb

Как было отмечено в Блоке 6.3, let мемоизирует свое значение, так что первый вложенный describe блок в Листинге 6.28 вызывает let для получения пользователя из базы данных с помощью find_by, но второй describe блок уже не обращается к базе данных.

У пользователя есть безопасный пароль

В предыдущей версии Rails, добавление безопасного пароля было сложным и долгим, как это можно увидеть в Rails 3.0 версии Rails Tutorial16http://railstutorial.ru/chapters/3_0/beginning, где описано создание аутентификационной системы с нуля. Но понимание веб-разработчиками того как лучше всего аутентифицировать пользователей созрело настолько, что она (аутентификация) теперь поставляется в комплекте с последней версией Rails. В результате чего мы закончим реализацию безопасных паролей (и получим зеленый набор тестов) используя лишь несколько строк кода.

Во-первых, нам нужна валидация длины для пароля которая использует ключ :minimum по аналогии с ключом :maximum из Листинга 6.12:

validates :password, length: { minimum: 6 }

(Валидации наличия и подтверждения автоматически добавляются has_secure_password.)

Во-вторых, нам нужно добавить к атрибутам password и password_confirmation требование наличия пароля, требование их совпадения и добавить authenticate метод для сравнения зашифрованного пароля с password_digest для аутентификации пользователей. Это единственный непростой шаг и в последней версии Rails все эти фичи бесплатно поставляются в одном методе - has_secure_password:

has_secure_password

Пока столбец password_digest присутствует в базе данных, добавление одного лишь этого метода к нашей модели дает нам безопасный способ для создания и аутентификации новых пользователей.

(Если вы хотите увидеть как реализован has_secure_password, я советую взглянуть на хорошо документированый и вполне читабельный исходный код secure_password.rb. Этот код включает строки

validates_confirmation_of :password,
                          if: lambda { |m| m.password.present? }

которые (как описано в Rails API) автомагически создают атрибут password_confirmation. Он также включает валидацию для password_digest атрибута.

Совместно с валидацией наличия из Листинга 6.26, вышеприведенные элементы приводят к модели User показаной в Листинге 6.29, который завершает реализацию безопасных паролей.

class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence:   true,
                    format:     { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, length: { minimum: 6 }
end
Листинг 6.29. Законченная реализация безопасных паролей. app/models/user.rb

Теперь необходимо убедиться что набор тестов проходит:

$ bundle exec rspec spec/

Примечание: Если вы получили deprecation warning вроде

[deprecated] I18n.enforce_available_locales will default to true in the future

вы можете попробовать избавиться от него отредактировав config/application.rb следующим образом:

require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    I18n.enforce_available_locales = true
    .
    .
    .
  end
end
Вадим Обозин
Вадим Обозин

Здравствуйте, записался на курс. При этом ставил галочку на "обучаться с тьютором". На email пришло письмо, о том, что записался на самостоятельное изучение курса. Как выбрать тьютора?

Акбар Ахвердов
Акбар Ахвердов
Россия, г. Москва
Артём Зайцев
Артём Зайцев
Украина, ДНР