【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
```


今回は`AuthenticationsController`の`failure`アクションが呼ばれるように設定したので、`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)