認証システムと認可モデル
認証システム(Authentification System)
以下のような仕組みのことを言います。
- Webブラウザが、ログインしている状態を保持する
- ユーザーによってWebブラウザが閉じられたら状態を破棄する
認可モデル(Authorization Model)
以下のような仕組みのことを言います。
- ログイン済みのユーザー(current user)だけがアクセスできるページの制御
- ログイン済みのユーザーが扱える機能の制御
認証システムと認可モデルを基盤として実現する機能
以下のような機能は、認証システムと認可モデルを基盤として実現される機能です。
- ログインしているユーザーのみが、ユーザー一覧のページへ移動できるようにする
- 正当なユーザーだけが自分のプロフィール情報を編集できるようにする
- 管理者だけが他のユーザーをデータベースから削除できるようにする
また、以下のような機能の実装には、「誰がログインしているのか」という情報が必須となります。
- 他のユーザーをフォローする機能
- 自分のフィード一覧
Sessionsコントローラ
HTTPはステートレスなプロトコルである
「ステートレス」というのは、「状態を持たない」という意味です。「状態を持たない」という文面には、以下の事柄が含意されます。
- HTTPのリクエストは、1つ1つが独立したものとして捉えられる
- 以前のリクエストの情報は全く利用できないように構築されている
上述の特性ゆえに、HTTPというプロトコルの枠組み内には、「Webブラウザで特定のページから別のページに移動した場合にユーザーのIDを保持する」ための手段は全くありません。そうした機能が欲しいならば、HTTPの枠外で準備する必要があるのです。
HTTPの枠外でユーザーのログイン情報を保持する - セッション
ユーザー端末のWebブラウザとWebアプリケーションのサーバーの間では、HTTPとは別個に、セッションと呼ばれる半永久的な接続が設定されます。セッションに関する仕組みは、RailsをはじめとしたWebアプリケーションの側で実装されます。
セッションはHTTPとは階層が異なる(HTTPより上位の階層に属する)アプリケーションの機能なので、HTTPの枠外でユーザーのIDを保持する機能を実現することができます。
余談 - 郵便チェスで説明するHTTPとセッションの関係
Railsチュートリアルの本文では、訳注として、郵便将棋の事例が紹介されています。その注釈を見て私が思い出したのは「郵便チェス」というゲームの存在です。「HTTPとセッションの関係は、郵便システムと郵便チェスの関係にたとえることができる」というのは目から鱗でした。
郵便チェスにおいて、HTTPに相当するのは郵便システム、セッションに相当するのは郵便はがきに書かれた本文です。重要なポイントは以下になります。
- 郵便チェスにおいては、郵便配達人がチェスのルールや対局の進行状況、対局の内容について知っている必要がない(ステートレス)
- 対局者相互が盤の状態を保持していれば郵便チェスは成立する(セッション)
Cookieとは
Cookieは、Railsその他多くのWebアプリケーションにおいて、セッション機能を実現するために一般に使われる技術です。Cookieの実体は、ユーザーのWebブラウザに保存される小さなテキストファイルです。
Cookieの内容は、あるページから別のページに移動したときにも破棄されません。よって、Cookieを利用することにより、「Webブラウザで特定のページから別のページに移動した場合にユーザーのIDを保持する」という要求を達成することができます。Webアプリケーションは、Cookieの内容を用いて、例えばログイン中のユーザーが所有する情報をRDBから取り出すことができます。
セッションに関するRailsのメソッド
session
メソッド
memcachedというツールを基盤技術として、オンメモリでセッション情報を保持するためのメソッドです。session
メソッドにより生成されたセッション情報は、Webブラウザを閉じると自動的に破棄されます。
なお、「オンメモリ」の裏では、実は結構複雑な仕組みが動いています。そのあたりの仕組みに興味があるのであれば、@zettaittenaniさんのRails の session を完全に理解した - Qiita読んでみるのがいいかと思います。
cookies
メソッド
ファイルとしてのCookieを用いた、session
メソッドによるものより生存期間が長いセッションを生成するメソッドです。Railsチュートリアルに置いては、今後の学習内容となります。
RESTfulなセッション
セッション情報についても、ユーザー情報と同様にRESTfulなリソースとしてモデリングすることが可能です。具体的には、以下のような考え方になります。
- ログインページでは、
new
で新しいセッションを生成・保存する - ログアウトすると
destroy
でセッションを削除する
「情報の取り扱い方法に共通性をもたせることができる」というのは便利です。実際に、この後セッション機能を実装するために、Usersコントローラと同様のSessionsコントローラを実装していきます。
一方で、セッション情報とユーザー情報ではデータの保存場所が異なります。
- ユーザー情報を永続化したものはサーバー側のRDBに保存される
- セッション情報を永続化したものはクライアント側のCookieに保存される
Sessionsコントローラ
Railsにおいて、セッション情報をRESTfulなリソースとして実装する…となるとまず必要になるのはコントローラですね。
Sessionsコントローラに必要となる動作は以下のとおりです。
- ログイン情報入力フォームを処理する
new
アクション -
new
からPOST
リクエストを受け、実際のログイン(セッション生成)処理を行うcreate
アクション -
DELETE
リクエストを受け、実際のログアウト(セッション破棄)処理を行うdestroy
アクション
セッション情報については、RDB等が介在するわけではないため、Active RecordsライブラリやORM、モデルオブジェクトといった要素は登場しません。代わりに登場するのはActive Modelライブラリです。
Sessionsコントローラの生成
Sessionsコントローラの生成には、例によってrails generate
コマンドを使用します。
まずはじめに、new
アクションが定義されたSessionsコントローラを生成してみましょう。
# rails generate controller Sessions new
Running via Spring preloader in process 367
create app/controllers/sessions_controller.rb
route get 'sessions/new'
invoke erb
create app/views/sessions
create app/views/sessions/new.html.erb
invoke test_unit
create test/controllers/sessions_controller_test.rb
invoke helper
create app/helpers/sessions_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/sessions.coffee
invoke scss
create app/assets/stylesheets/sessions.scss
new
アクションに対応するビューには、Railsチュートリアルの図 8.1のような内容のフォームを実装していきます。
なぜrails generate
でnew
のみを生成したのか
rails generate
でアクションを生成すると、対応するビューも生成されます。
create app/views/sessions/new.html.erb
今回実装するSessionsリソースの場合、create
やdestroy
に対応するビューは必要ありません。無駄なビューが生成されることを防ぐために、rails generate
で生成するのはあえてnew
のみにした、ということです。
Sessionsコントローラに対応するルーティングを定義する
今回必要になるのは、sessions
のnew
・create
・destroy
に対するルーティングとなります。それぞれに対して割り付けるエンドポイントとリクエスト、対応する名前付きルートは以下の通りになります。
アクション | エンドポイント | リクエスト | 名前付きルート |
---|---|---|---|
sessions#new |
/login | GET |
login_path |
sessions#create |
/login | POST |
login_path |
sessions#destroy |
/logout | DELETE |
logout_path |
config/routes.rb
における、対応する変更は以下の通りになります。
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
post '/signup', to: 'users#create'
+ get '/login', to: 'sessions#new'
+ post '/login', to: 'sessions#create'
+ delete '/logout', to: 'sessions#destroy'
resources :users
end
rails generate
により、上述以外のルーティングも生成されますが、上述以外のルーティングは削除して構わないそうです。
rails routes
コマンド
Railsにおいて、現状で定義済みのルーティングの一覧を表示するコマンドです。例えば、現状のsampleアプリケーションで定義されているルーティングの一覧であれば以下のようになります。
# rails routes
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
help GET /help(.:format) static_pages#help
about GET /about(.:format) static_pages#about
contact GET /contact(.:format) static_pages#contact
signup GET /signup(.:format) users#new
POST /signup(.:format) users#create
login GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout DELETE /logout(.:format) sessions#destroy
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
Sessionsコントローラに対するテストを修正する
rails generate
により、テストの骨格は生成されました。
invoke test_unit
create test/controllers/sessions_controller_test.rb
しかしながら、new
アクションで発行されるGET
リクエストに対するアクセス先は変更する必要があります。具体的には、www.example.com/sessions/new
ではなく/login
にアクセスするようにします。違いは以下ですね。
- フルURLではなく、アプリケーションルートを基準にする
- エンドポイントは
/sessions/new
ではなく/login
である
require 'test_helper'
class SessionsControllerTest < ActionDispatch::IntegrationTest
test "should get new" do
- get sessions_new_url
+ get login_path
assert_response :success
end
end
演習 - Sessionsコントローラ
1. GET login_path
とPOST login_path
との違いを説明できますか? 少し考えてみましょう。
GET
というのは、何らかの永続化されたリソースを読み込むためのリクエストとなります。永続化されたリソース(今回であればセッション情報)には何らの変更も行いません。また、永続化されたリソースの内容は、同じリクエストを何回送出しても同じになります。GET login_path
であれば、sessions#new
に割り付けられた静的ページを読み込む動作を行います。
POST
というのは、永続化されたリソースを新たに生成するためのリクエストとなります。永続化されたリソース(今回であればセッション情報)の変更を伴います。また、リクエストを送出するたびに新たなリソースが生成されます。POST login_path
であれば、sessions#create
に割り付けられた処理によって新たなセッション情報を生成する動作を行います。
2. Sessionsリソースに関するルーティングだけを表示させてみましょう。現在、いくつのSessionsリソースがあるでしょうか?
# rails routes | grep "sessions"
login GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout DELETE /logout(.:format) sessions#destroy
Sessionsリソースの数は3つです。new
・create
・destroy
ですね。
ターミナルのパイプ機能を使って
rails routes
の実行結果とgrep
コマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。
# rails routes | grep "users"
signup GET /signup(.:format) users#new
POST /signup(.:format) users#create
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
ログインフォーム
Sessionsリソースに対し、ここまででコントローラとルーティングの定義が完了しました。続いてはビューの定義です。
ログイン失敗時の動作
ログインフォームで入力した情報に誤りがあった場合、エラーメッセージを含むログインページを表示するようにします。このときのログイン画面は、Railsチュートリアルの図 8.2のようなものを目指して実装していきます。
Usersリソースにおけるエラーメッセージは、Active Recordsの機能を使って実装してきました。しかしながら、Sessionsリソースは、Usersリソースの場合と異なり、RDBを扱いません。RDBを扱わないということは、Active Recordsも使っていないということです。Active Recordsの機能が使えないので、エラーを表示するメカニズムは、自分で実装する必要があります。
今回は、フラッシュメッセージを使ってエラーを表示するメカニズムを実装することとします。
Sessionsリソースにモデルオブジェクトは存在しない
Usersリソースとは異なり、Sessionsリソースにモデルオブジェクトは存在しません。よって、@user
のようなインスタンス変数に相当するものも存在しません。
form_for(@user)
以上のような書き方をすること、あるいはインスタンス変数に基づく対象リソースの自動判定機能1を使うことはできないのです。
これは何を意味するのか。「リソースの名前と、それに対応するURLを、具体的に指定して渡さなければならない」ということです。
具体的なコードとしては以下の通りになります。
form_for(:session, url: login_path)
「シンボルとオプションハッシュを引数とするメソッド」であることがわかりますね。
実際のログインフォームのコード
実際のログインフォームのコードは以下の様になります。
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
ユーザー登録ページへのリンク
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
「ユーザー登録ページへのリンクが記述されている」というのは一つのポイントです。「まだ登録されていない新規ユーザーが直接ログインページに到達した場合の配慮」といった配慮は、Webサイト訪問者のユーザー経験の質を高めることに繋がります。
実際にユーザー登録ページを表示してみる
ここまで完了したところで、実際にユーザー登録ページを表示してみます。結果は以下のようになります。
現時点では、以下の制限事項があります。
- アドレスバーに直接 /login とURLを直接入力しなければならない
- 現時点では、Homeページ他に存在する[Log in]リンクがまだ効かないため
- 「Log in」ボタンをクリックしても動作しない
- /login へのPOSTに対する動作がまだ実装されていないため
生成されたログインフォームのHTMLコードは以下のようになります。
<form action="/login" accept-charset="UTF-8" method="post" >
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="I3wZBVVMzlbS5CR3TOMPpgtOYt0YrXJPDqoanRLMQk6DjGW0ebhy8FK6eFOBStDHtBDMjtGCRqVehwxG3b/uBA==">
<label for="session_email">Email</label>
<input class="form-control" type="email" name="session[email]" id="session_email">
<label for="session_password">Password</label>
<input class="form-control" type="password" name="session[password]" id="session_password" data-kpxc-id="session_password">
<input type="submit" name="commit" value="Log in" class="btn btn-primary" data-disable-with="Log in">
</form>
このHTMLコードから、POST
メソッドが送出された際のHTTPリクエストからparams
ハッシュへの割付けの中身は、以下のような形になることが推測できます。
- Emailフィールドの内容→
params[:session][:email]
- Passwordメソッドの内容→
params[:session][:password]
演習 - ログインフォーム
1. リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreate
アクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。
<form action="/login" accept-charset="UTF-8" method="post">
....略
</form>
Rails.application.routes.draw do
# ...略
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
# ...略
end
以上のソースコードが要点です。
- フォームのHTMLソースから、このフォームにsubmitすると、 /login というエンドポイントに対して
POST
リクエストが送出されることがわかる -
config/routes.rb
の記述内容により、 /login というエンドポイントにPOST
リクエストが送出されると、Railsアプリケーションはそのリクエストをsessions#create
というアクションで受ける
以上の動作により、「設問で言及されているフォームで送信すると、Sessionsコントローラのcreate
アクションに到達する」という動作になるわけです。
ユーザーの検索と認証
ログイン時のセッション機能を実装する上でまずはじめに行うことは、入力が無効な場合の処理を実装することです。ユーザー登録の場合と異なり、実装前に既存のセッション情報が必要とされることはありません。
Sessionsコントローラの最低限の定義
class SessionsController < ApplicationController
def new
end
def create
+ render 'new'
end
def destroy
end
end
以上は、Sessionsコントローラの最低限の定義を記述したコードです。以下3つのアクションの存在が定義されています。
-
new
アクション -
create
アクション -
destroy
アクション
また、create
アクションにおいては、「アクションを実行するとnew
ビューが出力される」という動作を実装しています。
現時点におけるparams
ハッシュの中身
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
...略
session: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
email: user@foobar.com
password: foobar
commit: Log in
controller: sessions
action: create
params
ハッシュは、中にparams[:session]
ハッシュを含む入れ子構造となっています。
{ session: { email: 'user@foobar.com', password: 'foobar' } }
{ email: 'user@foobar.com', password: 'foobar' }
params[:session][:email] # => "user@foobar.com"
params[:session][:password] # => "foobar"
認証に必要なすべての情報は、params
ハッシュから取り出すことができます。
ユーザーをデータベースから見つけて検証する
Active Recordのfind_by
メソッドと、has_secure_password
のauthenticate
メソッドを使って実装します。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# TODO: ユーザーログイン後にユーザー情報のページにリダイレクトする
else
# TODO: エラーメッセージを作成する
render 'new'
end
end
def destroy
end
end
コードの意味
user = User.find_by(email: params[:session][:email].downcase)
フォームから送信されたメールアドレスをキーとして、RDBからユーザー情報を取り出すコードです。
downcase
というのは、有効なメールアドレスが入力されたときに確実にマッチするようにするために用いるメソッドです。過去の実装にて「メールアドレスのアルファベットは全て小文字に変換して保存する」という処理を行ったことに対応します。
user && user.authenticate(params[:session][:password])
ユーザー情報が存在し、パスワードが正しいかどうかを検証するコードです。
&&
は論理積を表す演算子です。また、Rubyでは、「nil
とfalse
以外の全てのオブジェクトは、真理値として評価するとtrue
になる」という性質があります。そのため、以下の表から、「入力したメールアドレスに対応するユーザー情報が存在し、かつパスワードが一致する場合のみ、この式の真理値はtrue
になる」ということになります(パスワードが正しくない場合、authenticate
メソッドの戻り値はfalse
になることに注意してください)。
User | Password | a && b |
---|---|---|
存在しない | 任意 | (nil && [any object]) == false |
有効なユーザー | 誤ったパスワード | (true && false) == false |
有効なユーザー | 正しいパスワード | (true && gtrue) == false |
演習 - ユーザーの検索と認証
Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめてみましょう. まずはuser = nil
の場合を、次にuser = User.first
とした場合を確かめてみてください。
ヒント: 必ず論理値オブジェクトとなるように、4.2.3で紹介した!!のテクニックを使ってみましょう。例:
!!(user && user.authenticate('foobar'))
>> user = nil
=> nil
>> !!(user && user.authenticate('foobar'))
=> false
>> user = User.first
>> !!(user && user.authenticate('foobar'))
=> true
フラッシュメッセージを表示する
前提
Userリソースとは異なり、Sessionリソースは特定のActive Recordオブジェクトに関連付けられていません。そのため、「Active Recordに用意された枠組みを用いてエラーメッセージ表示機能を実装する」という手段は使えません。
今回は、「フラッシュメッセージによってログイン時のエラーメッセージ表示機能を実装する」という方法をとることとします。
実装(誤りあり)
フラッシュメッセージによるエラーメッセージ表示機能を実装します。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# TODO: ユーザーログイン後にユーザー情報のページにリダイレクトする
else
- # TODO: エラーメッセージを作成する
+ flash[:danger] = 'Invalid email/password combination' # XXX:このコードには誤りあり
render 'new'
end
end
def destroy
end
end
ただしこのコードは、あえて誤りがある形で実装されています。このコードの誤りについては、後述「フラッシュのテスト」で修正していきます。
フラッシュメッセージが表示されない
実際にログインに失敗してみます。
Railsチュートリアルの記述では、ここでフラッシュメッセージが表示されるはずです。しかしながら、フラッシュメッセージは表示されません。
何が原因かといいますと、app/views/layouts/application.html.erb
の記述ミスが原因です。
...略
<% flash.each do |message_type, message| %>
- <% content_tag(:div, message, class: "alert alert-#{message_type}") %>
+ <%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
<% end %>
...略
埋め込みRubyでcontent_tag
メソッドを書く場合、書き出しは<%
ではなく<%=
でなければなりません。「HTMLとしてのレンダリングを含むから」というのが理由ですね。
ログインページを抜けてもフラッシュメッセージが表示されたまま
(このままでは学習が先に進まないので、ひとまず)先ほどの不具合をfixした上で、改めてログインに失敗してみます。
今度はフラッシュメッセージがきちんと表示されました。
ここでHomeページヘのリンクをクリックしてみましょう。
Homeページでもフラッシュメッセージが表示されたままです。これは想定した動作ではないですね。
フラッシュのテスト
想定した動作ではない。「エラーをキャッチするテストを先に書いて、そのエラーが解決するようにコードを書く」というルールが適用される場面です。早速、ログインフォームの送信についての簡単な統合テストを書いてみます。
テストの生成
# rails generate integration_test users_login
Running via Spring preloader in process 548
invoke test_unit
create test/integration/users_login_test.rb
統合テストなので、テストを生成するコマンドはrails generate integration_test
です。
実際のテストコードの記述とその内容
class UsersLoginTest < ActionDispatch::IntegrationTest
test "login with invalid information" do
get login_path
assert_templete 'sessions/new'
post login_path, params: { session: { email: "", password: ""} }
assert_templete 'sessions/new'
assert_not flash.empty?
get root_path
assert flash.empty?
end
このテストコードでは、以下の事柄についてテストを行っています。
-
login_path
に対してGET
リクエストを実行した場合に、sessions/new
テンプレートが正しく返ってくること -
login_path
に対して正しくないメールアドレスとパスワードの組み合わせでPOST
リクエストを実行した場合に、以下の条件がすべて満たされていること-
sessions/new
テンプレートが正しく返ってくること -
flash
変数が空でないこと
-
- さらにその後に別のエンドポイント(この場合は
root_path
)にGET
リクエストを実行した場合に、flash
変数が空であること
テストを実行してみる、そして通らない
rails test
の引数にファイルパスを与えれば、指定したテストファイルのみに対してテストを実行することができます。今回は、rails test test/integration/users_login_test.rb
としてテストを実行してみましょう。
# rails test test/integration/users_login_test.rb
Running via Spring preloader in process 645
Started with run options --seed 43137
FAIL["test_login_with_invalid_information", UsersLoginTest, 1.3804758000042057]
test_login_with_invalid_information#UsersLoginTest (1.38s)
Expected false to be truthy.
test/integration/users_login_test.rb:11:in `block in <class:UsersLoginTest>'
1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.38317s
1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
現状でテストは通りません。テストが通らなかった場所はtest/integration/users_login_test.rb:11
とあります。
assert flash.empty? # 11行目
「Homeページでもフラッシュメッセージが表示されたまま」という不具合にきちんと対応していますね。
テストが通るようにする
このテストが通るようにするには、app/controllers/sessions_controller.rb
のflash
をflash_now
に変更すればOKです。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# TODO: ユーザーログイン後にユーザー情報のページにリダイレクトする
else
- flash[:danger] = 'Invalid email/password combination' # XXX:このコードには誤りあり
+ flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
改めてテストを実行してみます。
# rails test test/integration/users_login_test.rb
Running via Spring preloader in process 658
Started with run options --seed 26869
1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.80917s
1 tests, 4 assertions, 0 failures, 0 errors, 0 skips
今度こそテストが通りました。
flash.now
とは
flash.now
とは、「次にアクションが実行されるまでの間に限り、フラッシュメッセージが表示されるようにする」という動作をするメソッドです。
-
flash.now
の直後にrender
が実行されると、フラッシュメッセージが表示された状態のHTMLが生成される-
render
はアクションではないため、flash.now
の生存条件を満たす
-
-
flash.now
の後にget
やredirect_to
が実行されると、以降フラッシュメッセージは表示されなくなる-
get
やredirect_to
はアクションであり、flash.now
の生存条件を満たさない
-
余談 - 「フラッシュメッセージがHTMLとして正しく表示されているか」のテストを追加する
先ほどの「フラッシュメッセージがHTMLとして正しく表示されていない」というのも、「エラーをキャッチするテストを先に書いて、そのエラーが解決するようにコードを書く」というルールが適用される場面ですね。実際にテストを書いてみます。
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
test "login with invalid information" do
get login_path
assert_template 'sessions/new'
post login_path, params: { session: { email: "", password: ""} }
assert_template 'sessions/new'
assert_not flash.empty?
+ assert_select 'div.alert-danger'
get root_path
assert flash.empty?
end
end
テストが通らない場合
<!DOCTYPE html>
<html>
...略
<body>
<%= render 'layouts/header' %>
<div class="container">
<% flash.each do |message_type, message| %>
+ <% content_tag(:div, message, class: "alert alert-#{message_type}") %>
<% end %>
...略
</div>
</body>
</html>
rails test test/integration/users_login_test.rb
を実行してみます。
# rails test test/integration/users_login_test.rb
Running via Spring preloader in process 684
Started with run options --seed 58388
FAIL["test_login_with_invalid_information", UsersLoginTest, 2.4125660999998217]
test_login_with_invalid_information#UsersLoginTest (2.41s)
Expected at least 1 element matching "div.alert-danger", found 0..
Expected 0 to be >= 1.
test/integration/users_login_test.rb:10:in `block in <class:UsersLoginTest>'
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.42038s
1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
テストは通りません。
テストログには以下のようにあります。
Expected at least 1 element matching "div.alert-danger", found 0..
Expected 0 to be >= 1.
test/integration/users_login_test.rb:10:in `block in <class:UsersLoginTest>'
alert-danger
というクラスが定義されたdiv
要素が存在しない、という意味ですね。ソースの不具合をきちんと捉えてくれています。
テストが通る場合
<!DOCTYPE html>
<html>
...略
<body>
<%= render 'layouts/header' %>
<div class="container">
<% flash.each do |message_type, message| %>
- <% content_tag(:div, message, class: "alert alert-#{message_type}") %>
+ <%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
<% end %>
...略
</div>
</body>
</html>
改めてrails test test/integration/users_login_test.rb
を実行してみます。
# rails test test/integration/users_login_test.rb
Running via Spring preloader in process 697
Started with run options --seed 30877
1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.92592s
1 tests, 5 assertions, 0 failures, 0 errors, 0 skips
今度こそテストが通りました。
演習 - フラッシュのテスト
1. 8.1.4の処理の流れが正しく動いているかどうか、ブラウザで確認してみてください。特に、flashがうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。
まず、 /login をブラウザで開いてみます。
現時点でフラッシュメッセージは表示されていません。Email・Password、いずれも入力せずに「Log in」ボタンをクリックしてみます。
「Invalid email/password combination」というエラーメッセージを含むフラッシュメッセージが表示されました。続いて、Homeへのリンクをクリックしてみましょう。
フラッシュメッセージが表示されなくなりました。想定通りの挙動です。
以下はrails server
のログです。
Started GET "/login" ...略
Processing by SessionsController#new as HTML
Rendering sessions/new.html.erb within layouts/application
...略
Completed 200 OK in 672ms (Views: 638.4ms | ActiveRecord: 0.0ms)
Started POST "/login" ...略
Processing by SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"GnMb9uYd/DvOG6bQmzX65PzE480s5TCxzTRqk0W8mxq6g2dHyulAnU5F+vRWnCWFQ5pNnuXKBFudGXxIis83UA==", "session"=>{"email"=>"", "password"=>"[FILTERED]"}, "commit"=>"Log in"}
User Load (5.9ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", ""], ["LIMIT", 1]]
Rendering sessions/new.html.erb within layouts/application
...略
Completed 200 OK in 770ms (Views: 793.9ms | ActiveRecord: 5.9ms)
Started GET "/" ...略
Processing by StaticPagesController#home as HTML
Rendering static_pages/home.html.erb within layouts/application
...略
Completed 200 OK in 539ms (Views: 491.2ms | ActiveRecord: 0.0ms)
ポイントは以下です。
- 以下3つのリクエストが送出されている
-
GET "/login"
…最初にログイン画面を表示した際 -
POST "/login"
…ログイン「Log in」ボタンをクリックした際 -
GET "/"
…ログイン失敗後にHomeへのリンクをクリックした際
-
-
POST
リクエストの結果が以下のようになっている-
sessions/new.html.erb
のレンダリングが行われている -
sessions/new.html.erb
のレンダリングに際し、別のHTTPリクエストが送出されていない -
302 Found
で終わっていない
-
-
当該例でいえば、「このフォームの
action
は、/usersというURLに対するPOST
動作である」というのが自動で判定されます。 ↩