【Rspec】devise + slack-omniauthのテスト方法
はじめに
こんにちは!大ちゃんの駆け出し技術ブログです。
今回はPFにあたり詰まった「devise + slack-omniauthのテスト方法」を紹介します。
slackログインの正式な方法についてはQiitaにてあげる予定ですが、記憶が新しいうちに紹介しておきたく、、、!
では早速解説していきます。
テスト内容
テスト内容について触れておきます。
slackログインの認証方法としてdeviseを使用しています。なので認証時にはコールバックコントローラに返すように指定します。
# app/controllers/users/omniauth_callbacks_controller.rb module Users class OmniauthCallbacksController < Devise::OmniauthCallbacksController skip_before_action :authenticate_user! def slack user_info = get_user_info(request.env['omniauth.strategy']) @user = User.from_omniauth(request.env['omniauth.auth'], user_info) if @user.persisted? sign_in_and_redirect @user, event: :authentication else redirect_to root_path end end def failure flash[:alert] = 'Slack認証に失敗しました。' redirect_to root_path end end end
deviseのSNS認証はメジャーかと思いますのでコードの見た目はほぼ同じかと思います。認証成功時にはslack
メソッドがリダイレクトされ、逆に認証ができなかった場合failure
メソッドにリダイレクトします。
テストする内容としては下記2点のみです。
- [ ] 認証成功時に
slack
メソッドにリダイレクトし、意図したページにアクセスすること - [ ] 認証失敗時に
failure
メソッドに失敗し、トップページにアクセスすること
長文になりますが補足で説明します。Rspecとはあまり関係がありませんが、Rspecのテストの方法に影響があるので念のため説明させてください。
上述したように、コールバックコントローラの見た目はほぼ同じですが、1点違う箇所があります。
user_info = get_user_info(request.env['omniauth.strategy']) @user = User.from_omniauth(request.env['omniauth.auth'], user_info)
get_user_info
でログインしたユーザー情報にアクセスし、JSON形式で返却するようにしています。
def get_user_info(request) request.access_token.user_token.get('/api/users.identity').parsed end
基本的にdeviseでのSNS認証時にrequest.env['omniauth.auth']
という値は使う必要がありません。
例えば、QiitaでLGMTが多い下記記事のcallbackメソッドの例は以下のようになります。
https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
# common callback method def callback_for(provider) @user = User.from_omniauth(request.env["omniauth.auth"]) if @user.persisted? sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format? else session["devise.#{provider}_data"] = request.env["omniauth.auth"].except("extra") redirect_to new_user_registration_url end end
@user = User.from_omniauth(request.env["omniauth.auth"])
とあるように、request.env["omniauth.auth"]
をのみを使っています。そしてrequest.env["omniauth.auth"]
の値を使って、名前だったりユーザーの画像だったりを格納します。上記の記事ではemailのみしか取得していなかったのですが、他の記事を当たると多くの情報を格納するようです。
https://qiita.com/sakakinn/items/321e6b49c92b9f02f83f
def self.from_omniauth(auth) where(provider: auth.provider, uid: auth.uid).first_or_create do |user| user.provider = auth.provider user.uid = auth.uid user.name = auth.name user.email = auth.info.email user.password = Devise.friendly_token[0, 20] # ランダムなパスワードを作成 user.image = auth.info.image.gsub("_normal","") if user.provider == "twitter" user.image = auth.info.image.gsub("picture","picture?type=large") if user.provider == "facebook" end end
では、なぜ自分のコードではAPIを取得して追加情報の取得が必要なのかというと、slackより返却されるrequest.env["omniauth.auth"]
の値が他のSNS認証時に返却されるものとは異なるからです。というよりは返却される情報が少ないと行った方が正しいです。
実はこのことに関する記事はありません。slack-omniauth
のgemのGithub上でissueでちょこっと触れられていました。
https://github.com/ginjo/omniauth-slack/issues/10#issuecomment-621560008
この記事で述べている箇所はrequest.env["omniauth.auth"]
の値の例です。
{ "enterprise" : null, "ok" : true, "team" : { "id" : "T0BXXXXXX" }, "authed_user" : { "scope" : "identity.basic,identity.email,identity.avatar,identity.team", "id" : "U0BXXXXXX", "token_type" : "user", "access_token" : "xoxp-111111111111-22222222222-3333333333333-fa39d45841fa1daab3a98f945a133d02" }, "app_id" : "A0XXXXXXXXX" }
他のrequest.env["omniauth.auth"]
の値と比べて圧倒的に少ないです。他のrequest.env["omniauth.auth"]
の値にはユーザー情報として、ユーザーのアイコン、名前、メールアドレスが含まれるはずなのですが、slackの場合はそれら個人の情報が一切ないです。そのため、追加でユーザー情報を取得しないと、十分なユーザー情報を格納できないのです。
そしてこのissueの中でユーザーへのアクセスはユーザーのアクセストークンを使用して直接取得するように記載されています。
@bot_token = env['omniauth.strategy'].access_token @user_token = @bot_token.user_token @user_token.get("/api/users.identity", ...).parsed # --> response data hash from Slack
これを見て自分もユーザー情報を追加で取得してアイコンなどの追加情報を取得しました。
テストの方法
先にテストコードを紹介してする前に、必要な設定を紹介します。
実はOmniauthのテストにドキュメントがあります。
https://github.com/omniauth/omniauth/wiki/Integration-Testing
必要な設定として、spec/rails_helper.rb
に下記を記載して下さい。
# spec/rails_helper.rb # Turn on "test mode" for OmniAuth OmniAuth.config.test_mode = true
これによりどうなるのかというと、SNS認証を行う際、一度アプリケーションのブラウザからSNSのサイトの認証画面に移行する必要があります。テストでも同じように認証画面にアクセスすることはあまり好ましくないと思います。毎回テストするたびに認証画面に移行してもし万が一のことがあったら危険ですし、なにより通信超過のためにテストに時間を要します。それを防ぐために上記の設定を行うことで、認証画面への移行をスキップし、そのままコールバックコントローラにリダイレクトしてくれます。
加えて、新規ファイルでconfig/initializers/omniauth.rb
を作成します。
# config/initializers/omniauth.rb Rails.application.config.middleware.use OmniAuth::Builder do on_failure { |env| Users::OmniauthCallbacksController.action(:failure).call(env) } end
この設定が必要な理由は、デフォルトの仕様ですと、認証失敗時の挙動は例外の発生となります。しかし、devise認証ではホーム画面にリダイレクトするように設定していますので例外は発生しません。それを防ぐために、上記の設定が必要となります。
※ 公式では以下のように設定していますが、自分の場合こちらの設定では反映されませんでした。のちほど調査します。
OmniAuth.config.on_failure = Proc.new { |env| OmniAuth::FailureEndpoint.new(env).redirect_to_failure }
設定が終わったところでテストコードを紹介します。最初の検証が失敗時のテストで、次にテストしているのが成功時の検証です。
# spec/system/slack_login_spec.rb require 'rails_helper' RSpec.describe "SlackLogin", type: :system do before do Rails.application.env_config["devise.mapping"] = Devise.mappings[:user] # Devise使用時に必要な記載 Rails.application.env_config["omniauth.auth"] = set_slack_omniauth # omniauth.authの値を代入 allow_any_instance_of(ApplicationController).to receive(:get_user_info).and_return(set_user_info) # strategy.authの値を代入 end context 'oauthがinvali_omniauthの場合' do before do Rails.application.env_config["omniauth.auth"] = set_invalid_omniauth visit root_path click_on 'Slackログイン' end it "トップページにリダイレクトされる" do expect(current_path).to eq(root_path), 'ルートパスにリダイレクトされていません' expect(page).to have_content('Slack認証に失敗しました。'), 'フラッシュメッセージが表示されていません' end end context '初めてアプリにログインする時' do before do visit root_path click_on 'Slackログイン' end it 'プロフィール新規作成画面にアクセスする' do expect(page).to have_content('新規登録完了しました。次にプロフィールを作成してください。'), '意図したフラッシュメッセージが表示されていません' expect(current_path).to eq(new_profile_path), 'プロフィール新規作成画面にアクセスしていません' end end end
最初に下記の記述です。
Rails.application.env_config["devise.mapping"] = Devise.mappings[:user] # Devise使用時に必要な記載
これは公式の説明にも書いてあります。deviseを使用する場合は必須のようですね。
https://github.com/omniauth/omniauth/wiki/Integration-Testing
公式の記述
before do Rails.application.env_config["devise.mapping"] = Devise.mappings[:user] # If using Devise Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter] end
次にモックを導入してテストをしています。
Rails.application.env_config["omniauth.auth"] = set_slack_omniauth # omniauth.authの値を代入 allow_any_instance_of(ApplicationController).to receive(:get_user_info).and_return(set_user_info) # get_user_infoの値を代入
これによりrequest.env["omniauth.auth"]
の値とget_user_info
メソッドの返り値をモックかしています。モックファイルは下記に記載します。
# spec/support/omniauth_helper.rb # TODO: API認証時のテストを追加(未実装) module OmniauthHelpers def set_slack_omniauth OmniAuth.config.mock_auth[:slack] = OmniAuth::AuthHash.new( { 'provider' => 'slack', 'uid' => 'mock_uid_1234', 'credentials' => { 'token' => 'mock_credentails_token_1234', }, } ) end def set_user_info user_info = { "user"=> {"name"=>Faker::Name.name, "email"=>Faker::Internet.email, "image_192"=> "https//image-XXXXXXXXXX.png",}, "team"=> {"id"=>rand(10 ** 19).to_s, "name"=>"XXXXXXXXXX", "image_34"=>"https//image-XXXXXXXXXX.jpg", } } user_info end def set_invalid_omniauth OmniAuth.config.mock_auth[:slack] = :invalid_credentials end end
ここが他の記事とは異なる点です!
他の記事では追加でユーザー情報を取得することがないために、set_slack_omniauth
(つまりrequest.env["omniauth.auth"]
の値)の箇所だけしか書いておりません。今回の自分の実装の場合、追加情報でAPIにアクセスしないといけないため、その返却値のJSONをモック化しなければいけなかったのです。モックをよく知らなかった自分にとってはかなり難しかったです、、。
allow_any_instance_of(ApplicationController).to receive(:get_user_info).and_return(set_user_info) # get_user_infoの値を代入
テスト箇所ですが、認証成功時の箇所は極めてシンプルです。
context '初めてアプリにログインする時' do before do visit root_path click_on 'Slackログイン' end it 'プロフィール新規作成画面にアクセスする' do expect(page).to have_content('新規登録完了しました。次にプロフィールを作成してください。'), '意図したフラッシュメッセージが表示されていません' expect(current_path).to eq(new_profile_path), 'プロフィール新規作成画面にアクセスしていません' end end
当然アプリによってリダイレクト先は異なりますが、それに合わせてテストを設計するだけです。
失敗時のテストですが、これについては説明させてください。
context 'oauthがinvali_omniauthの場合' do before do Rails.application.env_config["omniauth.auth"] = set_invalid_omniauth visit root_path click_on 'Slackログイン' end it "トップページにリダイレクトされる" do expect(current_path).to eq(root_path), 'ルートパスにリダイレクトされていません' expect(page).to have_content('Slack認証に失敗しました。'), 'フラッシュメッセージが表示されていません' end end
といっても注目箇所はこちらの部分のみです。
Rails.application.env_config["omniauth.auth"] = set_invalid_omniauth
これはモックファイルで定義していた値を格納します。
def set_invalid_omniauth OmniAuth.config.mock_auth[:slack] = :invalid_credentials end
これを使うことで認証失敗時の挙動をテストしてくれます。公式のomniauthのテスト方法のドキュメントでも同じように紹介しています。
OmniAuth.config.mock_auth[:twitter] = :invalid_credentials
重要なのは認証失敗時のテストでは必ずset_invalid_omniauth
を使わないといけないということです。例えば、他の記事では代わりにnilを代入していましたが、それでは意図したものとは異なる挙動を見せます。
実際にnilを代入して試してみました。
Rails.application.env_config["omniauth.auth"] = nil
するとなんとfailure
メソッドにリダイレクトされず、成功時と同じようにプロフィール作成画面にリダイレクトしました。これはどういうことかというと、先に代入されているset_slack_omniauth
の値が既に格納されており、Rails.application.env_config["omniauth.auth"]
にnil
は反映されないためです。
では、set_slack_omniauth
の箇所をコメントアウトしてみると失敗するのではと思い、コメントアウトしてnilの代入箇所のみテストしてみました。するとまた別の挙動を見せました。
failure
メソッドにはリダイレクトされずslack
メソッドにリダイレクトされましたが、request.env['omniauth.auth']
の値に全く設定していない値が格納されていたのです。
> request.env['omniauth.auth'] => {"provider"=>"default", "uid"=>"1234", "info"=>{"name"=>"Example User"}}
これはどうやらデフォルトのOmniauthのモックの値のようです。
つまり、nil
を格納しようとしてもnil
は反映されず、デフォルト値が代わりに反映される仕様になっているようです。なのでどうやってもnil
は格納できない仕様となっています。他の記事ではnil
を代入しているところもありますが、おそらくOmniauthのアップデートのせいで使用できなくなったのではないかと思っています。
以上で、本記事を終わりとします。ありがとうございました!