【Rails Tutorial 機能拡張】REST API を追加してみた

こんにちは。業務でRuby / Rails によるバックエンドエンジニアをしています。
業務経歴が半年立ったので、改めて幅広くRailsについて知りたいと思いRails Tutorial を完遂しました。

最後の｢14.4.1 サンプルアプリケーションの機能を拡張する｣の項の[REST API](https://railstutorial.jp/chapters/following_users?version=6.0#sec-exercises_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のフォーマットごとにレスポンスを返せるようにしました。

```app/controllers/users_controller.rb
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ステータスコード
- レスポンスの内容

```test/integration/users_api_test.rb 
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](https://railstutorial.jp/chapters/following_users?version=6.0#sec-exercises_rest_api)の項には、
>セキュリティには十分注意してください。認可されたユーザーにのみAPIアクセスを許可する必要があります。

との記載がありましたが、この認可の機能についてはブラウザでログインしたユーザーのみを認可するというやり方をとりました。
とはいえ、APIとしてブラウザ外から利用されることを前提に考えるとこの方法では良くなかったような気がしています。
こうしたAPIの認可はリクエストのクエリパラメーターにuser_idやトークンをついかして、あらかじめ発行したトークンを持つユーザーに各アクションを許可するようなやり方があるようですね。
上記のようなブラウザ外から利用されることを前提にした認可機能についても今後実装してみたいなと思っています。