RSpec+Capybaraによるチェックボックスの存在確認とCookieの有効期限日を取得する方法
Ruby on Rails Tutorial*1の9.6 Exercisesを終えた後、"Remember me"というチェックボックスをSign inページに追加しようと思いました。
この本で作成するマイクロブログ「Sample App」は、ログイン後、Cookieに保存する"remember_token"の有効期限日を20年後*2に設定しています*3。[Remember me]チェックボックスをチェックせずに[Sign in]ボタンを押下した場合、このクッキーの有効期限日をセッション終了時になるよう改修してみました。
Sample Appは、RSpec+CapybaraによるTDDで開発されていますので、私もそれに従います。
[追記: 8/29 23:19]
第3版の第8章で[Remember me]チェックボックスを追加するそうです。本記事は、第3版が出版される以前に書いたもので、関係はありません。*4
チェックボックスなど追加を確認するテストとその実装
Sign inページのパスワードを入力するフィールドの下に[Remember me]のラベルとチェックボックスを追加するので、authentication_pages_spec.rbにテストを書きました。
spec/requests/authentication_pages_spec.rb
describe "Authentication" do subject { page } describe "signin page" do before { visit signin_path } it { should have_content('Sign in') } it { should have_title('Sign in') } it { should have_css('input#session_remember_me[type="checkbox"]') } it { should have_css('label.checkbox', text: 'Remember me') } end : :
ラベルとチェックボックスの実装です。チェックボックス下のマージンが狭く(5px)、15pxに拡大したかったのでlabel要素のid属性に「remember_me」を指定し、custom.css.scssにスタイルを追加しています。
app/views/sessions/new.html.erb
: : <%= f.password_field :password %> <%= f.label :remember_me, id: "remember_me", class: "checkbox" do %> <%= f.check_box :remember_me %>Remember me <% end %> <%= f.submit "Sign in", class: "btn btn-large btn-primary" %> : :
Cookieの値と有効期限日の取得
追加した[Remember me]チェックボックスをチェックした・していない場合のテストを書く前に、utilities.rbにCookieの値と有効期限を取得するメソッドを追加しました。
cookieメソッドは、引数にCookie名を指定すると、その値を返します。cookie_expiresメソッドは、有効期限日が設定されている場合はTimeオブジェクトを返し、それ以外はnilを返します。また、既存のsign_inメソッドに[Remember me]チェックボックスをチェックする行を追加しています。
spec/support/utilities.rb
include ApplicationHelper def sign_in(user, options={remember_me: true, no_capybara: false}) if options[:no_capybara] # Sign in when not using Capybara. remember_token = User.new_remember_token cookies[:remember_token] = remember_token user.update_attribute(:remember_token, User.digest(remember_token)) else visit signin_path fill_in 'Email', with: user.email fill_in 'Password', with: user.password check 'session_remember_me' if options[:remember_me] click_button "Sign in" end end def cookie(key) page.driver.browser.rack_mock_session.cookie_jar[key] end def cookie_expires expires = nil page.driver.browser.rack_mock_session.cookie_jar.instance_variable_get(:@cookies).each do |cookie| if cookie.instance_variable_defined?(:@options) && (options = cookie.instance_variable_get(:@options)).key?('expires') date = options['expires'] expires = Time.parse(date) unless date.blank? break end end expires end
チェックボックスをチェックした・していない場合のテストとその実装
[Remember me]チェックボックスを有効にした場合、Cookieの有効期限日が現在から20年後以降になっているかテストしました。このチェックボックスを無効にした場合は、Cookieに有効期限日が設定されていないことも確認しています。
また、データベースのUsersテーブルにあるremember_tokenフィールドとCookieのremember_tokenが同一か、併せてテストしています。
spec/requests/authentication_pages_spec.rb
: : 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') } describe "after visiting another page" do before { click_link "Home" } it { should_not have_selector('div.alert.alert-error') } end end describe "with valid information" do let(:user) { FactoryGirl.create(:user) } let(:remember_token) { User.digest(cookie('remember_token')) } let!(:expire_date) { -20.years.ago } before { sign_in user } it { should have_title(user.name) } it { should have_link('Users', href: users_path) } it { should have_link('Profile', href: user_path(user)) } it { should have_link('Settings', href: edit_user_path(user)) } it { should have_link('Sign out', href: signout_path) } it { should_not have_link('Sign in', href: signin_path) } specify { expect(remember_token).to eq user.reload.remember_token } specify { expect(cookie_expires.to_i).to be >= expire_date.to_i } describe "followed by signout" do before { click_link "Sign out" } it { should have_link('Sign in') } end end describe "with valid information, but not remembered" do let(:user) { FactoryGirl.create(:user) } before { sign_in user, remember_me: false } specify { expect(cookie_expires).to be_nil } end end : :
最後に、sessions_helper.rbのsign_inメソッドとsessions_controller.rbのcreateメソッドを改修しました。
app/helpers/sessions_helper.rb
module SessionsHelper def sign_in(user, permanently = true) remember_token = User.new_remember_token if permanently cookies.permanent[:remember_token] = remember_token else cookies[:remember_token] = remember_token end user.update_attribute(:remember_token, User.digest(remember_token)) self.current_user = user end : :
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController : : def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) sign_in user, params[:session][:remember_me] == '1' redirect_back_or user else flash.now[:error] = 'Invalid email/password combination' render 'new' end end : :
余談
実装していて気づいたのですが、Sign in後にCookieを削除し、Sign outするとアプリケーションエラーが発生するなど、元のソースに不具合がありました。今回の件とは直接関係しませんが、以下のように修正しました。
app/helpers/sessions_helper.rb
module SessionsHelper : : def current_user cookie = cookies[:remember_token] @current_user ||= User.find_by(remember_token: User.digest(cookie)) unless cookie.nil? end : : def sign_out current_user.update_attribute(:remember_token, User.digest(User.new_remember_token)) unless current_user.nil? cookies.delete(:remember_token) session.delete(:return_to) self.current_user = nil end def redirect_back_or(default) url = session[:return_to] || default session.delete(:return_to) redirect_to(url) end end