testable constraint

関心

Rails を利用するに当たって先日の脆弱性

Rails 4.2.11.1, 5.0.7.2, 5.1.6.2, 5.2.2.1, and 6.0.0.beta3 have been released! | Riding Rails

の対策などはいちいち controller に処理が渡る前に処理を終わらせて 404 などのエラーを返してしまいたい。実際に稼働中のアプリに脆弱性がない場合でも悪意のある bot 対策という意味ではいろいろなケースで同様のことが言える。

これは具体的には routing の constraint を利用することで実現可能である。1

このような設計にしておくことには controller の before_action callback などで処理する手法に比べて以下のようなメリットがある。

  • action の dispatch に掛かるコストを節約できる(脆弱性目的のアクセスは過負荷になりがち)
  • controller は自分の責務に集中したコードだけを書けばよいので読み書きしやすい

ではどうやってこれを実現すればよいだろうか。

複雑なconstraintをProcで書くのは無理がある

しかし constraint を Proc で書こうとすると非常に書きにくい。単純な文字列比較などならよいが、request オブジェクトの中身を検証するようなものは

  • コードが長くなって rouitng が読みにくくなる
  • そもそもテストしにくい
    • routing のテストになるが、routing spec の DSL ではそのまま request header をセットすることができない
  • 条件を変えてテストコードを書いておきたいが、例えば constraint が外れていた場合に実際に問題が発生する(脆弱性に繋がる例外があがる)ことを検証しようとするとますます複雑になる

という問題がある。

Objectで書こう

constraint は Object と method で書くこともできる。

Rails Routing from the Outside In — Ruby on Rails Guides

これを読むと

#matches?

を呼べさえすればよい。#matches? に request が渡ってくるのでオッケーかどうかを true か false で返してあげるという寸法だ。

class Constraint
  def matches?(request)
  end
end

で new して渡しても

class Constraint
  def self.matches?(request)
  end
end

でそのまま渡しても include Singleton してもよい。

テストする場合は request object をそのまま渡せばよいので、ActionDispatch::Request や ActionDispatch::TestRequest を使えばよい。

これで routing spec の DSL が足りないといって気を揉む必要もなくなる。

条件を変える

constraint のテストだけでは constraint が与えた request に基づいて true を返すか false を返すかしか検証できない。慣れてしまえばこれだけで十分と判断できるかもしれないが、本当のことを言うと特定の条件を満たす request に対して Rails アプリがどのように反応するかもテストしたいはずである。つまり、constraint が有効な場合と無効な場合のテストが書けないと不安にならないだろうか?

constraint は基本的に routing にそのまま書くので適用されっぱなしになってしまう。Proc オブジェクトで書く場合は Proc の中で条件分岐させることができるが、Object と matches? メソッドで書く場合はそういうわけにいかない。

そこで stub out 可能なメソッドを用意しておく。こんな感じだ。

class Constraint
  def enabled?
    true
  end

  def matches?(request)
    if enabled?
      ..
    else
      true
    end
  end
end

これでテストの際には enabled? を false に stub out してあげれば、constraint が無効な場合にどんな問題が起きるかをテストできるようになった。そのうえで e2e の request を投げてあげれば、Rails アプリ全体がどのように反応するかが分かる。

余談だが、簡単に stub out できる Plain Old Object スバラシイと改めて思う。

  1. Rack middleware や Web サーバでも可能だけど response の返し方を揃えておくとか、利用しているフレームワークのそばに書いてある方が関連を理解しやすいという意味では Rails の中で処理するのが望ましそう。異論は認める。 

More