まっしろけっけ

めもてきなやーつ

rspec-mail_matcher という gem を作った

経緯

仕事で開発している minne というサービスの Rails の version を 5.1.3 から 5.1.4 にあげようと雑に bundle update rails して見たら CI が通らんぞってなっていろいろ調べて行った結果。

CI が通らなくなった箇所

mailer の spec が落ちるようになっていた。具体的な内容はこんな感じ

it '本文に url が含まれること' do
  url = %r{http://example.com/?test1=1\&test2=2}
  expect(mail).to have_body_text(/#{url}/)
end

CI の結果を見ると本文に含まれる URL が http://example.com/test1=1test2=2 と & が消えた状態になってしまっていた(& が消えてることに気づかずにあってるやん!なんで!と二時間ほどハマったのは内緒)

原因の調査その 1

have_body_text という matcher は email-spec という gem で実装されている。

で email-spec を読むと #default_part_body という method から取得した文字列をメールの本文として扱っているので #default_part_body の結果が期待するものかとりあえず調べる。

it '本文に url が含まれること' do
  url = %r{http://example.com/?test1=1\&test2=2}
  binding.pry
  expect(mail).to have_body_text(/#{url}/)
end

spec を実行して止まったところで mail.default_part_body とやると確かに & が消えた状態になっている。


なるほどでは mail.html_part.body では?ということで実行して見ると & は & となっていて消えてはいない。なるほど〜 ここ の HTMLEntities の処理がおかしいっぽいぞというのがわかった

原因の調査その 2

その 1 で htmlentities がアレっぽいというのがわかったので処理を追っていくと ここ の処理で HTML 関連の文字列置き換えをしているぞというのがわかる。

prepare(source).gsub(@entity_regexp){
  binding.pry
  if $1 && codepoint = @map[$1]
    codepoint.chr(Encoding::UTF_8)
  elsif $2
    $2.to_i(10).chr(Encoding::UTF_8)
  elsif $3
    $3.to_i(16).chr(Encoding::UTF_8)
  else
    $&
  end
}

上記のように pry を追記して spec を実行して止まったところでそれぞれの違いを見て見た。

# 5.1.3
pry> prepare(source)
=> "......"
# 5.1.4
pry> prepare(source)
=> "......"

prepare(source) は差分なし

# 5.1.3
pry> $1
=> "amp"
#5.1.4
pry> $1
=> nil

なるほど〜、ということは String#gsub が ActiveSupport あたりで override されてるのか?という考えにたどり着く。
上記の pry 時に caller を実行すると activesupport/lib/active_support/core_ext/string/output_safety.rb が 5.1.4 だと追加されていることがわかる。
確証を得たいので下記で確認。

# 5.1.3
pry> prepare(source).class
=> String
pry> prepare(source).class.ancestors
=> [ActiveSupport::ToJsonWithActiveSupportEncoder,
 String,


#5.1.4
pry> prepare(source).class
=> String
pry> prepare(source).class.ancestors
=> [ActiveSupport::SafeBuffer,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 String,.....

なるほどね。ということで ActiveSupport::SafeBuffe が追加されたことで htmlentities の処理がうまく動かなくなってしまったことが原因だということが判明。

解決方法を考える

email-spec or htmlentities に PR 投げるか〜というのを最初考えてリポジトリ見たら 3 years ago とかの文字が並んでてなるほど...という感じになった。
それなら自分で作るか〜という気持ちになって今回作ったのでした。

email-spec との差

minne の spec を書き換えるのはダルいと思ったので基本的に matcher の method 名は email-spec に合わせる方向で設計したのだけれど、email-spec の default_part_body に関しては納得いかなくて html_part, text_part の順で取得されるのでこの仕様を知らないと have_body_text で text mail の本文をチェックしているはずなのに html mail の本文チェックしてて同じ記述があるから CI 通った(逆も然り)ということが起きそうなのがダメっぽいな〜という感じになった。

そこで have_body_text で text mail の body を have_body_html で html mail の body をチェックする matcher を定義して実装した。
しかし、現状の v0.1.2 だと 3,4 時間で雑に作ったので multipart? が false の際は雑に #body を使ってしまっているので次の version ではそこら辺の metcher を用意するか have_body_ でそれぞれチェックするかの処理を実装する予定

github.com

最後に

5.1.3 と 5.1.4 のソースの差分 ActiveSupport っぽいなというのはわかっていたけれど、どこがどう問題になっているのか調べて見るか〜と言った感じ。
だらだらと調査内容から書いたのはこのブログを PR に貼り付けて説明いらずにするためです。