ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

Ruby on Railsの認証 - Devise ユーザー名で認証 + 簡単なRSpec

Railsの認証を行うためのgemに、Deviseというものがあります。
デフォルトではメールアドレスとパスワードで認証を行うため、ユーザー名でログインするサンプルが
なかなか見つからなかったのですが、RailsCastsで見つけたので、実際にやってみました。

以下、今回参考にしたRailsCastsの記事です。
http://railscasts.com/episodes/209-devise-revised?view=asciicast

(日本語訳は以下の2記事が該当しますが、多少古い情報かもしれません。)
http://ja.asciicasts.com/episodes/209-introducing-devise
http://ja.asciicasts.com/episodes/210-customizing-devise

上記の記事を参考にし、以下の点に手を加えました。
・既存のRailsアプリではなく、新規のRailsアプリを作り、Deviseを適用した。
・認証済みユーザーに公開するページは、home/edit画面とした。
・外見はTwitter Bootstrap を使用した。
RSpecによるテストを追加した。またテストし易いように一部のソースを変えた。

今回作成したアプリは、GitHub上に載せておきました。参考にして下さい。
https://github.com/UnderSourceCode/devise_name_auth_sample

では、手を加えた箇所を中心に、手順を書いていきたいと思います。

Railsアプリの作成〜Twitter Bootstrapの適用
1.アプリの新規作成
以下のコマンドで、アプリを作成します。

  • T オプションで、デフォルトのテストを作成しないようにしています。

$ rails new devise_name_auth_sample -T

2.「Could not find a JavaScript runtime.」というエラー対策
アプリを起動すると、エラーが発生します。JavaScriptのRuntimeが必要なので
以下の2行をGemfileに追加して、bundle install します。

gem 'therubyracer'
gem 'execjs'

3.rspec、spork、capybara等の準備
RSpecの実行に必要なものをインストールするため、以下をGemfileに追記して
bundle install します。

group :development, :test do
gem 'rspec-rails'
gem 'spork'
gem 'factory_girl_rails'
gem 'capybara'
end
これ以降の手順については、RSpecの自分なりの使用方法を参考にしてください。

4.twitter bootstrapの準備
以下をGemfileに追記して、bundle install します。

gem "twitter-bootstrap-rails"
gem "less-rails"
これ以降の手順については、twitter-bootstrap-railsの導入を参考にしてください。

■homeコントローラの作成
新規アプリなので、認証済みのユーザーのみに公開するページ、全てのユーザーに公開するページ
を作る必要があります。
今回はhomeコントローラに、index、editという2つのViewをつくり、indexは全てのユーザーに公開
editは認証済みのユーザーにのみ公開することにしました。

1.コントローラの作成
以下のコマンドで、コントローラとViewを作成します。

$ rails g controller home index edit

2.URL Rootの設定
routes.rbを変更し、indexをルート画面とします。

root to: 'home#index' #追加
#"home/index" ###コメントアウト or 消去

3.ルート画面の確認
/public/index.htmlを削除し、rails s でアプリを起動します。
home#index画面が表示されることを確認します。

■deviseの準備
1.インストール
以下をGemfileに追記して、bundle install します。

gem 'devise'

2.deviseのインストーラを起動
以下のコマンドを実行します。

$ rails g devise:install

3.deviseのインストーラのメッセージを読み、必要と思われるものを行う
deviseのインストーラを起動すると、メッセージが表示されます。
その中から、必要と思われる手順を実行します。
今回は、config/environments/development.rbに以下を追加だけ行いました。

onfig.action_mailer.default_url_options = { :host => 'localhost:3000' }

※メッセージを表示する箇所をapplication.html.erbに追加する手順は行いませんでした。
理由は、Twitter Bootstrapでメッセージ表示領域が既に追加されているからです。
application.html.erbの以下の行が、メッセージ表示領域となります。

<%= bootstrap_flash %>

4.userモデルを作成
以下のコマンドを実行します。

$ rails g devise user

5.データベースにテーブルを作成
以下のコマンドを実行します。

$ rake db:migrate

■Viewの作成
1.Login, Sign up, Logoutリンクの作成
ユーザーがログイン等の操作を行うためのリンクを作成します。
参考にしたRailsCastsの記事では、/app/views/layouts/application.html.erbにリンクを作成していますが
今回はビューヘルパーに作成しました。

理由は、ビューヘルパーに外出しすることでテストをし易くするためです。
以下が、ビューヘルパーのソースです。/view/sharedフォルダを作成し、その中に作成します。

_user_nav.html.erb

          <div id="user_nav">
<% if user_signed_in? %>
Logged in as <strong><%= current_user.email %></strong>.
<%= link_to 'Edit profile', edit_user_registration_path %> |
<%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% else %>
<%= link_to "Sign up", new_user_registration_path %> |
<%= link_to "Login", new_user_session_path %>
<% end %>
</div>

<% flash.each do |name, msg| %>
<%= content_tag :div, msg, id: "flash_#{name}" %>
<% end %>

見ての通り、内容はRailsCastsと同じです。

以下が、application.html.erbの一部抜粋です。
<%= render 'shared/user_nav' %>でビューヘルパーを参照しています。

application.html.erb

    <div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-target=".nav-collapse" data-toggle="collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="#">DeviseNameAuthSample</a>

<%= render 'shared/user_nav' %>

</div>
</div>
</div>

2.認証のためのビューを作成
以下のコマンドで、認証に必要なdeviseのビューを作成します。

$ rails g devise:views

3./app/views/devise/sessions/new.html.erbを変更
ログイン画面を変更します。ソースについては、RailsCastsの「/app/views/devise/sessions/new.html.erb」
を参考にしてください。

4.login, logoutのパスを/config/routes.rbに定義
以下のdevise_for〜で始まる定義を、routes.rbに追加します。

routes.rb

DeviseNameAuthSample::Application.routes.draw do
devise_for :users, path_names: {sign_in: "login", sign_out: "logout"}

root to: 'home#index'

get "home/edit"
end

■ユーザー名でログインできるようにする
上記の手順では、メールアドレスでログインするようになっているので
ユーザー名でログインできるように変更します。

この手順についても、RailsCastsと同じなので、RailsCastsの「Changing Authentication Fields」を
参考にしてください。

■テストの作成
最後になりましたが、RSpecによるテストの作成です。実は、ここが一番作りたかった箇所でもあります。

1.spec/factories.rbの作成
spec/factories.rbファイルを作成し、FactoryGirlsに読み込ませるため
ダミーのユーザー情報を定義します。

spec/factories.rb

require 'factory_girl'

FactoryGirl.define do
factory :user do
username 'UnderSourceCode'
email 'UnderSourceCode@ggg.com'
password 'password'
password_confirmation "password"
end
end

2.spec/support/controller_macros.rbの作成
spec/support/controller_macros.rbファイルを作成し、RSpecでログイン状態を再現するための処理を
記述します。

spec/support/controller_macros.rb

module ControllerMacros
def login_user
user = FactoryGirl.create(:user)
sign_in user
return user
end
end

3.spec/spec_helper.rbに以下を追加
RSpecでdeviseのヘルパーと、上記のControllerMacrosを使用するための定義を追加します。

spec/spec_helper.rb

  config.include Devise::TestHelpers, :type => :controller
config.include Devise::TestHelpers, :type => :view
config.include ControllerMacros, :type => :controller
config.include ControllerMacros, :type => :view

4.テスト用データベースの作成
以下のコマンドで、テストで使うデータベースをコピーして作成します。

$ rake db:test:clone

5.Userモデルのテスト
Userモデルのテストです。今回はusernameが入力必須であることをテストしました。
Userモデルについては、RailsCastsの記事を参考にしてください。

spec/models/user_spec.rb

require 'spec_helper'

describe User do
it '全てを入力した場合は正常に保存されること' do
@user = User.new(:email => 'test@test.com',
:password => 'password',
:password_confirmation => 'password',
:remember_me => true,
:username => 'test')
result = @user.save
result.should equal(true)
@user.should be_valid
end

it '名前を入力しない場合は保存されないこと' do
@user = User.new(:email => 'error@test.com',
:password => 'errorpass',
:password_confirmation => 'errorpass',
:remember_me => true,
:username => '')
result = @user.save
result.should equal(false)
@user.should_not be_valid
end
end

6.Login, Sign up, Logoutリンクのテスト
ビューヘルパーに外出ししたリンクのテストです。
ログイン済みかどうかで表示するリンクが異なるので、その辺りをテストします。

spec/vies/shared/_user_nav.html.erb_spec.rb

require 'spec_helper'

describe "shared/_user_nav.html.erb" do
describe "ログインしている場合" do
before(:each) do
user = login_user
view.stub(:current_user).and_return(user)
render ("shared/user_nav")
end

it "ログアウトのリンクが表示されていること" do
rendered.should have_selector('a', 'Logout')
end
end

describe "ログインしていない場合" do
before(:each) do
render ("shared/user_nav")
end

it "サインアップのリンクが表示されていること" do
rendered.should have_selector('a', 'Sign up')
end

it "ログインのリンクが表示されていること" do
rendered.should have_selector('a', 'Login')
end
end
end

以上です。

■追記
別のプロジェクトにこの手順でDeviseを導入したところ
以下のエラーは発生しました。

Factory already registered: user

対応策とては、自動で作成される/spec/factories/users.rbを
削除することでした。