今回達成すること
今回は、
- トークンを発行する
AuthToken
クラスと(この記事で作成) Authenticator
モジュールのauthenticate_user
メソッド(この記事で作成)- 今から作成するユーザーコントローラーの
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
メソッドを
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の
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」ディレクトリ内にテストファイルが作成されました。
この
api/test/integration
└── authenticator_test.rb
テストのセットアップ
まず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()
でオブジェクトに変換しています。
続いて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["APP_URL"], aud)
end
end
-
assert_in_delta(期待値, 実際値, 想定値範囲)
... 期待値と実際値の差は、想定値範囲内か。をテストする。実行タイミングのズレにより、2週間後の日時と完全一致しません。
そこでこのテストメソッドを使い、1分の誤差を許容するようにしています。
2021年10月05日 修正
- assert_equal(ENV["API_DOMAIN"], aud) を
- assert_equal(ENV["APP_URL"], aud) に書き換えました。
下記記事、
【Rails×JWT】ログイン認証解説とJWT初期設定ファイルの作成
目次「JWTの初期設定ファイルを作成する」の2021年06月09日 修正情報がこちらの記事に反映されておりませんでした。申し訳ありません。
テストコマンドを実行します。
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メソッドをテストする
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["APP_URL"], 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
2021年10月05日 修正
assert_equal(other_user, @controller.send(:current_user)))の綴じカッコが1つ多くありましたので削除しました。
全てのテストが整ったら、テストコマンドを実行しましょう。
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. ユーザーコントローラーをテストする
show
アクションをテストします。
テスト用のログインメソッドを作成する
テスト内でログイン状態を作り出すため、Cookieにトークンをセットするメソッドを作成します。
logged_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アクションのレスポンスをテストする
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
メソッドをテストしました。
さて次回は?
次回は、ログインを実装する
次回でログイン機能が完成しますよー。
修正情報
-
2021年10月05日
- 目次「1. JWTのデコードデータをテストする」のテストコードを一行書き換えました。
- 目次「headerトークンが優先されているか」のコードに、綴じカッコが余計にありましたので削除しました。