Class: RuboCop::Cop::DevDoc::Test::NoPersistenceInMailerPreviews

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/test/no_persistence_in_mailer_previews.rb

Overview

Mailer previews must not persist records. A preview renders on every GET in development, so a save!/update!/create! inside one mutates the development database as a side effect of merely VIEWING an email — and re-renders keep re-writing it.

Rationale

Previews are also a relocation target: when the HTTP-driven lint pushes mailer-body assertions into preview variants (snapshot-tested via generate_preview_tests), the temptation is to persist the state the variant needs. Don't — the mailer receives its objects in memory via .with(...), so in-memory assignment (record.attr = value) renders identically with no side effect. For sample data, use fixtures/seeded rows or unsaved new/build records.

This was a real defect: a preview called checklist.update!(note_for_assignee: ...) to stage a note variant, silently rewriting the first published checklist's note in the dev DB on every preview view. In-memory assignment renders the same body.

When in-memory assignment can't work: add a FIXTURE, don't write

Some mailers re-query their records at render time (async-safe designs re-scope ids against live access rules), so an in-memory attribute never reaches the render and persistence looks "irreducible". It still isn't — the fix is data, not code: put the state the variant needs into a fixture (or seeded row) and have the preview select it instead of creating it. Real case: a "documents shared with you" note variant used doc.update_column(:standing_note, ...) because the mailer re-queries the accessible documents; the fix was a fixture document that carries the standing note, with the two preview variants choosing their document_ids via where(standing_note: nil) vs all ids. Fixtures make the variant deterministic for snapshot-tested previews AND leave the dev DB untouched — never reach for an on-the-fly write (or an Exclude for this cop) before trying a fixture.

AllowedMethods exists for projects whose preview data setup genuinely must write (e.g. a sandboxed preview database) — prefer leaving it empty.

Examples:

# bad — viewing the preview rewrites the record
def reminder_with_note
  checklist.update!(note_for_assignee: 'Standing note')
  ChecklistMailer.with(checklist: checklist).reminder
end

# good — same rendered body, no side effect
def reminder_with_note
  checklist.note_for_assignee = 'Standing note'
  ChecklistMailer.with(checklist: checklist).reminder
end

Constant Summary collapse

MSG =
'`%<method>s` persists from a mailer preview — previews render on every GET in ' \
'development, so viewing the email mutates the dev DB. Assign in memory instead ' \
'(`record.attr = value`): the mailer receives the object via `.with(...)`, so the ' \
'rendered body is identical. For sample data, use fixtures/seeded rows or unsaved ' \
'`new`/`build` records.'.freeze
PERSISTENCE_METHODS =

delete and insert are deliberately absent: Hash#delete / Array#insert are everyday preview code and would false-positive; record-level intent is still covered by destroy/delete_all/insert_all.

%i[
  save save! create create! update update! update_column update_columns
  update_all update_attribute delete_all destroy destroy! destroy_all
  insert! insert_all insert_all! upsert upsert_all
  increment! decrement! toggle! touch touch_all
  find_or_create_by find_or_create_by! create_or_find_by create_or_find_by!
  first_or_create first_or_create!
].freeze

Instance Method Summary collapse

Instance Method Details

#on_send(node) ⇒ Object Also known as: on_csend



75
76
77
78
79
80
81
# File 'lib/rubocop/cop/dev_doc/test/no_persistence_in_mailer_previews.rb', line 75

def on_send(node)
  method = node.method_name
  return unless PERSISTENCE_METHODS.include?(method)
  return if allowed_methods.include?(method.to_s)

  add_offense(node.loc.selector, message: format(MSG, method: method))
end