はじめに
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 8章 8.2 ログインの演習まとめ&解答例です。
個人の解答例なので、誤りがあればご指摘ください。
8.2.1 log_inメソッド
本章での学び
Railsのセッション用ヘルパー
Sessionsコントローラを作成すると自動生成される。 また、ビューでも自動でインクルードされる。
Railsの全コントローラのベースクラス(Application_controller.rb)にインクルードすれば、すべてのコントローラでセッション用ヘルパーが使えるようになる。
include SessionsHelper
sessionメソッド
Railsで事前定義済みのメソッド。ハッシュのように扱える。
session[:user_id] = user.id
ユーザのブラウザ内の一時cookiesに暗号化済みのユーザIDが自動で生成される。後続のページで、session[:user_id]
を使うことで、ユーザIDを元通りに取り出せる。
cookies
メソッドとは対照的に、session
メソッドはブラウザを閉じた瞬間に有効期限が終了する。
セッション用ヘルパーの修正
同じログイン手法を様々な場所で使いまわせるようにするために、セッション用ヘルパーにlog_in
メソッドを追加する。
def log_in(user) session[:user_id] = user.id end
session
メソッドで暗号化した一時cookiesは、攻撃者が盗んだとしても、それを使って本物のユーザとしてログインすることはできない。(永続的な情報ではないためと思われる)
sessionsコントローラの修正
create
アクションを修正する。
ユーザログインを行い、ユーザのプロフィールページへリダイレクトを行う。
if user && user.authenticate(params[:session][:password]) log_in user redirect_to user
redirect_to user
は、Railsgがuser_url(user)
に自動的に変換する。
演習1
有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか? ヒント: ブラウザでcookiesを調べる方法が分からない? 今こそググってみるときです! (コラム 1.1)
ログインページヘアクセスする。 chromeでF12を押して開発ツールを表示して、sessionの値を確認。
有効なユーザでログインする。
演習2
先ほどの演習課題と同様に、Expiresの値について調べてみてください。
8.2.2 現在のユーザー
本章での学び
current_userメソッド
セッションIDに対応する情報をデータベースから取得することができるようにするcurrent_user
メソッドを作成する。
例)セッションIDに対応するユーザ名を取得する。
<%= current_user.name %>
find_by
メソッドを使うことで、nilが返ってくることを防ぐ。
User.find_by(id: session[:user_id])
Railsの処理高速化のテクニック
User.find_by
の実行結果を、インスタンス変数に保存することで、データベースの読み込みは1回になる。
以降はインスタンス変数を返すようになる。
if @current_user.nil?
or equals代入演算子
||=
という代入演算子を使うことで、or演算子を短縮して書くことができる。
javaで言うところの、x = x + 1
を、x += 1
と書くのと同じイメージ。
短縮系 | 短縮しない場合 |
---|---|
x += 1 | x = x +1 |
@foo | | = “bar” |@foo = @foo || “bar” |
「変数の値がnilなら変数に代入するが、nilでなければ代入しない」という操作が、Rubyではよく行われる。
Rubyの王道
書ける処理は短縮形で書く。 or equals演算子を使うことで、以下の複数行の処理が1行で書ける。
if @current_user.nil? @current_user = User.find_by(id: session[:user_id]) else @current_user end
上のコードと同じ意味になる。 こちらがRubyの王道。
@current_user ||= User.find_by(id: session[:user_id])
演習1
Railsコンソールを使って、User.find_by(id: …)で対応するユーザーが検索に引っかからなかったとき、nilを返すことを確認してみましょう。
ユーザIDに存在しない100を指定して検索。 nilが返ってくることを確認。
>> User.find_by(id: '100') User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]] => nil
演習2
先ほどと同様に、今度は:user_idキーを持つsessionハッシュを作成してみましょう。リスト 8.17に記したステップにしたがって、||=演算子がうまく動くことも確認してみましょう。
ユーザIDが1のユーザ情報をもとに、sessionハッシュを生成する。
?> user = User.find_by(id: '1') User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2017-05-07 21:50:35", updated_at: "2017-05-07 21:50:35", password_digest: "$2a$10$esrzyVS.n7KGJ27bcs7UF.78tD7e75pMkwy6gnnwhlQ..."> >> session = { user_id: user.name, name: user.name, email: user.email } => {:user_id=>"Rails Tutorial", :name=>"Rails Tutorial", :email=>"example@railstutorial.org"}
リストの手順に従い、||=演算子を検証する。
ユーザーIDが1(というか最初のユーザ)でハッシュを生成する場合は、
session[:user_id] = User.first.id
でハッシュを生成したほうが楽。
?> session = {} => {} >> session[:user_id] = nil => nil >> @current_user ||= User.find_by(id: session[:user_id]) User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ? [["LIMIT", 1]] => nil >> session[:user_id] = User.first.id User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => 1 >> @current_user ||= User.find_by(id: session[:user_id]) User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2017-05-07 21:50:35", updated_at: "2017-05-07 21:50:35", password_digest: "$2a$10$esrzyVS.n7KGJ27bcs7UF.78tD7e75pMkwy6gnnwhlQ..."> >> @current_user ||= User.find_by(id: session[:user_id]) => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2017-05-07 21:50:35", updated_at: "2017-05-07 21:50:35", password_digest: "$2a$10$esrzyVS.n7KGJ27bcs7UF.78tD7e75pMkwy6gnnwhlQ...">
8.2.3 レイアウトリンクを変更する
本章での学び
ログアウト用リンク
ログアウト用のリンクは以下の通り、引数にmethod: : delete
を渡している。
Webブラウザは実際にはDELETEリクエストを発行できないため、RailsではJavaScriptを使って偽装している。
<%= link_to "Log out", logout_path, method: :delete%>
ユーザプロフィール用リンク
省略形でcurrent_user
を使う。
<%= link_to "Profile", current_user %>
Railsによって、以下のコードに自動変換される。
<%- link_to "Profile", user_path(current_user) %>
ドロップダウンメニューの作成
bootstrapのdropdown
クラスや、dropdown-menu
にて実現する。
これらのクラスを実装しただけでは、ドロップダウンメニューは表示されない。
bootstrapを有効にする
ドロップダウンメニューを有効にするために、application.js
にBootstrapのカスタムjsをインクルードするように指定する必要がある。
//= require bootstrap
railsを再起動すると、ドロップダウンメニューが表示される。
演習1
ブラウザのcookieインスペクタ機能を使って (8.2.1.1)、セッション用のcookieを削除してみてください。ヘッダー部分にあるリンクは非ログイン状態のものになっているでしょうか? 確認してみましょう。
chromeの開発ツールでcookie情報を表示する。
右クリック=>clear
でcookieを削除して、F5で更新。
ヘッダー部にあるリンクが非ログイン状態になることを確認。
演習2
もう一度ログインしてみて、ヘッダーのレイアウトが変わったことを確認してみましょう。その後、ブラウザを再起動させ、再び非ログイン状態に戻ったことも確認してみてください。注意: もしブラウザの [閉じたときの状態に戻す] 機能をオンにしていると、セッション情報も復元される可能性があります。もしその機能をオンにしている場合、忘れずにオフにしておきましょう (コラム 1.1)。
再度ログイン。ヘッダーのレイアウトが変わることを確認。
ブラウザをxで閉じてから再起動。ヘッダーのレイアウトがログイン前に戻ることを確認。
8.2.4 レイアウトの変更をテストする
本章での学び
テスト用データを生成する
fixtureファイルで作成することができる。 テストに必要なデータをtestデータベースに読み込んでおくことが可能。
fixture向けのdigestメソッドを定義する
fixtureで有効なメールアドレス、名前を設定しておく。
テスト中にそのユーザとしてログインするために、そのユーザの有効なパスワードも設定しておく。
password_digest属性をユーザのfixtureに追加するために、User
モデル内に、独自のdigest
メソッドを追加する。
bcryptによるパスワードのハッシュ化
has_secure_password
でbcryptのパスワードが生成されるため、同様の仕組みにする。
BCrypt::Password.create(string, cost: cost)
string
は、ハッシュ化する文字列。
cost
は、コストパラメータ。ハッシュを算出するための計算コストを指定する。コストパラメータの値を高くすることで、ハッシュからオリジナルのパスワードを計算で算出することは困難になる。
三項演算子によるコストパラメータの計算
テストにおいてはコストパラメータを高くする意味はない。
三項演算子で記載されたhas_secure_password
内の実装を参考にする。
参考演算子とは、
if boolean? 何かをする else 別のことをする end
上記の分岐を、1行で記載することができる。
論理値? ? 何かをする : 別のことをする
上記を踏まえ、has_secure_password
内の実装を見ると、ActiveModel::SecurePassword.min_cost
がtrueなら、BCrypt::Engine::MIN_COST
を使用する。つまり、テスト中はコストを最小にして、本番ではコストを明確に指定して計算する。
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
クラスメソッドとインスタンスメソッド
クラスメソッドは、クラスオブジェクトから実行可能なメソッド (Javaで言うところのstaticメソッド)
def self.method_name
で定義する。
今回のdigest
メソッドも、クラスメソッドで定義している。
(ユーザーごとのインスタンスで計算する必要はないため、インスタンスメソッドではなくクラスメソッドにしている)
インスタンスメソッドは、インスタンスオブジェクトから実行可能なメソッド
違いは以下を参照。 http://qiita.com/tbpgr/items/56eb65c0ea5882abbb07
ユーザーログインのテストで使うfixture
名前、メールアドレス、パスワードを定義する。 fixtureの中では、ERbを使用することができる。
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %>
fixtureの生パスワード
fixture内でpassword
属性を指定することはできない(DBのカラムにもpasswordがいない)ため、テストケース内で直接記載する。
fixtureのユーザが増えても、全員、同じパスワードの値(今回はpassword)を使用する。
テストシナリオのコード化
0.事前準備
users
とは、fixtureのファイル名users.yml
のこと。
users.yml
から、michaelユーザの情報を読み込み、インスタンス変数へ格納する。
def setup @user = users(:michael) end
1.ログイン用のパスを開く
get login_path
2.セッション用パスに有効な情報をpostする
assert_redirected_to @user
で、リダイレクト先が正しいかを確認する。
follow_redirect!
で、実際にリダイレクトする。
post login_path, params: { session: { email: @user.email, password: 'password' } } assert_redirected_to @user follow_redirect! assert_template 'users/show'
3.ログイン用リンクが表示されなくなったことを確認する
count: 0
を付与することで、assert_select
に渡したパターンに一致するリンクが0可動化を確認する。
assert_select "a[href=?]", login_path, count: 0
4.ログアウト用リンクが表示されていることを確認する
assert_select "a[href=?]", logout_path
5.プロフィール用リンクが表示されていることを確認する
assert_select "a[href=?], user_path(@user)"
テストファイル内の特定のテストのみ実行する
テストファイル内に複数のテストシナリオが存在する場合、テストファイル名とテスト名を指定することができる。
--name
で指定するテスト名は、接頭語に「test_」をつけて、test ‘…’ doの部分の単語をアンダースコアでつないだ文字列で指定する。
test "login with valid information" do ・・・略 end
yokoyan:~/workspace/sample_app (basic-login) $ rails test test/integration/users_login_test.rb --name test_login_with_valid_information Running via Spring preloader in process 3098 Started with run options --name test_login_with_valid_information --seed 3699 1/1: [=====================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.82456s 1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
演習1
試しにSessionヘルパーのlogged_in?メソッドから!を削除してみて、リスト 8.23が redになることを確認してみましょう。
#ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? # !current_user.nil? current_user.nil? end
テスト結果がredになることを確認。 ユーザーがログインしていても、trueを返せないためredになっている。
yokoyan:~/workspace/sample_app (basic-login) $ rails test test/integration/users_login_test.rb --name test_login_with_valid_information Running via Spring preloader in process 3164 Started with run options --name test_login_with_valid_information --seed 35997 FAIL["test_login_with_valid_information", UsersLoginTest, 0.8784956429954036] test_login_with_valid_information#UsersLoginTest (0.88s) Expected exactly 0 elements matching "a[href="/login"]", found 1.. Expected: 0 Actual: 1 test/integration/users_login_test.rb:29:in `block in <class:UsersLoginTest>' 1/1: [=====================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.88460s 1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
演習2
先ほど削除した部分 (!) を元に戻して、テストが greenに戻ることを確認してみましょう。
修正箇所をもとに戻す。
#ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end
テスト結果がgreenになることを確認。
yokoyan:~/workspace/sample_app (basic-login) $ rails test test/integration/users_login_test.rb --name test_login_with_valid_information Running via Spring preloader in process 3246 Started with run options --name test_login_with_valid_information --seed 37023 1/1: [=====================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.02458s 1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
8.2.5 ユーザー登録時にログイン
本章での学び
ユーザー登録時にログインする
Users
コントローラの、create
メソッド内で、log_in @user
を追加すればOK。
Sessionsコントローラがあることで、UsersコントローラでもSessionsHelper
で定義しているlog_in
メソッドが使える。
正確には、application_controller.rb
内で、SessionsHelper
をインクルードしているため、Usersコントローラでもlog_in
メソッドが使える。
def create @user = User.new(user_params) if @user.save log_in(@user) ・・・略 end
テストヘルパーメソッドの修正
ログイン中かどうかを確認するテストを追加する。
ログイン中かどうかの判定は、sessions_helper.rb
で定義したヘルパーメソッド、logged_in?
で取得できる。
# 現在ログイン中のユーザーを返す (いる場合) def current_user @current_user ||= User.find_by(id: session[:user_id]) end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end
しかし、ヘルパーメソッドはテスト内から呼び出すことができない。
そのため、test_helper.rb
内に新しく、is_logged_in?
を定義する。
sessionヘルパー内のcurrent_user
を呼び出せないため、代わりにsession
メソッドを使って実現する。
# テストユーザーがログイン中の場合にtrueを返す def is_logged_in? !session[:user_id].nil? end
ユーザー登録後のログインのテスト
1.ユーザー登録画面のパスを開く
get signup_path
2.ユーザー登録情報をpostして、ユーザー登録数が1増えているかを確認する
assert_difference 'User.count', 1 do post users_path , params: { { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" }} end
3.リダイレクトする
follow_redirect!
4.ユーザー情報表示画面のテンプレートが表示されることを確認する
assert_template 'users/show'
5.フラッシュメッセージ(エラーメッセージ)が無いことを確認する
assert_not flash.empty?
6.ログイン中かどうかを確認する
定義したis_logged_in?
ヘルパーメソッドを呼び出す。
assert is_logged_in?
演習1
リスト 8.25のlog_inの行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。
log_inをコメントアウトしてテストを実行。
if @user.save #log_in(@user)
redになることを確認。
原因はユーザ登録後、ログインしていないため。
assert is_logged_in?
でエラーになっている。
yokoyan:~/workspace/sample_app (basic-login) $ rails test Running via Spring preloader in process 2623 Started with run options --seed 59224 FAIL["test_valid_signup_information", UsersSignupTest, 1.0323550210014218] test_valid_signup_information#UsersSignupTest (1.03s) Expected false to be truthy. test/integration/users_signup_test.rb:39:in `block in <class:UsersSignupTest>' 23/23: [===================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.47616s 23 tests, 63 assertions, 1 failures, 0 errors, 0 skips
コメントアウトを戻すとgreenになることを確認。
演習2
現在使用しているテキストエディタの機能を使って、リスト 8.25をまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタのコメントアウト機能については Test Editor Tutorial の Commenting Out (英語) などを参照してみてください。
cloud9の場合、Ctrl+/
で複数行コメントアウトできる。
もちろんテストはredになる。
yokoyan:~/workspace/sample_app (basic-login) $ rails test Running via Spring preloader in process 2723 Started with run options --seed 55337 ERROR["test_layout_links", SiteLayoutTest, 0.8837061480007833] test_layout_links#SiteLayoutTest (0.88s) ActionView::Template::Error: ActionView::Template::Error: First argument in form cannot contain nil or be empty app/views/users/new.html.erb:6:in `_app_views_users_new_html_erb__3664408681092639721_27399160' test/integration/site_layout_test.rb:17:in `block in <class:SiteLayoutTest>' ERROR["test_invalid_signup_information", UsersSignupTest, 0.9015909680019831] test_invalid_signup_information#UsersSignupTest (0.90s) ActionView::Template::Error: ActionView::Template::Error: First argument in form cannot contain nil or be empty app/views/users/new.html.erb:6:in `_app_views_users_new_html_erb__3664408681092639721_26560060' test/integration/users_signup_test.rb:9:in `block in <class:UsersSignupTest>' ERROR["test_valid_signup_information", UsersSignupTest, 0.916260575002525] test_valid_signup_information#UsersSignupTest (0.92s) ActionView::Template::Error: ActionView::Template::Error: First argument in form cannot contain nil or be empty app/views/users/new.html.erb:6:in `_app_views_users_new_html_erb__3664408681092639721_25494440' test/integration/users_signup_test.rb:29:in `block in <class:UsersSignupTest>' ERROR["test_login_with_valid_information", UsersLoginTest, 1.090877412003465] test_login_with_valid_information#UsersLoginTest (1.09s) ActionView::Template::Error: ActionView::Template::Error: undefined method `name' for nil:NilClass app/views/users/show.html.erb:1:in `_app_views_users_show_html_erb___1927824141870267168_46476740' test/integration/users_login_test.rb:27:in `block in <class:UsersLoginTest>' ERROR["test_should_get_new", UsersControllerTest, 1.1993190759967547] test_should_get_new#UsersControllerTest (1.20s) ActionView::Template::Error: ActionView::Template::Error: First argument in form cannot contain nil or be empty app/views/users/new.html.erb:6:in `_app_views_users_new_html_erb__3664408681092639721_38933240' test/controllers/users_controller_test.rb:5:in `block in <class:UsersControllerTest>' 23/23: [===================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.20674s 23 tests, 43 assertions, 0 failures, 5 errors, 0 skips
再度コメントアウトを戻して、テストがgreenになることを確認。
yokoyan:~/workspace/sample_app (basic-login) $ rails test Running via Spring preloader in process 2801 Started with run options --seed 61736 23/23: [===================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.26744s 23 tests, 63 assertions, 0 failures, 0 errors, 0 skips
おわりに
ユーザ登録、ログイン機能がひと通り完成しました。 RailsはSQLを意識しなくても、データベースのCRUD処理ができてしまうのがすごいなあと感じます。 その分、中で何が起こっているのかを理解するために、もう一歩踏み込みながら学習したいと思います。