Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
25
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

【OmniAuth + Devise】Twitterのログイン認証をテストする方法【Rspec】

はじめに

仕事でAuth認証のspecを書く場面があったので、いろいろハマったことも含め簡潔に書いていきたいと思います。

【実行環境】
・ Rails 5.1.4
・ RSpec 3.7
・ Ruby 2.4.3

ざっくり実装の手順

  1. Rspecのセットアップ
  2. モックの作成
  3. Controllerの認証テスト

【前提】omniauthのルート設定

複数モデルで実装する方も多いと思うので、
Omniauthの認証処理はAuthenticationsControllerでやるとします。

config/routes.rb
  get "auth/:provider" => "authentications#new", as: :new_authentication
  get "auth/:provider/callback" => "authentications#create", as: :create_authentication

参考: https://github.com/plataformatec/devise/wiki/OmniAuth-with-multiple-models

テストするもの

・Authが正常に渡ってきたときの処理(createアクション)
・Authが渡って来なかったときのエラーハンドリング(createアクション)
・Auth認証に失敗した時の処理(failureアクション)

app/controllers/authentications_controller.rb
class AuthenticationsController < ApplicationController 
  # get "auth/:provider" 
  # def new  ← omniauthにより、newアクションは自動生成される
  # end

  #  get "auth/:provider/callback"
  def create
    raise "request.env[omniauth.auth]がありません" if auth_params.nil?
    @social_profile = SocialProfile.new.set_value(auth_params)
    @user = User.new(emial: @social_profile.email)
    if @user.save
      @social_profile.save!
      redirect_to edit_uer_registratiion_url
    else
      redirect_to new_user_registration_path
    end
  end

  # callbackに失敗したときに呼ばれるアクション
  def failure
    redirect_to user_setting_social_profiles_path if current_user
  end

  private
    # ユーザー情報の入った
    def auth_params
      request.env["omniauth.auth"]
    end
end

肝となるのはやはりrequest.env["omniauth.auth"]です。
今回テストすべきなのはauth/:provider/callbackを叩いた時に正常にユーザーデータが渡ってくるかなので、ここでいう、createアクションのspecを主に書いていこうと思います。

Rspecのセットアップ

まずはOmniAuthをテストモードに変えておきましょう。
また、deviseのヘルパーを使う場合は、それぞれ設定しておきます。

spec/rails_helper.rb
# OmniAuthをテストモードに変更
OmniAuth.config.test_mode = true

RSpec.configure do |config|
  # FactoryBotの記述省略
  config.include FactoryBot::Syntax::Methods
  # deviseで使うヘルパー
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include Devise::Test::IntegrationHelpers, type: :request
end

テストで用意するデータ

ユーザーデータのハッシュが入ってるのが、request.env["omniauth.auth"]なので、
そのユーザーデータを擬似的に作ってテストしてあげればOK。

本来request.env["omniauth.auth"]にどんなハッシュが渡ってくるかは以下を参考に↓
https://github.com/arunagw/omniauth-twitter#authentication-hash

そのために、認証用のモックを作っていきます。

モックの作成

request.env["omniauth.auth"]に入れるモックを作成しましょう。

どこでも使うのでspec/rails_helper.rbに記述してもいいですが、結構長いので今回はspec/support/omniauth_mocks.rbに書いて最後にincludeするようにします。

spec/support/omniauth_mocks.rb
module OmniauthMocks
  def twitter_mock
    OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new({
      "provider" => "twitter",
      "uid" => "123456",
      "info" => {
        "name" => "Mock User",
        "image" => "http://mock_image_url.com",
        "location" => "",
        "email" => "mock@example.com",
        "urls" => {
          "Twitter" => "https://twitter.com/MockUser1234",
          "Website" => ""
        }
      },
      "credentials" => {
        "token" => "mock_credentails_token",
        "secret" => "mock_credentails_secret"
      },
      "extra" => {
        "raw_info" => {
          "name" => "Mock User",
          "id" => "123456",
          "followers_count" => 0,
          "friends_count" => 0,
          "statuses_count" => 0
        }
      }
    })
  end
end
spec/rails_helper.rb
RSpec.configure do |config|
  config.include OmniauthMocks
end

コントローラで認証処理をテストする

コールバックが呼ばれたときに、以下の内容をテストします。
・ authが渡ってこなければエラーを起こす
・ authが渡ってこれば各モデルが作成されること
・ authにemailが含まれてなければ再度認証画面にリダイレクトさせること

app/controllers/authentications_controller.rb
class AuthenticationsController < ApplicationController 
  #  get "auth/:provider/callback"
  def create
    raise "request.env[omniauth.auth]がありません" if auth_params.nil?
    @social_profile = SocialProfile.new.set_value(auth_params)
    @user = User.new(emial: @social_profile.email)
    if @user.save
      @social_profile.save!
      redirect_to edit_uer_registratiion_url
    else
      redirect_to new_user_registration_path
  end

  private
    # ユーザー情報の入った
    def auth_params
      request.env["omniauth.auth"]
    end
  end

Rspec↓

spec/controllers/authentications_controller_spec.rb
require 'rails_helper'

RSpec.describe AuthenticationsController, type: :controller do
  before { request.env["omniauth.auth"] = twitter_mock }
  subject { get :create, params: { provider: "twitter" } }

  it "oauthが渡ってこない場合エラーになる" do
    request.env["omniauth.auth"] = nil
    expect { subject }.to raise_error("request.env[omniauth.auth]がありません")
  end

  context "emailがある場合" do
    it "新規作成" do
      expect { subject }.to change { User.count }.from(0).to(1)
      expect { subject }.to change { SocialProfile.count }.from(0).to(1)
      expect(subject).to redirect_to edit_user_registration_url
    end
  end

  context "emailがない場合" do
    it "登録画面へリダイレクト" do
      request.env["omniauth.auth"]["info"]["email"] = nil
      expect(subject).to redirect_to new_user_registration_path
    end
  end
end

①request.env["omniauth.auth"]に先ほど作ったユーザーデータの入ったハッシュ型のモック(twitter_mockd)が入る。

②一つ目のexample → request.env["omniauth.auth"] = nilとすることで、authが渡って来なかったと判断し、エラーを吐く。

③2つ目のexample → auth_paramsにモックが入ってるので、欲しいデータがしっかり入っていれば正常にsave(作成)される。

④3つ目のexample → request.env["omniauth.auth"]["info"]["email"] = nilで、モックのeamilを空にする。この状態でリクエストを送ればモデルは作成されずリダイレクトされる.

認証失敗時のテスト

callbackが失敗した時、どこにリダイレクトすればいいのかはconfig/initializers/omniauth.rbで設定できます。

config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, Settings.oauth.twitter.consumer_key, Settings.oauth.twitter.consumer_secret   
  on_failure do |env|
    AuthenticationsController.action(:failure).call(env) ##ここで設定
  end 
end

今回はAuthenticationsControllerfailureアクションが呼ばれるように設定したので、AuthenticationsControllerでテストしてみることにします。

app/controllers/authentications_controller.rb

class AuthenticationsController < ApplicationController
  # ...省略

  def failure
    redirect_to user_setting_social_profiles_path if current_user
  end
end

じゃあコールバックを失敗させるにはどうすればいいかというと、失敗用のモックをまた別に作ってあげる必要があります。

spec/support/omniauth_mocks.rb
module OmniauthMocks
  # ...省略

  def twitter_invalid_mock
    OmniAuth.config.mock_auth[:twitter] = :invalid_credentails
  end
end

このモックを使ってコールバックを叩けばfailureにリダイレクトさせることができます。
参考: https://github.com/omniauth/omniauth/wiki/Integration-Testing#mocking-failure

コールバック失敗時のテストコードを書いてみる

早速 failureアクションのテストがしたいから、

get :failure, params: { provider: "twitter" }

みたいな感じに書いてみましたが、「そんなルートねぇよ!」と怒られました。。

なぜなら、failureは、コールバックに失敗した時にしか呼ばれないからです。

僕はこれにハマってしまい、なぜルートがないのに呼ばれるのだろう?と思っていたのですが、どうやらomniauthの方でcallbackに失敗したら自動的で呼ばれるようになっているらしい。

コードの中身: https://github.com/omniauth/omniauth/blob/c2380ae848ce4e0e39b4bb94c5b8e3fd0a544825/lib/omniauth/builder.rb#L22
Github : https://github.com/omniauth/omniauth/wiki/Integration-Testing

Github(翻訳): https://techracho.bpsinc.jp/hachi8833/2017_05_22/40297

というわけで、コールバックのURLを叩きにいって、失敗させて上げる必要があります。
今回コールバックのURLは

config/routes.rb
get "auth/:provider/callback" => "authentications#create", as: :create_authentication

となっているため、Authentications#createにGETリクエストを投げてテストしてみました。

spec/controllers/authentications_controller_spec.rb
require "rails_helper"

RSpec.describe "omniauth", type: :request do
  # ...省略

  describe "GET #failure" do
    let(:user) { create(:user) }
    before { request.env["omniauth.auth"] = twitter_invalid_mock }
    subject { get :create, params: { provider: "twitter" }

    it "current_userが存在する" do
      sigin_in user
      expect(subject).to redirect_to user_setting_social_profiles_path
    end
    it "current_userが存在しない" do
      expect(subject).to render_template :failure
    end
  end
end

と、書きたいところですが、これではfailureアクションにはリダイレクトしてくれませんでした。
また、テスト用のparams(:invalid_credentails)をアクション内で整形してしまいエラーを吐いてしまいます。

これでは明示的にfailureアクションに飛ばすしか方法がありません。

アクションを介さずに直接リダイレクトするには、request specに書きましょう。

spec/requests/authentications_spec.rb
require "rails_helper"

RSpec.describe "omniauth", type: :request do
  describe "GET #failure" do
    before do
      Rails.application.env_config["omniauth.auth"] = twitter_invalid_mock
    end
    subject { get "/auth/twitter/callback" }

    it "current_userが存在する" do
      sigin_in user
      expect(subject).to redirect_to user_setting_social_profiles_path
    end
    it "current_userが存在しない" do
      expect(subject).to render_template :failure
    end
  end
end

これで無事通すことができました。
参考: https://qiita.com/Apuruny/items/38dc94628f7b52bd172e

まとめ

Twitter認証の仕組みなんてそもそもわからないし、ましてやテストコードをかけとは何事だと思ってましたが、いろんな記事に恵まれて無事実装することができました。

全て動作確認をしてるわけではないので、コピペする際はご注意ください(m_ _m)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
25
Help us understand the problem. What are the problem?