##はじめに
仕事でAuth認証のspecを書く場面があったので、いろいろハマったことも含め簡潔に書いていきたいと思います。
【実行環境】
・ Rails 5.1.4
・ RSpec 3.7
・ Ruby 2.4.3
##ざっくり実装の手順
- Rspecのセットアップ
- モックの作成
- Controllerの認証テスト
##【前提】omniauthのルート設定
複数モデルで実装する方も多いと思うので、
Omniauthの認証処理はAuthenticationsController
でやるとします。
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
アクション)
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
のヘルパーを使う場合は、それぞれ設定しておきます。
# 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するようにします。
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
RSpec.configure do |config|
config.include OmniauthMocks
end
コントローラで認証処理をテストする
コールバックが呼ばれたときに、以下の内容をテストします。
・ authが渡ってこなければエラーを起こす
・ authが渡ってこれば各モデルが作成されること
・ authにemailが含まれてなければ再度認証画面にリダイレクトさせること
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↓
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
で設定できます。
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
でテストしてみることにします。
class AuthenticationsController < ApplicationController
# ...省略
def failure
redirect_to user_setting_social_profiles_path if current_user
end
end
じゃあコールバックを失敗させるにはどうすればいいかというと、失敗用のモックをまた別に作ってあげる必要があります。
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は
get "auth/:provider/callback" => "authentications#create", as: :create_authentication
となっているため、Authentications#create
にGETリクエストを投げてテストしてみました。
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
に書きましょう。
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)