アルパカログ

Webエンジニア兼マネージャーがプログラミングやマネジメント、読んだ本のまとめを中心に書いてます。

Ruby on Rails 認証付きAPIとテストの書き方(RSpec)

f:id:otoyo0122:20200815101749p:plain:w300

Ruby on Rails 5で認証付きのAPIを作りたいというケースがあります。

このエントリでは、Ruby on Rails 5でJWTを使った認証付きAPIの作り方と、RSpecを使ったテストの書き方を説明します。

ちなみにJWTはジョットと発音するそうです。

ApplicationController による認証

ApplicationControllerに認証のメソッドを追加し、before_actionに設定します。

認証に失敗した場合、ステータスコード403(Forbidden)を返すようにしています。

ただし、実際にはもっと細かくJWTのエラー(claim)をハンドリングしなければならないことに注意してください。

ここでは説明のシンプルさを優先しています。

ここではJWT を扱うにあたって ruby-jwt というRuby Gemsを使っています。

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action :authenticate

  private

  def authenticate
    _, token = request.headers['Authorization'].split(' ')
    @payload, = JWT.decode token, 'api secret', true, algorithm: 'HS256'
  rescue JWT::DecodeError
    head :forbidden
  end
end

authenticateが認証用のメソッドです。

このように、actionとして呼ばれることを想定していないメソッドはプライベートにしておきましょう。

Only public methods are callable as actions. It is a best practice to lower the visibility of methods (with private or protected) which are not intended to be actions, like auxiliary methods or filters.

これでApplicationControllerを継承したControllerはbefore_actionで認証が課されるになります。

RSpec によるテストの書き方

テストのポイントは、「認証のテストをどう書くか」ということと「Controller のテストでどうやって認証を回避するか」ということでしょう。

1つずつ見ていきましょう。

認証のテスト

認証のテストはControllerのテスト(controller spec)としてではなく、Requestのテスト(request spec)として書きます。

Request Specにもactionが必要になるので、先に適当なControllerとaction, routesを追加しておきましょう。

app/controllers/cats_controller.rb

class CatsController < ApplicationController
  def index
    render json: []
  end
end

config/routes.rb

Rails.application.routes.draw do
  resources :cats, only: [:index]
end

Request Specは次のようになります。

type: :request とすることで Request Specになります。

spec/requests/authentication_spec.rb

require 'rails_helper'

RSpec.describe 'Authentication', type: :request do
  let(:payload) { { 'data' => 'test' } }
  let(:api_secret) { 'api secret' }
  let(:algorithm) { 'HS256' }
  let(:token) { JWT.encode payload, api_secret, algorithm }
  let(:headers) { { 'Authorization' => "Bearer #{token}" } }

  describe 'ApplicationController#authenticate' do
    describe 'response status' do
      subject do
        get '/cats', headers: headers
        response
      end

      context 'invalid token' do
        let(:token) { 'invalid token' }
        it { is_expected.to have_http_status(:forbidden) }
      end

      context 'valid token' do
        it { is_expected.to have_http_status(:ok) }
      end
    end
  end
end

Request Spec とController Specとでは、getが取り得る引数に違いがあるので注意しましょう。

Controller のテストで認証を回避する

Controller Specで認証を回避するには、ApplicationControllerで定義したauthenticateメソッドをstubしてあげれば良いです。

しかしControllerのテストを書くたびにauthenticateをstubするというのはかなり煩雑です。

そこで、authenticateメソッドをstubするためのHelperモジュールを作りincludeします。

まずHelperを次のように定義します。

spec/support/authentication_helper.rb

module AuthenticationHelper
  extend ActiveSupport::Concern

  included do
    before do
      allow_any_instance_of(ApplicationController).to receive(:authenticate)
    end
  end
end

ActiveSupport::Concernがわからない方は、良い解説記事がたくさんあるので調べてみてください。

このモジュールのポイントは、includeされたときにauthenticateメソッドをstubしていることです。

これにより、モジュールをincludeするだけで認証がスキップされるため、使い勝手が良くなります。

次に、このモジュールをrails_helper.rbでincludeします。

spec/rails_helper.rb

...
RSpec.configure do |config|
  ...
  config.include AuthenticationHelper, type: :controller, authentication: :skip
end

これでtype: :controller, authentication: :skipオプションのある全てのController Specで認証がスキップされるようになりました。

Controller Specで認証を回避するには、次のように書きます。

RSpec.describe CatsController, type: :controller, authentication: :skip do
  ...
end

以上です。

このエントリでは、Ruby on Rails 5でJWTを使った認証付きAPIの作り方と、RSpecを使ったテストの書き方を説明しました。

参考になった方は、ぜひ「はてブ」やSNSでシェアしていただけると嬉しいです。