RequestSpecでDeviseを使ったユーザーでログインさせる

今回はインテグレーションテストにはCucumberを使わずRequestSpecを使ってみよーぜーということになった。Deviseでのユーザーをログインさせる方法を考えていたのだが、普通にログイン画面を開いてメアドとパスワードを打たせてログインしてから、さてさて…とやるのは時間がかかるだろうから、いい方法を調べよう!ということになったので、メモ。

参考にしたサイトは以下の通り。

spec_helpers.rbに

RSpec.configure do |config|
  # Devise
  config.include Devise::TestHelpers, :type => :controller
  config.extend ControllerMacros, :type => :controller
end

と書けば、というのはあくまでもコントローラーのテストの場合で、RequestSpecではうまくいかなかった。:type => :requestにしてもうまくいかなかった。その後、Warden::Test::Helpersをincludeしてやりなさいという記事を見つけたので、RequestSpec側のファイルで行ったところ、うまくいった。しかし、毎回全部のRequestSpecファイルに書くのは面倒だなーと思って模索していたのだが、ようやくそれっぽい解に辿り着いた。

まず、spec/supportフォルダ以下に、request_helpers.rbを作成し、そこにRequestHelpersモジュールを定義する。

# coding: utf-8
# spec/support/request_helpers.rb
include Warden::Test::Helpers

module RequestHelpers
  def create_logged_in_user
    user = FactoryGirl.build :user, :role => FactoryGirl.create(:role) # Cancan使ってるから権限もある
    user.confirm!
    user.save!
    login(user)
    user
  end

  def login(user)
    login_as user, scope: :user, :run_callbacks => false
  end
end

その後、spec_helpers.rbに以下を定義する。

RSpec.configure do |config|
  # Devise
  config.include Devise::TestHelpers, :type => :controller
  config.extend ControllerMacros, :type => :controller
  config.include RequestHelpers, :type => :request
end

これで、適当なspec/requestsフォルダ以下のテストで、以下のようbeforeでcreate_logged_in_userメソッドを使えば、毎回ログイン処理をおこなってくれる。

# coding: utf-8
require 'spec_helper'
describe "Hoges" do

  before do
    create_logged_in_user
  end

  describe "GET /hoges" do
    it "works! (now write some real specs)" do
      get hoges_path
      response.status.should be(200)
    end
  end
end

これでようやくテストをする下地ができたかなぁ〜という感じ。


Deviseの画面以外で認証をかける方法

逆の言い方をするとDeviseの画面のみ認証を行わない方法です。
認証チェックを全部のページにやりたいけれど1つ1つのコントローラーに設定していくのは面倒すぎるので一発で全部やっちゃおーぜということです。
でもDeviseのログイン画面とか諸々まで影響するのでそれは避けたい。その方法がこちら。
ApplicationControllerに設定します。ただし、DeviseController系のコントローラーの場合は除外、というナイスな方法です。

class ApplicationController < ActionController::Base
  check_authorization :unless => :devise_controller?
  ...(略)
end

Devise以外のコントローラーで認証を行いたくない場合は、skip_authorization_checkを設定します。トップ画面は認証いらないから除外したいという場合は以下のようにします。

# coding: utf-8
class TopController < ApplicationController
  skip_authorization_check
  def index
  end
end

CanCanのload_and_authorize_resourceについて

CanCanで、そのリソースへのアクセス権限チェックを行う関数として、load_and_authorize_resourceがあります。
これを使うと、アクセス権限をチェックした上で、違反した場合は例外送りにしてくれます。

class HogeController < ApplicationController
  load_and_authorize_resource

  def index
    ...(略)
  end

  def show
    ...(略)
  end

  def new
    ...(略)
  end

  def edit
    ...(略)
  end

  ...(略)
end

しかも、このload_and_authorize_resourceはすごい機能があって、文字通りなんですが、リソースをロードしてくれます。つまり、scaffoldで定義していたような処理を勝手にやってくれます。index, show, new, editみたいなメソッドはcancanに任せて、削除できます。

class HogeController < ApplicationController
  load_and_authorize_resource
  ...(略) # こんなに短くなる!
end

しかし、kaminariを使っている場合など、イレギュラーなものには対応してないっぽくて、indexメソッドを定義する必要があります。

class HogeController < ApplicationController
  load_and_authorize_resource

  def index
    params[:page] ||= 1
    @hoge = Hoge.page(params[:page])

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @hoge }
    end
  end

  ...(略) # まぁこんなところです。
end

また、一部のメソッドでは認証したくないという場合、skip_load_and_authorize_resourceメソッドが使えます。これだけ定義すると、全部スキップしようとするので、:onlyでメソッドを指定します。

class HogeController < ApplicationController
  load_and_authorize_resource
  skip_load_and_authorize_resource :only => :search_hoge
  ...(略)
end

ハマりがちなんですが、CanCanの権限チェックだけに注視していると、Deviseの認証チェックにひっかかってたりする場合があるので、skipなんちゃらをするときは、Deviseのほうもskipする必要があるでしょう。

class HogeController < ApplicationController
  skip_authorization_check :only => :search_hoge
  load_and_authorize_resource
  skip_load_and_authorize_resource :only => :search_hoge
  ...(略)
end

こんな感じですかねぇ。


Deviseでユーザー確認をスキップする方法

Deviseでユーザー登録をさせたい場合に、ユーザー登録メールを送信してごにょごにょみたいなことをしたいのだけれど、まだそこは置いといて、とりあえずシードでユーザーをDBに投入したところ、ログイン画面でログインしようとしたところ、「本登録してください」と怒られた。

本登録処理をするにはメールを受けないと無理じゃないか?
どうすればいいんだろうか?と思ったらちゃんとskip_confirmation!というメソッドがあった。
それを使えばよい。

user = User.create!(
  :email => "hoge@email.com",
  :password => "hogehoge"
)
user.skip_confirmation!

これでいけるんだろうと思っていたら、ダメだった。
なんとuser.saveしないといけないと。skip_confirmation!っていう破壊的な感じがするから保存もしてくれるんだと思ったらしてくれないなんてびっくりだ!

user = User.create!(
  :email => "hoge@email.com",
  :password => "hogehoge"
)
user.skip_confirmation!
user.save!

これでよい。


Devise使ってログイン前とログイン後のレイアウトを切り替える

Deviseを使っていて、ログイン前はサイドバーなしでログイン後はサイドバー見せるようにしたいなー、どうしたらいいのかなーと思って、適当にやってみたらダメだった。

# 適当にやったコード
class ApplicationController < ActionController::Base
  protect_from_forgery
  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_url, :alert => exception.message
  end

  layout ((current_user.nil?) ? "single" : "application")
end

current_userというメソッドはないと言われて撃沈。
ここでは使えないようである。ぐぬぬ。

ということでググってみたらいい方法があった。
stackoverflow: How to render login page without a layout?

class ApplicationController < ActionController::Base
  protect_from_forgery
  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_url, :alert => exception.message
  end

  layout :layout

  private

  def layout
    is_a?(Devise::SessionsController) ? "single" : "application"
  end
end

Devise::SessionControllerならばsingleをレイアウトに指定する。それ以外はapplication(デフォルト)を指定するという方法である。とてもクール!