Опубликован: 27.01.2016 | Доступ: свободный | Студентов: 914 / 58 | Длительность: 23:07:00
Лекция 8:

Войти, выйти

Тестирование входа

Сравнивая рис. 8.1 с рис. 7.11, мы видим, что форма входа (или, что эквивалентно, форма новой сессии) выглядит аналогично форме регистрации, за исключением того что в ней два поля (email и пароль) вместо четырех. Как и с формой регистрации, мы можем протестировать форму входа используя Capybara для заполнения формы данными и последующего клика по кнопке.

В процессе написания тестов мы вынуждены обращать внимание на аспекты дизайна нашего приложения, что является одним из приятных побочных эффектов разработки через тестирование. Мы начнем с провального входа, как это показано на наброске в рис. 8.2.

Набросок провального входа.

Рис. 8.2. Набросок провального входа.

Как видно на рис. 8.2, при предоставлении невалидной информации мы хотим вновь отрендерить страницу входа и вывести на экран сообщение об ошибке. Мы будем рендерить ошибку как флэш сообщение, что мы можем протестировать следующим образом:

it { should have_selector('div.alert.alert-error') }

Здесь используется предоставляемый Capybara метод have_selector который мы видели ранее в решениях для двух упражнений, Листинге 5.38 и Листинге 7.32. Метод have_selector проверяет наличие конкретного селектора (т.е. HTML тега, однако в Capybara 2.0 это работает только для видимых элементов). В данном случае мы ищем

div.alert.alert-error

который проверяет тег div. В частности, вспомнив что в CSS точка обозначает "класс" (Раздел 5.1.2), вы возможно догадались что это тест на наличие тега div с классами "alert" и "alert-error", вроде этого:

<div class="alert alert-error">Invalid...</div>

Комбинация тестов заголовка и флэша приводит нас к коду в Листинге 8.5. Как мы увидим, эти тесты упускают одну важную деталь, которой мы займемся в Разделе 8.1.5.

require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "signin" do
    before { visit signin_path }

    describe "with invalid information" do
      before { click_button "Sign in" }

      it { should have_title('Sign in') }
      it { should have_selector('div.alert.alert-error') }
    end
  end
end
Листинг 8.5. Тесты для провального входа. spec/requests/authentication_pages_spec.rb

Теперь, написав тесты для провального входа, мы возвращаемся к успешному входу. Изменениями которые мы хотим протестировать являются рендеринг страницы профиля пользователя (что предопределено заголовком страницы, который должен быть именем пользователя), совместно с тремя запланированными изменениями в навигации сайта:

  1. Появление ссылки на страницу профиля пользователя
  2. Появление ссылки "Sign out"
  3. Исчезновение ссылки "Sign in"

(Мы отложим тесты для ссылки "Settings" до Раздела 9.1 и для ссылки "Users" до Раздела 9.3.) Набросок этих изменений представлен на рис. 8.3.1Изображение взято с http://www.flickr.com/photos/hermanusbackpackers/3343254977/ Обратите внимание на то, что ссылки на выход и на профиль пользователя появляются в выпадающем меню "Account"; в Разделе 8.2.4, мы увидим как сделать такое меню с помощью Bootstrap.

Набросок профиля пользователя после успешного входа.

Рис. 8.3. Набросок профиля пользователя после успешного входа.

Код тестов для успешного входа представлен в Листинге 8.6.

require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "signin" do
    before { visit signin_path }
    .
    .
    .
    describe "with valid information" do
      let(:user) { FactoryGirl.create(:user) }
      before do
        fill_in "Email",    with: user.email.upcase
        fill_in "Password", with: user.password
        click_button "Sign in"
      end

      it { should have_title(user.name) }
      it { should have_link('Profile',     href: user_path(user)) }
      it { should have_link('Sign out',    href: signout_path) }
      it { should_not have_link('Sign in', href: signin_path) }
    end
  end
end
Листинг 8.6. Тесты успешного входа. spec/requests/authentication_pages_spec.rb

Здесь мы использовали метод have_link. Он принимает в качестве аргументов текст ссылки и необязательный параметр :href, таким образом

it { should have_link('Profile', href: user_path(user)) }

убеждается в том что якорный тег a имеет правильный атрибут href (URL) — в данном случае, ссылку на страницу профиля пользователя. Обратите также внимание на то что мы позаботились upcase email адрес пользователя для того чтобы быть уверенными в том что наша способность находить пользователя в базе данных не зависит от регистра.

Форма для входа

После написания тестов мы готовы приступить к разработке формы для входа. Вспомним из Листинга 7.17 что форма регистрации использует вспомогательный метод form_for, принимающий в качестве аргумента переменную экземпляра @user:

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

Основное отличие между этим и формой для входа заключается в том что у нас нет модели Session, и, следовательно, нет аналога для переменной @user. Это означает, что при конструировании формы для новой сессии нам необходимо предоставить методу form_for чуть больше информации; в частности, тогда как

form_for(@user)

позволяет Rails сделать вывод о том, что действием формы должно быть POST к URL /users, в случае с сессиями мы должны явно указать имя ресурса и соответствующий URL:

form_for(:session, url: sessions_path)

(Вторым возможным способом является использование form_tag вместо form_for; это было бы даже более идеоматически корректным решением с точки зрения Rails, но оно бы имело мало общего с формой регистрации, а на этом этапе я хочу подчеркнуть параллельность структуры. Создание рабочей формы с помощью form_tag оставлено в качестве упражнения (Раздел 8.5).)

Имея на руках правильный form_for легко сделать форму для входа соответствующую наброску на рис. 8.1 используя форму регистрации (Листинг 7.17) в качестве модели, как это показано в Листинге 8.7.

<% provide(:title, "Sign in") %>
<h1>Sign in</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(:session, url: sessions_path) do |f| %>

      <%= f.label :email %>
      <%= f.text_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.submit "Sign in", class: "btn btn-large btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>
Листинг 8.7. Код для формы входа. app/views/sessions/new.html.erb

Обратите внимание на то, что мы для удобства добавили ссылку на страницу входа. С кодом в Листинге 8.7, форма для входа выглядит как на рис. 8.4.

Форма для входа (/signin).

Рис. 8.4. Форма для входа (/signin).

Несмотря на то, что вы вскоре избавитесь от привычки смотреть на HTML генерируемый Rails (вместо этого доверив хелперам, делать свою работу), пока все же давайте взглянем на него (Листинге 8.8).

<form accept-charset="UTF-8" action="/sessions" method="post">
  <div>
    <label for="session_email">Email</label>
    <input id="session_email" name="session[email]" type="text" />
  </div>
  <div>
    <label for="session_password">Password</label>
    <input id="session_password" name="session[password]"
           type="password" />
  </div>
  <input class="btn btn-large btn-primary" name="commit" type="submit"
       value="Sign in" />
</form>
Листинг 8.8. HTML для формы входа произведеный Листингом 8.7.

Сравнивая Листинге 8.8 с Листингом 7.20, вы, возможно, догадались, что отправка этой формы приведет к хэшу params, где params[:session][:email] и params[:session][:password] соответствуют email и password полям.

Обзор отправки формы

Как и в случае создания пользователей (регистрации), первый шаг в создании сессий (вход) состоит в обработке неверного ввода. У нас уже есть тесты для провальной регистрации (Листинг 8.5) и код приложения довольно прост за исключением пары тонкостей. Мы начнем с разбора того что происходит при отправке формы, а затем прикрутим полезное сообщение об ошибке появляющееся в случае провального входа (как это показано на наброске из рис. 8.2.) Затем мы заложим основу для успешного входа (Раздел 8.2) научив наше приложение оценивать каждую попытку входа, опираясь на валидность предоставленной комбинации email/password.

Давайте начнем с определения минималистичного действия create для контроллера Sessions (Листинг 8.9), которое пока не будет делать ничего кроме рендеринга представления new. После чего, отправка формы /sessions/new с пустыми полями, будет приводить к результату показанному на рис. 8.5.

class SessionsController < ApplicationController
  .
  .
  .
  def create
    render 'new'
  end
  .
  .
  .
end
Листинг 8.9. Предварительная версия Sessions create действия. app/controllers/sessions_controller.rb
Начальный провальный вход с create из Листинга 8.9.

Рис. 8.5. Начальный провальный вход с create из Листинга 8.9.

Тщательное изучение отладочной информации на рис. 8.5 показывает, что, как намекалось в конце Раздела 8.1.3, отправка формы приводит к хэшу params содержащему email и password под ключом :session:

---
session:
  email: ''
  password: ''
commit: Sign in
action: create
controller: sessions

Как и в случае регистрации пользователя ( рис. 7.15) эти параметры образуют вложенный хэш, как тот, что мы видели в Листинге 4.6. В частности, params содержит вложенный хэш формы

{ session: { password: "", email: "" } }

Это означает что

params[:session]

само является хэшем:

{ password: "", email: "" }

Как результат,

params[:session][:email]

является предоставленным адресом электронной почты и

params[:session][:password]

является предоставленным паролем.

Иными словами, внутри create действия хэш params имеет всю информацию, необходимую для аутентификации пользователей по электронной почте и паролю. Совершенно не случайно у нас уже как раз есть необходимый нам метод: User.find_by_email предоставленный Active Record (Раздел 6.1.4) и метод authenticate предоставляемый has_secure_password (Раздел 6.3.3). Вспомните что authenticate возвращает false для невалидной аутентификации, наша стратегия для входа пользователя может быть резюмирована следующим образом:

def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    # Sign the user in and redirect to the user's show page.
  else
    # Create an error message and re-render the signin form.
  end
end

Здесь первая строка вытягивает пользователя из базы данных с помощью предоставленного адреса электронной почты. (Вспомните из Раздела 6.2.5 что email адреса сохраняются в нижнем регистре, поэтому здесь мы используем метод downcase для обеспечения соответствия когда предоставленный адрес валиден.) Следующая строка может немного смутить, но она довольна распространена в идеоматическом Rails программировании:

user && user.authenticate(params[:session][:password])

Здесь используется && (логическое и) для определения валидности полученного пользователя. Принимая в расчет что любой объект кроме nil и самой false является true в булевом контексте (Раздел 4.2.3), возможные результаты выглядят как Таблица 8.2. Мы видим в Таблице 8.2 что выражение if является true только если пользователь с данным адресом электронной почты и существует в базе данных и имеет данный пароль, что нам и было необходимо.

Таблица 8.2. Возможные результаты user && user.authenticate(…).
Пользователь Пароль a && b
не существует что-нибудь nil && [anything] == false
валидный пользователь неправильный пароль true && false == false
валидный пользователь правильный пароль true && true == true
Вадим Обозин
Вадим Обозин

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