Ruby on Rails Tutorial (11)
Chapter 8 突入。とりあえず branch をナニ。
$ git checkout -b sign-in-out
8.1 Sessions and signin failure
そうか signin/signout ということはセッション云々になるのか。
- new session のために signin page を出す
- signin で session が create される
- signout で その session が destroy される
cookie-based なソレになるのは次の章らしい。あら、次の節なのかな。
8.1.1 Sessions controller
とりあえず Sessions controller あたりのソレを generate しなさいとのこと。
$ rails generate controller Sessions --no-test-framework create app/controllers/sessions_controller.rb invoke erb create app/views/sessions invoke helper create app/helpers/sessions_helper.rb invoke assets invoke coffee create app/assets/javascripts/sessions.js.coffee invoke scss create app/assets/stylesheets/sessions.css.scss $ rails generate integration_test authentication_pages invoke rspec create spec/requests/authentication_pages_spec.rb
signin な page の mock が提示されてて signin_path って名前が付くよ、とのこと。そしてとりあえず以下な試験を書きなさい、とのこと。
require 'spec_helper' describe "Authentication" do subject { page } describe "signin page" do before { visit signin_path } it { should have_selector('h1', text: 'Sign in') } it { should have_selector('title', text: 'Sign in') } end end
とりあえず試験 red を確認しておきます。
$ bundle exec rspec spec/requests/authentication_pages_spec.rb Rack::File headers parameter replaces cache_control after Rack 1.5. FF Failures: 1) Authentication signin page Failure/Error: before { visit signin_path } NameError: undefined local variable or method `signin_path' for #<RSpec::Core::ExampleGroup::Nested_1::Nested_1:0x00000005b2f548> # ./spec/requests/authentication_pages_spec.rb:8:in `block (3 levels) in <top (required)>' 2) Authentication signin page Failure/Error: before { visit signin_path } NameError: undefined local variable or method `signin_path' for #<RSpec::Core::ExampleGroup::Nested_1::Nested_1:0x00000003111450> # ./spec/requests/authentication_pages_spec.rb:8:in `block (3 levels) in <top (required)>'
試験 green のためにまず route を云々。RESTful な route のために以下をナニ。
resources :sessions, only: [:new, :create, :destroy]
あと、signin および signout な route も追加。
match '/signin', to: 'sessions#new' match '/signout', to: 'sessions#destroy', via: :delete
via というのは HTTP の DELETE メソドを、という意図かな。そうみたい。ええと上を盛り込んで保存した後に rake routes を確認してみます。追加分のみ以下にて。
sessions POST /sessions(.:format) sessions#create new_session GET /sessions/new(.:format) sessions#new session DELETE /sessions/:id(.:format) sessions#destroy signin /signin(.:format) sessions#new signout DELETE /signout(.:format) sessions#destroy
上三つが resource で定義されるヤツで下二つが match で追加したヤツ。次に Sessions controller に new なアクションを定義、とのこと。つうか何を間違えたのか現状空っぽだったりしますね。とりあえず以下な状態にしておいて
class SessionsController < ApplicationController def new end def create end def destroy end end
あとは app/views/sessions/new.html.erb をナニ。ちなみに app/views/sessions も空ですね。むむ。
<% provide(:title, "Sign in") %> <h1>Sign in</h1>
これで試験してみるとどうなるかというと green 終了ですね。ということで次。
8.1.2 Signin tests
signin 失敗時の flash なエラーメセジは以下なカンジらしい。
Invalid email/password combination
ので以下の試験を追加せよ、との由。
describe "signin" do before { visit signin_path } describe "with invalid information" do before { click_button "Sign in" } it { should have_selector('title', text: 'Sign in') } it { should have_selector('div.alert.alert-error', text: 'Invalid') } end end
あるいは signin 成功時の mock が出てますね。これを踏まえて成功時のソレは以下とのこと。
describe "with valid information" do let(:user) { FactoryGirl.create(:user) } before do fill_in "Email", with: user.email fill_in "Password", with: user.password click_button "Sign in" end it { should have_selector('title', text: 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
むむ、どうなっていなければ、なのかも非常に分かりやすいですね。
8.1.3 Signin form
以下に app/views/sessions/new.html.erb を変更とのこと。
<% 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>
現時点で signin なページへのリンクってあるのだったかな。確認してみます。あ、まだリンクが修正できていないですね。/signin にアクセスしたら出力されました。
ちなみに上記 form_for のおかげでフォームに入力されるテキストについては param[:session][:email] などでアクセスできる状態になる、とのこと。このあたりは突っ込んで確認入れた方が良いのかどうなのか。
8.1.4 Reviewing form submission
最初の create なアクションを書いてみる、とのこと。
def create render 'new' end
これは /sessions にアクセスすれば良いのかな。WEBrick 再起動してアクセスしてみたら routing error ってなりますね。何が悪いのかな。
あ、POST /sessions が sessions#create に routing されてますね。テキスト見たら signin (/sessions/new) から submit って書いてあるじゃんorz
/sessions/new からテキストボックス空で submit して以下な debug 出力を確認しています。
--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess session: !ruby/hash:ActiveSupport::HashWithIndifferentAccess email: '' password: '' commit: Sign in action: create controller: sessions
で、次の段階としては存在チェックして云々だろ、とのことなのかな。
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
とりあえず盛り込んでおきます。
8.1.5 Rendering with a flash message
まずは以下なエラー処理を盛り込みましょう、ということな模様。
flash[:error] = 'Invalid email/password combination' # Not quite right! render 'new'
これで再度 /sessions/new からの挙動を確認。flash なメセジが出ました。てことは試験が一つはパスするのかどうか。
$ $ bundle exec rspec spec/requests/authentication_pages_spec.rb \ -e "signin with invalid information"
green 終了確認です。ちなみにそこから Home に戻ったら flash 消してね、な試験を追加せよ、とのことです。
describe "after visiting another page" do before { click_link "Home" } it { should_not have_selector('div.alert.alert-error') } end
再度試験実行して red を確認。
flash[:error] = 'Invalid email/password combination' # Not quite right!
を
flash.now[:error] = 'Invalid email/password combination'
するんだそうで。これも別途確認しておきましょうね。とりあえず試験 green は確認しました。
8.2 Signin success
正常系の盛り込み。create アクションのソレを以下に、とのこと。
sign_in user redirect_to user
sign_in というメソドが追加になるのかどうか。
8.2.1 Remember me
その前に user model について云々してます。とりあえず application_controller で SessionHelper というモジュールを include する模様。
class ApplicationController < ActionController::Base protect_from_forgery include SessionsHelper end
これ、view ではデフォで使えるようなんですが、controller ではそうではないので云々、とのこと。以降で session id なナニを云々してます。ここでは user model に remember_token という名前で保持するようですね。つうことは user model に属性が一つ追加になるのか。
model な試験に以下を追加せよ、とのこと。
it { should respond_to(:password_confirmation) } it { should respond_to(:remember_token) } it { should respond_to(:authenticate) }
remember_token の行が追加分です。で、migration 追加とのこと。
$ rails generate migration add_remember_token_to_users invoke active_record create db/migrate/20130125073955_add_remember_token_to_uses.rb
rails generation て typo したのは秘密。
中身を以下に修正。
class AddRememberTokenToUses < ActiveRecord::Migration def change add_column :users, :remember_token, :string add_index :users, :remember_token end end
find_by ってするときには index にしとかないと、なんですね。何でもできるのか、って思ってました (ぇ
で、以下を実行して
$ bundle exec rake db:migrate invoke active_record create db/migrate/20130125073955_add_remember_token_to_uses.rb rms@rms-ThinkPad-Edge:~/Documents/rails_proj/sample_app$ bundle exec rake db:migrate == AddRememberTokenToUses: migrating ========================================= -- add_column(:users, :remember_token, :string) -> 0.0176s -- add_index(:users, :remember_token) -> 0.0386s == AddRememberTokenToUses: migrated (0.0566s) ================================ $ bundle exec rake db:test:prepare
む、これって某所の試験なソレに盛り込まれていないですね。別途盛り込んでおきましょうね。
話を元に戻してこの状態で試験実行らしい。
$ bundle exec rspec spec/models/user_spec.rb
model だけならこれで試験 green なのか。引き続き試験を追加。remember_token な属性については SecureRandom.urlsafe_base64 な乱数を signin する度に格納するのかな。before_save で云々とありますが、どう実装するんだろ。
とりあえず試験を書く模様。以下を追加なのか。
describe "remember token" do before { @user.save } its(:remember_token) { should_not be_blank } end
いっちゃんケツに追加。以下の書き方は
its(:remember_token) { should_not be_blank }
これと同じ意味、とのこと。
it { @user.remember_token.should_not be_blank }
で、user な model は以下に、とのことです。private なメソドにするんですね。当り前っちゃ当り前なのか。
before_save 追加と
before_save { |user| user.email = email.downcase } before_save :create_remember_token
private なメソドの追加。
private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 end
これで試験は green のはず。red な確認してないけどスルー。
8.2.2 A working sign_in method
これでようやく? signin する準備ができたのでしょうか。sign_in では
- ユーザを find して
- token を書き込んで
- page 遷移
なのかな。The complete (but not-yet-working) sign_in function. として app/helpers/sessions_helper.rb が示されております。
module SessionsHelper def sign_in(user) cookies.permanent[:remember_token] = user.remember_token self.current_user = user end end
つうか self.current_user て何だ。
8.2.3 Current user
expire 云々はスルー (を
やはり setter を追加らしい。と思ったら getter も必要ですよね (当り前
module SessionsHelper def sign_in(user) cookies.permanent[:remember_token] = user.remember_token self.current_user = user end def current_user=(user) @current_user = user end def current_user @current_user # Useless! Don't use this line. end end
あ、違うや。これなら attr_accessor 使えばいいよね。以下な工夫があるようです。すばらです。
def current_user @current_user ||= User.find_by_remember_token(cookies[:remember_token]) end
8.2.4 Changing the layout links
む、例のソレを盛り込むのか。
<% if signed_in? %> # Links for signed-in users <% else %> # Links for non-signed-in-users <% end %>
格好良いねぇ。そしてこの手続きは SessionHelper に盛り込み、らしい。
module SessionsHelper def sign_in(user) cookies.permanent[:remember_token] = user.remember_token self.current_user = user end def signed_in? !current_user.nil? end
そうか、curernt_user という属性が居るので判断は簡単ですな。これを踏まえて app/views/layouts/_header.html.erb を以下に、とのこと。
<header class="navbar navbar-fixed-top navbar-inverse"> <div class="navbar-inner"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav pull-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if signed_in? %> <li><%= link_to "Users", '#' %></li> <li id="fat-menu" class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", '#' %></li> <li class="divider"></li> <li> <%= link_to "Sign out", signout_path, method: "delete" %> </li> </ul> </li> <% else %> <li><%= link_to "Sign in", signin_path %></li> <% end %> </ul> </nav> </div> </div> </header>
なんか dropdown-menu とか見えますね。あと app/assets/javascripts/application.js を云々とありますがこれは一体何でしょ。
//= require jquery //= require jquery_ujs //= require bootstrap //= require_tree .
となってますが bootstrap が追加なんですね。で、試験を動かしてみます。
$ bundle exec rspec spec/
あれ red 終了しとるな。これを、ってことなのかな。
$ rails console >> User.first.remember_token => nil >> User.all.each { |user| user.save(validate: false) }
確かに確認してみるに User.first.remember_token は nil でした。これでリトライしてみるとどうなるか、というとまだ駄目ですな。まだ何らかの盛り込みが不足してるのでしょう、ということで先に進みます。
と思ったら
試験の書き方がダウトでした。visit してないから Email って Element が無いのだなとか完全に独り言。
"with valid information" な試験は "signin" の中に無いといけないのにそうなっておりませんでした。どうもいけません。この時点で試験 green 確認済みです。
8.2.5 Signin upon signup
signout したら template missing でした。そのあたりはこれから盛り込みなのかな。とりあえず spec/requests/user_pages_spec.rb の after saving the user なソレに以下を追加とのこと。
it { should have_link('Sign out') }
signout なリンクは確かにありました。上記の通り、踏んだら動かなかったですがそれはこれから盛り込みなはず。
あと app/controllers/users_controller.rb の create を以下に、とのこと。
def create @user = User.new(params[:user]) if @user.save sign_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end
そうか。登録したらそのままログインなのか。
8.2.6 Signing out
ようやく辿り着きました。
spec/requests/authentication_pages_spec.rb な試験の修正。with valid information なナニに以下を追加。
describe "followed by signout" do before { click_link "Sign out" } it { should have_link('Sign in') } end
あとは app/controllers/sessions_controller.rb の destroy アクションを以下にするのか。
def destroy sign_out redirect_to root_url end
sign_out なソレを SessionHelper に追加する必要あり。
def sign_out self.current_user = nil cookies.delete(:remember_token) end end
これで試験は green になる、はずなのかどうか。あら、red 終了だ。remember_token が云々と出力されてます。これって attr_accessible に追加なの? って思ったのですがそうではないみたいですね。
バグ発見。出力見てみるに以下なカンジでした。
Failure/Error: before { click_button submit } NoMethodError: undefined method `remember_token' for nil:NilClass # ./app/helpers/sessions_helper.rb:4:in `sign_in' # ./app/controllers/users_controller.rb:13:in `create' # (eval):2:in `click_button' # ./spec/requests/user_pages_spec.rb:54:in `block (5 levels) in <top (required)>'
そいえば users_controller.rb に sign_in を追加したなと。見てみたら以下になってました。
def create @user = User.new(params[:user]) if @user.save sign_in @users flash[:success] = "Welcome to the Sample App!"
@users て何だよorz
無事試験は green でした。やれやれ。というかこれが単体テスツの効果です。問題解決に時間が若干かかりましたがorz
とりあえず
ここでエントリ投入します。8.3 Introduction to Cucumber (optional) は別途ってことにて。
もう一つ。bootstrap について確認必要。備忘にて。