Udemy 12. サーバーサイドのログイン認証 #06
2020年10月01日に公開

【Rails×JWT】AuthTokenクラスとAuthenticatorモジュールをテストする

今回達成すること

今回は、

  1. トークンを発行するAuthTokenクラスと(この記事で作成)
  2. Authenticatorモジュールのauthenticate_userメソッド(この記事で作成)
  3. 今から作成するユーザーコントローラーのshowアクション

の3つをテストします。

  • トークンに期待される値は含まれているか、
  • authenticate_userメソッドが正しく機能しているか
  • showアクションのレスポンスは正しいか

を検証します。

ユーザーコントローラーにアクションを追加する

Authenticatorモジュールは、コントローラーに接続してテストします。

そこで、ユーザーコントローラーに「現在のユーザーを取得する」showアクションを追加します。

同時に、ユーザー一覧を表示するindexアクションはもう使用しないので削除しましょう。

api/app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
  # 追加
	before_action :authenticate_user

  # 削除
  # def index
  #   users = User.all
  #   render json: users.as_json(only: [:id, :name, :email, :created_at])
  # end

  # 追加
  def show
    render json: current_user.my_json
  end
end

共通のJSONを返すメソッドの追加

ユーザー共通のJSONを返すmy_jsonメソッドをuser.rbに追加します。

api/app/models/user.rb
  ...
  def email_activated?
    users = User.where.not(id: id)
    users.find_activated(email).present?
  end

  # 追加
  # 共通のJSONレスポンス
  def my_json
    as_json(only: [:id, :name, :email, :created_at])
  end

  private
  ...

showアクションへのルートを追加する

Railsのroutes.rbも追加します。

api/config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :users, only:[] do
        get :current_user, action: :show, on: :collection
      end
    end
  end
end
  • get :current_user, action: :show … showアクションを/users/current_userで接続できるように変更している。

  • on: :collection … ルートにidを必要としない場合に使用するオプション。

    通常、showアクションはuser_idを必要とします。

    => /api/v1/users/:user_id/current_user

    このオプションをつけると、パスのuser_idが不要となります。

    => /api/v1/users/current_user

正しくルートが追加できたか確認しておきましょう。

root $ docker-compose run --rm api rails routes

current_user_api_v1_users GET    /api/v1/users/current_user(.:format) api/v1/users#show
...

OK!!これで現在のユーザーを返すアクションができました。

認証用のテストファイルを作成する

テストファイルを作成します。

モデル、コントローラーに属さないテストファイルなので「integration(インテグレーション)」ディレクトリ内に作成します。

root $ docker-compose run --rm api rails g integration_test Authenticator

「integration」ディレクトリ内にテストファイルが作成されました。

このauthenticator_test.rb内で、認証関連メソッドが正しく動いているかテストします。

api/test/integration
└── authenticator_test.rb

テストのセットアップ

まずtest_hepler.rbに、コントローラーにアクセスURLを生成するapi_urlメソッドと、レスポンスを受け取るresponse_bodyメソッドを作成します。

api/test/test_helper.rb
...
class ActiveSupport::TestCase
  ...

  def active_user
    User.find_by(activated: true)
  end

  # 以下、追加
  def api_url(path = "/")
    "#{ENV["BASE_URL"]}/api/v1#{path}"
  end

  # コントローラーのJSONレスポンスを受け取る
  def response_body
    JSON.parse(@response.body)
  end
end
  • @response.body … コントローラーアクションのrender json:で返される値は、@response.bodyに入っています。

    JSON文字列で返されるため、JSON.parse()でオブジェクトに変換しています。

続いてauthenticator_test.rbに、setupメソッドを追加しましょう。

テストファイルで使用する@user@tokenのインスタンス変数を作成します。

api/test/integration/authenticator_test.rb
require 'test_helper'

class AuthenticatorTest < ActionDispatch::IntegrationTest
	# 追加
  def setup
    @user = active_user
    @token = @user.to_token
  end
end

1. JWTのデコードデータをテストする

トークンデコード時に必要な値が含まれているかをテストを行います。

  • subject(サブジェクト)@user.idと一致しているか
  • expiration(エクスパレイション)が含まれているか
  • 有効期限は2週間後であるか
  • audience(オーディエンス)が含まれているか
  • audienceの値は、APIドメインと一致しているか

以上、5つをテストします。

api/test/integration/authenticator_test.rb
 ...
  def setup
    @user = active_user
    @token = @user.to_token
  end

  # 追加
  test "jwt_decode" do
    payload = UserAuth::AuthToken.new(token: @token).payload
    sub = payload["sub"]
    exp = payload["exp"]
    aud = payload["aud"]

    # subjectは一致しているか
    assert_equal(@user.id, sub)

    # expirationの値はあるか
    assert exp.present?

    # tokenの有効期限は2週間か(1分許容)
    assert_in_delta(2.week.from_now, Time.at(exp), 1.minute)

    # audienceの値はあるか
    assert aud.present?

    # audienceの値は一致しているか
    assert_equal(ENV["API_DOMAIN"], aud)
  end

end
  • assert_in_delta(期待値, 実際値, 想定値範囲) … 期待値と実際値の差は、想定値範囲内か。をテストする。

    実行タイミングのズレにより、2週間後の日時と完全一致しません。

    そこでこのテストメソッドを使い、1分の誤差を許容するようにしています。

テストコマンドを実行します。

root $ docker-compose run --rm api rails t integration

...
Finished in 2.61979s
6 tests, 51 assertions, 0 failures, 0 errors, 0 skips

無事テストが通ったので次へ進みましょう。

2. authenticate_userメソッドをテストする

users_controller.rbに追加したbefore_action :authenticate_user(オーセンティケイトユーザー)メソッドが正しく機能しているかテストします。

@userとcurrent_userは一致しているか

トークンを発行した@userと、authenticate_userで返されるcurrent_userが一致しているかテストします。

getメソッドで"/users/current_user"にアクセスし、返されるユーザーが一致しているか確認しましょう。

api/test/integration/authenticator_test.rb(以下、省略)
    ...
    assert_equal(ENV["API_DOMAIN"], aud)
  end

  # 追加
  test "authenticate_user_method" do
    key = UserAuth.token_access_key

    # @userとcurrent_userは一致しているか
    cookies[key] = @token
    get api_url("/users/current_user")
    assert_response 200
    assert_equal(@user, @controller.send(:current_user))
  end
end

無効なトークンはアクセス不可か

今の@tokenに文字列を足して無効なトークンでアクセスしてみましょう。

レスポンスに401が返ってこれば成功です。

  ...
  test "authenticate_user_method_test" do
    ...

    # 追加

    # 無効なトークンはアクセス不可か
    invalid_token = @token + "a"
    cookies[key] = invalid_token
    get api_url("/users/current_user")
    assert_response 401

    # 何も返されないか
    assert @response.body.blank?
  end
end

トークンがない場合もアクセス不可か

トークンがnilの場合も401が返ってくるかテストします。

	...
  test "authenticate_user_method_test" do
    ...
    # 追加

    # トークンがない場合もアクセス不可か
    cookies[key] = nil
    get api_url("/users/current_user")
    assert_response 401
  end
end

トークンの有効期限内はアクセス可能か

有効期限内の場合に、正しくアクセスできているか確認しましょう。

時間経過後のテストを実行するには、travel_toメソッドのブロック内に期待される動作を記述します。

今回は2週間後の1分前にアクセス可能であることをテストしています。

   ...
    # 追加

    # トークンの有効期限内はアクセス可能か
    travel_to (UserAuth.token_lifetime.from_now - 1.minute) do
      cookies[key] = @token
      get api_url("/users/current_user")
      assert_response 200
      assert_equal(@user, @controller.send(:current_user))
    end
  end
end

トークンの有効期限が切れた場合はアクセス不可か

有効期限切れ後にはアクセスできず、401エラーが返ってきていることをテストします。

    ...
   # 追加

    # トークンの有効期限が切れた場合はアクセス不可か
    travel_to (UserAuth.token_lifetime.from_now + 1.minute) do
      cookies[key] = @token
      get api_url("/users/current_user")
      assert_response 401
    end
  end
end

headerトークンが優先されているか

リクエストヘッダーにトークンがある場合、優先されているかをテストします。

  • @user以外のユーザーが発行したトークンを用意し、
  • そのトークンをリクエストヘッダーに設定して
  • "/users/current_user"にアクセスします。
    ...
    # 追加
    # headerトークンが優先されているか
    cookies[key] = @token
    other_user = User.where.not(id: @user.id).first
    header_token = other_user.to_token

    get api_url("/users/current_user"), headers: { Authorization: "Bearer #{header_token}" }

    # Authenticatorのトークンはheaderトークンか
    assert_equal(header_token, @controller.send(:token))

    # current_userはother_userか
    assert_equal(other_user, @controller.send(:current_user)))
  end
end

全てのテストが整ったら、テストコマンドを実行しましょう。

root $ docker-compose run --rm api rails t integration

...
Finished in 1.73781s
7 tests, 61 assertions, 0 failures, 0 errors, 0 skips

OK!無事テストが通りました。

3. ユーザーコントローラーをテストする

users_controller.rbに追加したshowアクションをテストします。

テスト用のログインメソッドを作成する

テスト内でログイン状態を作り出すため、Cookieにトークンをセットするメソッドを作成します。

test_helper.rblogged_inメソッドを追加しましょう。

api/test/test_helper.rb
  ...
  # コントローラーのJSONレスポンスを受け取る
  def response_body
    JSON.parse(@response.body)
  end

  # 以下、追加
  # テスト用Cookie(Rack::Test::CookieJar Class)にトークンを保存する
  def logged_in(user)
    cookies[UserAuth.token_access_key] = user.to_token
  end
end

showアクションのレスポンスをテストする

users_controller_test.rb内で、ユーザーコントローラーのshowアクションから想定したJSONが返されるかテストします。

api/test/controllers/api/v1/users_controller_test.rb
require 'test_helper'

class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = active_user
    logged_in(@user)
  end

  test "show_action" do
    # レスポンスは正しいか
    get api_url("/users/current_user")
    assert_response 200
    assert_equal(@user.my_json, response_body)
  end
end

コントローラーテストを実行しましょう。

root $ docker-compose run --rm api rails t controllers

...
Finished in 1.88771s
8 tests, 63 assertions, 0 failures, 0 errors, 0 skips

OKですね。今回の作業は以上です。

コミットしとく

Authenticatorモジュールに用意したdelete_cookieメソッドのテストは次回に行います。

ここまでの編集をコミットしておきましょう。

root $ cd api
api $ git add -A && git commit -m "add_authenticator_test.rb" && cd ..
root $

まとめ

今回は、JWTデコード時の値の検証と、コントローラー内で使用するauthenticate_userメソッドをテストしました。

さて次回は?

次回は、ログインを実装するuser_token_controller.rbを追加します。

次回でログイン機能が完成しますよー。

あなたの力になれること
私自身が独学でプログラミングを勉強してきたので、一人で学び続ける苦しみは痛いほど分かります。そこで、当時の私がこんなのあったら良いのにな、と思っていたサービスを立ち上げました。周りに質問できる人がいない、答えの調べ方が分からない、ここを聞きたいだけなのにスクールは高額すぎる...。そんな方に向けた単発・短期間メンターサービスを行っています。下のサービスへお進みください。
独学プログラマのサービス
次の記事はこちら
1. このカテゴリーの歩き方 #01
【お知らせ】UdemyでRails × Nuxt.jsの動画を公開することになりました
1. このカテゴリーの歩き方 #02
【随時更新】アプリケーション仕様書
1. このカテゴリーの歩き方 #03
【随時更新】このカテゴリーの歩き方
1. このカテゴリーの歩き方 #04
(Docker+Rails6+Nuxt.js+PostgreSQL)=>Heroku 環境構築~デプロイまでの手順書
2. Docker入門 #01
Docker for Macをインストールする手順
2. Docker入門 #02
分かるDocker解説。仮想環境・コンテナ・Dockerイメージ・Dockerfileとは何か?
2. Docker入門 #03
分かるDocker解説。DockerComposeとは何か?
3. Dockerを使ったRails+Nuxt.js環境構築 #01
【Docker+Rails6+Nuxt.js】今回作成するアプリの開発環境の全体像を知ろう
3. Dockerを使ったRails+Nuxt.js環境構築 #02
【MacOS】Homebrew経由でGitをインストールする方法
3. Dockerを使ったRails+Nuxt.js環境構築 #03
Rails6を動かすAlpineベースのDockerfileを作成する(AlpineLinuxとは何か)
3. Dockerを使ったRails+Nuxt.js環境構築 #04
Nuxt.jsを動かすAlpineベースのDockerfileを作成する(C.UTF-8とは何か)
3. Dockerを使ったRails+Nuxt.js環境構築 #05
.envファイルを使ったdocker-compose.ymlの環境変数設計
3. Dockerを使ったRails+Nuxt.js環境構築 #06
Rails6・Nuxt.js・PostgreSQLを動かすdocker-compose.ymlファイルを作成する
3. Dockerを使ったRails+Nuxt.js環境構築 #07
docker-compose.ymlを使ってRails6を構築する(PostgreSQLパスワード変更方法)
3. Dockerを使ったRails+Nuxt.js環境構築 #08
docker-compose.ymlを使ってNuxt.jsを構築する
4. 複数プロジェクトのGit管理 #01
複数プロジェクトで行うGit管理の全体像を理解しよう(Gitサブモジュール解説)
4. 複数プロジェクトのGit管理 #02
【Git】既存の子ディレクトリをサブモジュール管理に変更する手順
4. 複数プロジェクトのGit管理 #03
【GitHub】秘密鍵の生成・公開鍵を追加・SSH接続するまでを画像で分かりやすく
4. 複数プロジェクトのGit管理 #04
【GitHub】リモートリポジトリの追加・サブモジュールのリンク設定を行う
5. RailsAPI×Nuxt.js初めてのAPI通信 #01
【Rails6】"Hello" jsonを返すコントローラを作成する
5. RailsAPI×Nuxt.js初めてのAPI通信 #02
【Nxut.js】axiosの初期設定を行う(baseURL・browserBaseURLを解説)
5. RailsAPI×Nuxt.js初めてのAPI通信 #03
【Rails6】Gem rack-corsを導入してCORS設定を行う(オリジン・CORSとは何か)
6. Heroku.ymlを使ったDockerデプロイ #01
デプロイ準備。Herokuへ新規会員登録を行いHerokuCLIをインストールする
6. Heroku.ymlを使ったDockerデプロイ #02
heroku.yml解説編。Docker環境のRails6をHerokuにデプロイする(1/2)
6. Heroku.ymlを使ったDockerデプロイ #03
HerokuCLI-manifestのデプロイ解説編。Docker環境のRails6をHerokuにデプロイする(2/2)
6. Heroku.ymlを使ったDockerデプロイ #04
Dockerfile解説編。Docker環境のNuxt.jsをHerokuにデプロイする(1/2)
6. Heroku.ymlを使ったDockerデプロイ #05
デプロイ完結編。Docker環境のNuxt.jsをHerokuにデプロイする(2/2)
7. モデル開発事前準備 #01
【Rails6】application.rbの初期設定(タイムゾーン・I18n・Zeitwerk)
7. モデル開発事前準備 #02
【Rails6】モデル開発に必要なGemのインストールとHirb.enableの自動化
7. モデル開発事前準備 #03
【Docker+Rails】A server is already running. Check /tmp/pids/server.pidエラーの対応
7. モデル開発事前準備 #04
【Docker】<none>タグのイメージを一括削除する & Rails .gitignoreの編集
8. ユーザーモデル開発 #01
Railsユーザーモデル作成。テーブル設計・ユーザー認証設計を理解する
8. ユーザーモデル開発 #02
Railsユーザーモデルのバリデーション設定(has_secure_password解説)
8. ユーザーモデル開発 #03
Railsバリデーションエラーメッセージの日本語化(ja.yml設定方法)
8. ユーザーモデル開発 #04
EachValidatorクラスのカスタムバリデーション設定(Rails6/lib以下読込)
8. ユーザーモデル開発 #05
Rails環境ごとにSeedデータ切り替えるseeds.rbの書き方
8. ユーザーモデル開発 #06
Rails6から導入された並列テストを理解する
8. ユーザーモデル開発 #07
Railsユーザーモデルバリデーションテスティング(name/email/password)
8. ユーザーモデル開発 #08
Nuxt.jsからRailsのユーザーテーブルを取得しHerokuにデプロイする
9. Nuxt.jsフロント開発事前準備 #01
【Nuxt.js2.13超解説】バージョンアップ手順と6つの新機能+2つの変更点
9. Nuxt.jsフロント開発事前準備 #02
Docker AlpineベースのNode.js上で動くNuxt.jsにVuetifyを導入する
9. Nuxt.jsフロント開発事前準備 #03
VuetifyにカスタムCSSを導入してオリジナルブレイクポイントを作る
9. Nuxt.jsフロント開発事前準備 #04
Nuxt.jsにnuxt-i18nを導入して国際化に対応する
10. ログイン前のレイアウト構築 #01
Nuxt.jsのレイアウト・ページ・コンポーネントの役割を理解しよう
10. ログイン前のレイアウト構築 #02
Nuxt.js ウェルカムページを構成するコンポーネントファイル群を作成しよう(1/4)
10. ログイン前のレイアウト構築 #03
Nuxt.js ウェルカムページにアイキャッチ画像・アプリ名・メニューボタンを表示しよう(2/4)
10. ログイン前のレイアウト構築 #04
Nuxt.js addEventListenerでスクロールを検知しツールバーの色を変化させよう(3/4)
10. ログイン前のレイアウト構築 #05
Nuxt.js ウェルカムページをレスポンシブデザインに対応させよう(4/4)
10. ログイン前のレイアウト構築 #06
Nuxt.js 会員登録ページのレイアウトファイルを作成しよう(1/4)
10. ログイン前のレイアウト構築 #07
Nuxt.js 名前、メール、パスワードのコンポーネントファイルを作成しよう(2/4)
10. ログイン前のレイアウト構築 #08
Nuxt.js 親子コンポーネント間の双方向データバインディングを実装する(3/4)
10. ログイン前のレイアウト構築 #09
Nuxt.js Vuetifyのv-text-fieldを使った会員登録フォームのバリデーション設定(4/4)
10. ログイン前のレイアウト構築 #10
Nuxt.js ログインページ実装とHerokuデプロイまで(router. replaceとpushの違いとは)
11. ログイン後のレイアウト構築 #01
Nuxt.js ログイン後のツールバーを作成しよう(inject解説)
11. ログイン後のレイアウト構築 #02
Nuxt.js アカウントメニューページ・ログアウト機能を実装しよう(nuxt-child解説)
11. ログイン後のレイアウト構築 #03
Nuxt.js ログイン後のトップページにプロジェクト一覧を表示しよう
11. ログイン後のレイアウト構築 #04
Nuxt.js プロジェクトページにVuetifyのナビゲーションドロワーを追加しよう
11. ログイン後のレイアウト構築 #05
Nuxt.js paramsIDからプロジェクトを検索してVuexに保存しよう
12. サーバーサイドのログイン認証 #01
JWTとは何か?(ruby-jwtのインストール)
新着
12. サーバーサイドのログイン認証 #02
【Rails×JWT】ログイン認証解説とJWT初期設定ファイルの作成
新着
12. サーバーサイドのログイン認証 #03
【Rails×JWT】トークン発行とデコードを行うAuthTokenクラスの作成
新着
12. サーバーサイドのログイン認証 #04
【Rails×JWT】 ログイン判定を行うAuthenticatorモジュールの作成
新着
12. サーバーサイドのログイン認証 #05
【Rails×JWT】UserクラスからJWTを扱うTokenizableモジュールの作成
新着
12. サーバーサイドのログイン認証 #06
【Rails×JWT】AuthTokenクラスとAuthenticatorモジュールをテストする
新着
12. サーバーサイドのログイン認証 #07
【Rails×JWT】JWTをCookieに保存するログインコントローラーの実装
新着
12. サーバーサイドのログイン認証 #08
【Rails×JWT】ログインコントローラーのテストとHerokuデプロイ
新着
SPA開発
ブログ構築
小ネタ集