所有しないオブジェクトの処理: 小林研 Rails Tips (79)

このページの内容は以下のリポジトリに1日遅れで反映されます(記事執筆前に前日分をコミットしています)。

https://github.com/hkob/hkob_blog

はじめに

Rails Tips の 79 回目です。昨日は記事にユーザの所有・非所有を取り扱うメソッドを追加しました。これを使って、所有しないオブジェクトに対する edit の対応を行います。

Rails をはじめよう - Railsガイド

article_spec

article_spec の頭に not_mine という記事用の let があるのでコメントを外しました。

RSpec.describe ArticlesController, type: :request do
  let!(:article) { articles :can_delete }
  let!(:object) { article }
  let!(:attrs) { object.attributes }
  let(:not_mine) { articles :article1 }

昨日は hkob というユーザでログインしていましたが、can_delete オブジェクトの所有者でログインした方がこの後が書きやすいので、can_delete でのログインに変更しました。

  context "when login by can_delete" do
    user_login :can_delete

subject では object を edit していましたが、one という変数に変更しています。この one はその下のwhen owned object および when not owned object のコンテキストの中で let で遅延評価しています。前者はこれまで通り記事更新ページになりますが、自分が所有しない記事についてはトップページに戻るようにしています。基本的にはそのようなリンクはつくらないのですが、ユーザが id を勝手に書き換えて他人のページを編集したりするときの対策です。

    describe "GET #edit" do
      subject { -> { get edit_article_path(one) } }
      context "when owned object" do
        let(:one) { object }
        it_behaves_like "レスポンスコード確認", 200
        it_behaves_like "描画結果に文字列が含まれている?", "記事更新"
      end

      context "when not owned object" do
        let(:one) { not_mine }
        it_behaves_like "レスポンスコード確認", 302
        it_behaves_like "rootリダイレクト確認"
      end
    end

これを通すために、take_one で取得した @article を昨日の owned_by? で所有確認し、所有権がないときには root_path にリダイレクトします。

  def take_one
    @article = object_from_params_id Article
    redirect_to root_path, alert: I18n.t("errors.messages.not_owned") unless @article.owned_by?(current_user)
  end

errors.messages.not_owned は以下のように翻訳テキストを準備しました。

ja:
  errors:
    (中略)
    messages:now_owned: は自分のものでなければなりません

ログインしていないときには、current_user が nil になっています。ここで owned_by? に nil を渡すとエラーになってしまっていました。テストを先に修正します。

    it_behaves_like "配列メソッド呼び出し" do
      let(:test_set) do
        {
          archived?: [nil, [false, false]],
          owned_by?: [
            [nil], [false, false],
            article.user, [true, false]
          ],
          user_name: [nil, %w[hkob can_delete]],
        }
      end
    end

owned_by? の実装を修正します。u.id の部分でエラーになるので、 &. でエラーにならないようにしました。

  # @param [User, nil] u 確認するユーザー
  # @return [TrueClass, FalseClass] ユーザーが所有していたら true
  def owned_by?(u)
    user_id == u&.id
  end

これで edit は通るようになりましたが、show でエラーになるようになりました。show はログインしていない状態でも take_one を読んでしまうにしていたからでした。この部分は共通化できないので、show における @article の取得は個別に実施することにします。まず、show で take_one を呼び込まないようにします。

  before_action :take_one, only: %i[edit update destroy]

そして、最後に show で article を呼び込むようにしておきます。

  def show
    @article = object_from_params_id Article
    @comments = @article.comments.order_created_at_desc
    @comment = objects_from_params(Comment) || @article.comments.build
  end

おわりに

これで、edit の際に所有権を確認するようにしました。take_one で修正したので、実装的には他のアクション (update, destroy) でも有効になっているはずです。ただし、テストは行なっていないので、明日はテストで無事に所有権が確認できるていることを確認します。