Class: RuboCop::Cop::DevDoc::Test::NoPersistenceInMailerPreviews
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Test::NoPersistenceInMailerPreviews
- 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.
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 =
deleteandinsertare 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
- #on_send(node) ⇒ Object (also: #on_csend)
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 |