Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid ActionMailer::Preview committing data to development database?

I'm using Rails 4.1.0.beta1's new Action Mailer previews and have the following code:

class EventInvitationPreview < ActionMailer::Preview
  def invitation_email
    invite = FactoryGirl.create :event_invitation, :for_match, :from_user, :to_user
    EventInvitationMailer.invitation_email(invite)
  end
end

This is all good until I actually try to preview my email and get an error saying that validation on a User object failed due to duplicate email addresses. Turns out that ActionMailer::Preview is writing to my development database.

While I could work around the validation failure or use fixtures instead of factories, is there any way to avoid ActionMailer::Preview writing to the development database, e.g. use the test database instead? Or am I just doing it wrong?

like image 257
weimeng Avatar asked Feb 22 '14 06:02

weimeng


4 Answers

Cleaner/Easier (based on other answers) and tested with Rails 7: Do not change Rails' classes but create your own. Id addition to not change the controller but the call method of ActionMailer::Preview.

# app/mailers/preview_mailer.rb

class PreviewMailer < ActionMailer::Preview
  def self.call(...)
    message = nil
    ActiveRecord::Base.transaction do
      message = super(...)
      raise ActiveRecord::Rollback
    end
    message
  end
end

# inherit from `PreviewController` for your previews

class EventInvitationPreview < PreviewController
  def invitation_email
    ...
  end
end

OLD:

You can simply use a transaction around email previews, just put this inside your lib/monkey_mailers_controller.rb (and require it):

# lib/monkey_mailers_controller.rb
class Rails::MailersController
  alias_method :preview_orig, :preview

  def preview
    ActiveRecord::Base.transaction do
      preview_orig
      raise ActiveRecord::Rollback
    end
  end
end

Then you can call .create etc. in your mailer previews but nothing will be saved to database. Works in Rails 4.2.3.

like image 141
Markus Avatar answered Nov 20 '22 05:11

Markus


TL;DR -- The original author of the ActionMailer preview feature (via the MailView gem) provides three examples of different supported approaches:

  • Pull data from existing fixtures: Account.first
  • Factory-like pattern: user = User.create! followed by user.destroy
  • Stub-like: Struct.new(:email, :name).new('[email protected]', 'Jill Smith')

~ ~ ~ ~ ~ ~ ~ ~ ~ ~

To elaborate on the challenge faced by the OP...

Another manifestation of this challenge is attempting to use FactoryGirl.build (rather than create) to generate non-persistent data. This approach is suggested by one of the top Google results for "Rails 4.1" -- http://brewhouse.io/blog/2013/12/17/whats-new-in-rails-4-1.html?brewPubStart=1 -- in the "how to use this new feature" example. This approach seems reasonable, however if you're attempting to generate a url based on that data, it leads to an error along the lines of:

ActionController::UrlGenerationError in Rails::Mailers#preview

No route matches {:action=>"edit", :controller=>"password_resets", :format=>nil, :id=>nil} missing required keys: [:id]

Using FactoryGirl.create (rather than build) would solve this problem, but as the OP notes, leads to polluting the development database.

If you check out the docs for the original MailView gem which became this Rails 4.1 feature, the original author provides a bit more clarity about his intentions in this situation. Namely, the original author provides the following three examples, all focused on data reuse / cleanup / non-persistence, rather than providing a means of using a different database:

# app/mailers/mail_preview.rb or lib/mail_preview.rb
class MailPreview < MailView
  # Pull data from existing fixtures
  def invitation
    account = Account.first
    inviter, invitee = account.users[0, 2]
    Notifier.invitation(inviter, invitee) 
  end

  # Factory-like pattern
  def welcome
    user = User.create!
    mail = Notifier.welcome(user)
    user.destroy
    mail
  end

  # Stub-like
  def forgot_password
    user = Struct.new(:email, :name).new('[email protected]', 'Jill Smith')
    mail = UserMailer.forgot_password(user)
  end
end
like image 26
DreadPirateShawn Avatar answered Nov 20 '22 06:11

DreadPirateShawn


A cleaner way to proceed is to prepend a module overriding and wrapping preview into a transaction:

module RollbackingAfterPreview
  def preview
    ActiveRecord::Base.transaction do
      super
      raise ActiveRecord::Rollback
    end
  end
end

Rails.application.config.to_prepare do
  class Rails::MailersController
    prepend RollbackingAfterPreview
  end
end
like image 7
Pierre Michard Avatar answered Nov 20 '22 05:11

Pierre Michard


For Rails 6:

@Markus' answer worked for me, except that it caused a nasty deprecation-soon-will-be-real error related to how Autoloading has changed in Rails 6:

DEPRECATION WARNING: Initialization autoloaded the constants [many constants seemingly unrelated to what I actually did]

Being able to do this is deprecated. Autoloading during initialization is going to be an error condition in future versions of Rails.

[...]

Well, that's no good!

After more searching, this blog and the docs for to_prepare helped me come up with this solution, which is just @Markus' answer wrapped in to_prepare. (And also it's in initializer/ instead of lib/.)

# /config/initializers/mailer_previews.rb
---
# Wrap previews in a transaction so they don't create objects.

Rails.application.config.to_prepare do

  class Rails::MailersController
    alias_method :preview_orig, :preview

    def preview
      ActiveRecord::Base.transaction do
        preview_orig
        raise ActiveRecord::Rollback
      end
    end
  end

end
like image 3
Grant Birchmeier Avatar answered Nov 20 '22 06:11

Grant Birchmeier