こんにちは。業務でRuby / Rails によるバックエンドエンジニアをしています。
業務経歴が半年立ったので、改めて幅広くRailsについて知りたいと思いRails Tutorial を完遂しました。
最後の「14.4.1 サンプルアプリケーションの機能を拡張する」の項のREST API機能の実装に取り組んでみたので、コードを掲載します。
誤っている点や改善点などあれば教えていただけると幸いです。
環境
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.4
BuildVersion: 19E287
$ ruby -v
ruby 2.6.5
$ rails -v
Rails 6.0.0
前提
Usersリソースのみに機能を追加しました。
すなわち、index
アクション、show
アクション、create
アクション、update
アクションについての実装をしました。
アプリケーションコード
respond_to
メソッドを使って要求されたHTMLとJSONのフォーマットごとにレスポンスを返せるようにしました。
class UsersController < ApplicationController
before_action :logged_in_user, only: %i[index edit update following followers]
before_action :correct_user, only: %i[edit update]
before_action :admin_user, only: :destory
def index
@users = User.where(activated: true).paginate(page: params[:page])
respond_to do |format|
format.html
format.json { render json: @users, status: 200 }
end
end
def show
@user = User.find(params[:id])
redirect_to(root_url) && return unless @user.activated?
@microposts = @user.microposts.paginate(page: params[:page])
respond_to do |format|
format.html
format.json { render json: @user, status: 200 }
end
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
respond_to do |format|
format.html do
@user.send_activation_email
flash[:info] = 'Please check your email to activate your account.'
redirect_to root_url
end
format.json { render json: @user, status: 200 }
end
else
respond_to do |format|
format.html { render 'new' }
format.json { render json: @user.errors, status: 400 }
end
end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update(user_params)
respond_to do |format|
format.html do
flash[:success] = 'Profile updates'
redirect_to @user
end
format.json { render json: @user, status: 200 }
end
else
respond_to do |format|
format.html { render 'edit' }
format.json { render json: @user.errors, status: 400 }
end
end
end
def destroy
@user = User.find(params[:id])
@user.destroy
flash[:success] = 'User deleted'
redirect_to users_url
end
def following
@title = 'Following'
@user = User.find(params[:id])
@users = @user.following.paginate(page: params[:page])
render 'show_follow'
end
def followers
@title = 'Following'
@user = User.find(params[:id])
@users = @user.followers.paginate(page: params[:page])
render 'show_follow'
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def correct_user
redirect_to root_url unless correct_user?(User.find(params[:id]))
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
end
テストコード
テスト項目は主に下記としました。
- HTTPステータスコード
- レスポンスの内容
require 'test_helper'
class UsersApiTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test 'GET /users' do
log_in_as(@user)
get users_path, as: :json
assert_response 200
user_response = JSON.parse(response.body)
assert 30, user_response.count
end
test 'GET /users/:id' do
log_in_as(@user)
get user_path(@user), as: :json
assert_response 200
user_response = JSON.parse(response.body)
assert @user.name, user_response['name']
assert @user.email, user_response['email']
end
test 'POST /users' do
# 新規ユーザー作成失敗
assert_no_difference 'User.count' do
post users_path,
params: { user: {
name: '',
email: ''
} },
as: :json
end
assert_response 400
error_response = JSON.parse(response.body)
assert_includes error_response['name'], "can't be blank"
assert_includes error_response['email'], "can't be blank"
# 新規ユーザー作成成功
assert_difference 'User.count', 1 do
post users_path,
params: { user: {
name: 'New User',
email: 'new_user@example.com',
password: 'password',
password_confirmation: 'password'
} },
as: :json
end
assert_response 200
user_response = JSON.parse(response.body)
assert_equal 'New User', user_response['name']
assert_equal 'new_user@example.com', user_response['email']
end
test 'PUT /users/:id' do
log_in_as(@user)
# 更新前のユーザー情報の確認
get user_path(@user), as: :json
non_updated_user_response = JSON.parse(response.body)
assert_not_equal 'Updated User', non_updated_user_response['name']
assert_not_equal 'updated@example.com', non_updated_user_response['email']
# ユーザー情報更新失敗
patch user_path(@user),
params: { user: {
name: '',
email: ''
} },
as: :json
assert_response 400
error_response = JSON.parse(response.body)
assert_includes error_response['name'], "can't be blank"
assert_includes error_response['email'], "can't be blank"
# ユーザー情報更新成功
patch user_path(@user),
params: { user: {
name: 'Updated User',
email: 'updated@example.com'
} },
as: :json
assert_response 200
updated_user_response = JSON.parse(response.body)
assert_equal 'Updated User', updated_user_response['name']
assert_equal 'updated@example.com', updated_user_response['email']
end
end
最後に
REST APIの項には、
セキュリティには十分注意してください。認可されたユーザーにのみAPIアクセスを許可する必要があります。
との記載がありましたが、この認可の機能についてはブラウザでログインしたユーザーのみを認可するというやり方をとりました。
とはいえ、APIとしてブラウザ外から利用されることを前提に考えるとこの方法では良くなかったような気がしています。
こうしたAPIの認可はリクエストのクエリパラメーターにuser_idやトークンをついかして、あらかじめ発行したトークンを持つユーザーに各アクションを許可するようなやり方があるようですね。
上記のようなブラウザ外から利用されることを前提にした認可機能についても今後実装してみたいなと思っています。