🦓

[Rails]OmniauthのDeveloper StrategyとRspecを使ってユーザー認証をテストする

2023/10/07に公開

はじめに

Omniauthでgithubとgoogleログインを実装したのでこれらのテストについて見ていきます。

Rspecの部分はOmniauth公式のwikiを参考します。
https://github.com/omniauth/omniauth/wiki/Integration-Testing#omniauthconfigtest_mode

OmniauthのDeveloper Strategyを使います。
https://www.rubydoc.info/github/intridea/omniauth/OmniAuth/Strategies/Developer

One strategy, called Developer, is included with OmniAuth and provides a completely insecure, non-production-usable strategy that directly prompts a user for authentication information and then passes it straight through. You can use it as a placeholder when you start development and easily swap in other strategies later.

Developer StrategyはOmniAuthに付属し、ユーザに認証情報を直接入力させ、それをそのまま通過させる仕組みです。開発中にはこれをプレースホルダとして使い、後でGoogleGithub等他のストラテジに入れ替えることができます。

ソーシャルログインを実装されていることを前提で進めていきます。

Devise + Omniauthのソーシャルログイン

環境

Rails 7.0.8
Ruby 3.2.1

tl;dr

  1. テスト用記述を追加する
  2. developerプロバイダーを追加する
  3. テストを作成する

テスト用記述を追加する

270行目あたりにdeveloperストラテジーを記述を追加します。
本番環境以外に使いたいのでif文を追加します。

config/initilizers/devise.rb
Devise.setup do |config|
 # ==> OmniAuth
 config.omniauth :developer unless Rails.env.production?
...
end

developerプロバイダーを追加する

google_authgithubと同じようにdeveloperメソッドを追加します。

app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  skip_before_action :verify_authenticity_token, only: %i[google_oauth2 github developer]
...
  def google_oauth2
    handle_omniauth("Google")
  end

  def github
    handle_omniauth("Github")
  end
  
  def developer
    handle_omniauth("Developer")
  end

  private

  def handle_omniauth(provider)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: provider) if is_navigational_format?
    else
      session["devise.#{provider.downcase}_data"] = request.env["omniauth.auth"].except(:extra)
      redirect_to root_path, alert: @user.errors.full_messages.join("\n")
    end
  end
end

developerプロバイダー用URLを追加する

app/models/user.rb
class User < ApplicationRecord
  devise :omniauthable, omniauth_providers: %i[google_oauth2 github developer]
end

developer用URLを追加されたことを確認します。

rails routes | grep omniauth                          
       user_developer_omniauth_authorize GET|POST /users/auth/developer(.:format)                                                                   users/omniauth_callbacks#passthru
        user_developer_omniauth_callback GET|POST /users/auth/developer/callback(.:format)                                                          users/omniauth_callbacks#developer

http://localhost:3000/users/auth/developerにアクセスし、Not found. Authentication passthru.が表示されます。

セットアップが完了しました。テストを作成していきます。

ユーザーが登録できる

test/integration/omniauth_callback_test.rbを作成します。

test/integration/omniauth_callbacks_test.rb
require "test_helper"

class OmniauthCallbacksTest < ActionDispatch::IntegrationTest
    setup do
	# テスト環境で OmniAuth ストラテジをテストモードに設定
        OmniAuth.config.test_mode = true
	# :developer ストラテジをモックしてテストデータを設定
        OmniAuth.config.mock_auth[:developer] = OmniAuth::AuthHash.new({
          provider: 'developer',
          uid: '123',
          info: {
            email: 'test@example.com',
            name: 'Test User',
            image: nil
          },
        })        
    end

    test "can signup w/ OAuth" do
        assert_difference "User.count" do
            get "/users/auth/developer/callback"
        end

	# ユーザーが指定通りに作成されたことを確認
        user = User.last
        assert_equal "123", user.uid
        assert_equal "test@example.com", user.email
    end
end

mock_authはOmniAuthテスト用に設定されたメソッドです。
テスト中に実際の外部認証プロバイダーへのリクエストを行わずに、テストデータを使ってプロバイダーへのリクエストをシミュレートするために使えます。
https://github.com/omniauth/omniauth/blob/a13cd110beb9538ea51be6c614bf43351c3f4e95/lib/omniauth.rb#L51

OmniAuth::AuthHashに指定できるパラメータ:
https://github.com/omniauth/omniauth/blob/master/lib/omniauth/strategy.rb#L390-L398
infoの部分はユーザーモデルに合わせて設定しましょう。

assert_differenceActiveSupport::TestCaseで提供されるテストヘルパーメソッドの1つです。
このメソッドは、特定の操作(通常はモデルのデータベースレコードの作成や削除)によってデータベース内のレコード数が期待通りに変更されたことを検証するのに使われます。

レコード数が少なくとも 1 以上増加していれば(これはデフォルトの動作です)、テストは合格します。レコード数が変化しないか減少した場合、テストは失敗します。

つまり、このテストは、/users/auth/developer/callback へのアクセス時に、データベースに新しいユーザーレコードが作成されることを確認しています。期待どおりの動作であれば、テストは合格し、そうでなければ失敗します。

ここで一度テストを実行してみます。

rails test test/integration/omniauth_callbacks_test.rb
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 53607

# Running:

.

Finished in 0.483789s, 2.0670 runs/s, 6.2011 assertions/s.
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips

OAuthのダミーデータの作成についてFakerを参考できます。
https://github.com/faker-ruby/faker/blob/main/doc/default/omniauth.md

ユーザーがログインできる

test "can login w/ OAuth" do
  # 1. テストユーザーの作成
  user = User.create!(
    email: "test@example.com",
    name: "Test User",
    password: "password",
    password_confirmation: "password",
    provider: :developer,
    uid: "123"
  )
  user.skip_confirmation!

  # 2. OAuthログイン
  get "/users/auth/developer/callback"

  # 3. ログイン状態の確認
   get "/profile/edit"
   assert_response :success
end

ユーザーをデータベースに作成し、OAuth認証のコールバックによってログインされます。
その後、プロフィール編集ページへのアクセスが成功するかどうかを確認しています。
プロフィール編集ページにアクセスするには認可が必要なためユーザーが正しく認証されていることを確認できます。

 rails test test/integration/omniauth_callbacks_test.rb
Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 63242

# Running:

..

Finished in 0.503905s, 3.9690 runs/s, 3.9690 assertions/s.
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

http://localhost:3000/users/auth/developerにアクセスしログインすることもできます。

ログを見てみるとテストユーザーが作成されました。

18:49:28 web.1  | Started POST "/users/auth/developer/callback" for ::1 at 2023-10-07 18:49:28 +0900
18:49:28 web.1  | D, [2023-10-07T18:49:28.459829 #13295] DEBUG -- omniauth: (developer) Callback phase initiated.
18:49:28 web.1  | Processing by Users::OmniauthCallbacksController#developer as HTML
18:49:28 web.1  |   Parameters: {"name"=>"Test User", "email"=>"test@example.com"}
...
irb(main):001:0> User.last
  User Load (0.7ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 13, email: "test@example.com", created_at: "2023-10-07 18:49:28.748623000 +0900", updated_at: "2023-10-07 18:49:28.836602000 +0900", name: "Test User", avatar: nil, description: nil, provider: "developer", uid: "test@example.com", role: "general">

uidはデフォルトでユーザーのメールアドレスです。

https://github.com/omniauth/omniauth/blob/master/lib/omniauth/strategies/developer.rb#L31-L35

Ominiauthのパラメータをテストする

authオブジェクトを取得するためauthメソッドを定義しましたが各々のパラメータだけを取得したい場合['omniauth.params']を使うことができます。

def auth
   request.env['omniauth.auth']
end

def omniauth_params
   request.env['omniauth.params']
end

https://github.com/omniauth/omniauth/blob/a13cd110beb9538ea51be6c614bf43351c3f4e95/lib/omniauth/strategy.rb#L237-L238

パラメータの値をユーザーに渡します:

  User.create(
    email: omniauth_params["email"], # request.env['omniauth.params']['email']
    name: omniauth_params["email"],
    password: "password",
    password_confirmation: "password",
    provider: omniauth_params["provider"],
    uid: "omniauth_params["uid"]"
  )

注意点としてはテストするときにPOSTリクエストを使います。

POST "/users/auth/developer/callback?name=Test+User&email=test@example.com"

Rspecでテストする

続いてRspecでログインの動きをテストします。gemをインストールします。

Gemfile
group :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'capybara'
  gem 'selenium-webdriver'
end

Chromeのバージョンを見つからないエラーが発生しました。
こちらの記事を参考して解決しました。

https://qiita.com/jnchito/items/f994dd3ac2cdc39bff8c

Omniauthモジュールを作成する

spec/support/omniauth_helper.rb
module OmniauthHelper
  def mock_omniauth(provider, uid:, email:, name:)
    OmniAuth.config.test_mode = true
    OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
      provider: provider.to_s,
      uid: uid,
      info: {
        email: email,
        name: name
      }
    })
  end

  def mock_google_oauth2_auth
    mock_omniauth(
      'google_oauth2',
      uid: '123456',
      email: 'test@example.com',
      name: 'Test User'
    )
  end

  def mock_github_auth
    mock_omniauth(
      'github',
      uid: 'github_uid',
      email: 'github@example.com',
      name: 'GitHub User'
    )
  end
end

rails_helper.rbにある読み込み記述のコメントアウトを解除し、モジュールをincludeします。

spec/rails_helper.rb
# コメントアウトを解除する
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

# モジュールをincludeする
RSpec.configure do |config|
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include Devise::Test::IntegrationHelpers, type: :request

  config.include OmniauthHelper
end

システムspecを作成する

spec/system/oauth_authentication_spec.rb
require 'rails_helper'

RSpec.describe "GitHub Authentication", type: :system, js: true do
  include OmniauthHelper
  before do
    Rails.application.env_config["devise.mapping"] = Devise.mappings[:user]
    Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:github]
  end

  it "allows the user to sign in with GitHub" do
    mock_github_auth

    visit new_user_session_path
    within('form.button_to[action="/users/auth/github"]') do
      click_button 'Signin with Github'
    end

    expect(page).to have_content("Github アカウントによる認証に成功しました") 
  end

  it "allows the user to sign in with Google" do
    mock_google_oauth2_auth

    visit new_user_session_path
    within('form.button_to[action="/users/auth/google_oauth2"]') do
      click_button 'Signin with Google'
    end

    expect(page).to have_content("Google アカウントによる認証に成功しました") 
  end
end

expect(page).to have_contentの部分をアプリに合わせて調整してください。
テストを実行してみます。

rspec spec/system/oauth_authentication_spec.rb


GitHub Authentication
  allows the user to sign in with GitHub
  allows the user to sign in with Google

Finished in 4.12 seconds (files took 2.45 seconds to load)
2 examples, 0 failures

Omniauthのテストケース

こちらはOmniAuthのモック機能のテストケースです。
認証プロバイダーと各パラメータが取得と設定されていることをテストしてます。

https://github.com/omniauth/omniauth/blob/a13cd110beb9538ea51be6c614bf43351c3f4e95/spec/omniauth_spec.rb#L102-L132

終わりに

Omniauthのテストを悩んだのでDeveloper Strategyを見つけて良かったです!

Discussion