Attachment付きメールでの multipart body 部分検出を修正

rails/rails

ActionMailer のテストアサーション assert_partassert_no_part が、添付ファイルと共に送信されるメールの本文パートを正しく検出できなかった問題を、Mail#all_parts へ置換して解消しました。

背景

assert_part / assert_no_part が multipart/alternative に入っている本文を見落としていたことがテストの信頼性を損なっていました。Mail#parts は直下の子要素しか返さず、添付ファイルと共に生成される multipart/mixed の下にある multipart/alternative コンテナ内の text/plaintext/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_partassert_no_part のテスト信頼性が回復しました。変更は局所的かつ後方互換性を保ち、既存テストコードへの影響はありません。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
fde5bca2

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文・背景・技術的変更・設計判断・まとめの5要素が揃っており、総論→各論→結論の流れが明確です。まとめは単なる繰り返しではなく、変更の意義を再提示しています。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きハイライトは正しい形式です。ただし PR リンクの表記が "[PR #57521](...)" となっており、仕様の "[#57521](...)" とは異なるため警告とします。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

内容は ActionMailer のテストコードに詳しいエンジニア向けで、初心者向けの過度な解説はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションはトピックセンテンスで始まり、1段落1トピック、6文以内に収まっています。段落間は空行で区切られ、可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは Diff の変更箇所と完全に一致しており、差分が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`assert_part`, `assert_no_part`, `Mail#all_parts` などの用語は PR と一致し、誤用は見られません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

根本原因、修正内容、影響範囲の説明が PR の記述と合致しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の主張はすべて PR の情報(タイトル、説明、Diff)で裏付けられており、憶測や捏造はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #57521 が正しく記載されており、他の数値や固有名詞の誤りはありません。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR の内容(添付ファイル付きメールの multipart body 検出修正)を適切に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

Rails のバージョンや LTS など外部情報は一切記載されていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の記載はなく、PR の記述と食い違う箇所もありません。