Attachment付きメールでの multipart body 部分検出を修正
ActionMailer のテストアサーション assert_part と assert_no_part が、添付ファイルと共に送信されるメールの本文パートを正しく検出できなかった問題を、Mail#all_parts へ置換して解消しました。
背景
assert_part / assert_no_part が multipart/alternative に入っている本文を見落としていたことがテストの信頼性を損なっていました。Mail#parts は直下の子要素しか返さず、添付ファイルと共に生成される multipart/mixed の下にある multipart/alternative コンテナ内の text/plain や text/html パートは取得できませんでした。結果として、本文が存在しても assert_part が失敗し、逆に本文が存在しても assert_no_part が誤って成功する false positive が発生しました。テストスイート全体の安全性が低下したため、根本的な取得ロジックの見直しが必要でした。
技術的な変更
Mail#all_parts を利用して全パートをフラット化することで、ネストされた本文も検索対象に加えました。変更前は [*mail.parts, mail] が使用されており、mail.parts がトップレベルの multipart/alternative のみを返していました。修正後は [*mail.all_parts, mail] に置き換え、Mail#all_parts が再帰的にすべてのパートを列挙するため、添付ファイルと同時に送信されたテキスト・HTML 本文も正しく検出できます。
- part = [*mail.parts, mail].find { |part| mime_type.match?(part.mime_type) }
+ part = [*mail.all_parts, mail].find { |part| mime_type.match?(part.mime_type) }
同様の修正が assert_no_part にも適用され、二つのアサーションが同一ロジックで動作するようになりました。さらに、添付ファイル付きメールの正当性を確認する新しいテスト AssertMultipartWithAttachmentEmailTest が追加され、assert_part がネストされた本文を見つけられること、assert_no_part が本文があるときに失敗することを自動的に検証します。
class AssertMultipartWithAttachmentEmailTest < ActionMailer::TestCase
class AssertMultipartWithAttachmentMailer < ActionMailer::Base
def test(options)
attachments["example.png"] = { mime_type: "image/png", content: "PNGDATA" }
mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format|
format.text { render plain: options[:text] } if options.key?(:text)
format.html { render plain: options[:html] } if options.key?(:html)
end
end
end
tests AssertMultipartWithAttachmentMailer
def test_assert_part_finds_body_parts_nested_under_attachment
AssertMultipartWithAttachmentMailer.test(html: "<div><p>foo</p></div>", text: "foo bar").deliver_now
assert_part :text do |text|
assert_includes text, "foo bar"
end
assert_part :html do |html|
assert_kind_of Rails::Dom::Testing.html_document, html
assert_dom html, "div" do
assert_dom "p", "foo"
end
end
end
def test_assert_no_part_detects_body_parts_nested_under_attachment
mail = AssertMultipartWithAttachmentMailer.test(html: "<p>foo</p>", text: "foo bar")
assert_raises Minitest::Assertion, match: "expected no part matching text/html" do
assert_no_part :html, mail
end
assert_raises Minitest::Assertion, match: "expected no part matching text/plain" do
assert_no_part :text, mail
end
end
end
設計判断
既存の Mail#all_parts を流用する方針が採られました。Mail#all_parts は Rails 本体でも ActionMailer::InlinePreviewInterceptor が利用している成熟した実装であり、追加のロジックや新規 API を導入する必要がありませんでした。そのため、変更は 2 行の差し替えに留まり、assert_part 系列のシグネチャや外部使用方法は一切変わりません。結果として、下位互換性を維持しつつテストアサーションの正確性を向上させるというトレードオフが最小化されました。
まとめ
Mail#all_parts への置換により、添付ファイル付きマルチパートメールでも本文パートが正しく検出され、assert_part と assert_no_part のテスト信頼性が回復しました。変更は局所的かつ後方互換性を保ち、既存テストコードへの影響はありません。