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 について確認必要。備忘にて。