Tuesday, 17 June 2008

assert email sent_to recipients

We have notifications sent out to users when their orders are finished processing. Recently we added "user groups" and so orders (which used to belong to a single user) now belong to groups of users. Which brought about the possibilty that a user could be removed from the group (and therefore could no longer see the order).

It's possible that this could happen between the time they submit their order and the time that the order has finished processing. We determined that, if the user no longer belongs to the group, they shouldn't get the notification - it should go to the next person in the group.

So our original notification code had something along the lines of:

  # Prepares an email that will notify the user that his calculation is complete
  def report_complete_notification(report)
    setup_common_values(report.user)
    @subject    += 'Your report is complete'
    body[:report] = report
  end

  # Details common to all emails from us
  def setup_common_values(user)
    recipients  user.email
    from        DEFAULT_FROM_EMAIL
    subject     "Our Funky Service - "
    sent_on     Time.now.utc
    body        :user => user
  end

So first we needed to update the notifier to call an appropriate 'recipient' if needed:

    setup_common_values(report.recipient)

Using the following recipient code in the model object:

  def recipient
    # always belongs to the original owner if no group is specified.
    return self.user unless self.group
    # use same recipient if creator still belongs to the request's group
    return self.user if self.group.users.include?(self.user)
    # otherwise grab the first person out of the group
    return self.group.users.first unless self.group.users.blank?
    # no users left - who do we notify?
    raise "HELP! The report: #{self.id} belongs to group: #{self.group.id}, but the group has no users so I don't know who should receive my notifications."
  end

As you can see, we aren't yet sure where to escalate notificaton if the group is completely empty. We have several options, but we're still waiting on the customer to get back to us and tell us what they want. The most likely option is to send an alternative notification to our admins.

How to test?

So now we need to test whether the appropriate person gets the notification. To begin with, testing whether an email went to a recipient, or set of them, isn't a pretty assertion, so I've abstracted it out into its own assertion (below). This also allows me to extend it if we need to check CC: and BCC: lists later. For now we don't use them, so "to" is good enough.

  # assert that the given recipients appeared in the recipient list of
  # the given email. If "want_match" is false - assert that *none* of the
  # given recipients were in the recipient list
  def assert_recipients(eml, recipients, want_match = true)
    recipients = [recips] unless recipients.respond_to?('[]') # arrayify if single item
    recipients.each do |recip|
      match = eml.to.any? {|r| r =~ /#{recip}/}
      match = !match unless want_match
      msg = want_match ? "email should have been sent to recipient #{recip}, but instead went to: #{eml.to}" : "email should not have been sent to recipient #{recip}"
      assert match, msg 
    end
  end
  # convenience function for asserting an email had recipients
  def assert_sent_to(eml, recips)
    assert_recipients eml, recips
  end
  # convenience function for asserting an email did not have recipients
  def assert_not_sent_to(eml, recips)
    assert_recipients eml, recips, false
  end

Now we can do various tests such as:

  # note - assume mailer.deliveries is instantiated as @emails in setup.
  def test_should_send_completion_notification
    order = orders(:completed)
    
    UserNotifier.deliver_completion_notification(order)
   
    assert_equal 1, @emails.size, "should have received one email to confirm completion"
    assert_sent_to @emails[0], order.user.email
  end
  def test_should_send_completion_notification_to_new_recipient_if_user_moved
    order = orders(:completed_for_moved_user)
    # sanity check
    assert_not_equal order.user, order.recipient, "user has moved group, but they are still showing up as the recipient"
    
    UserNotifier.deliver_completion_notification(order)
   
    assert_equal 1, @emails.size, "should have received one email to confirm completion"
    assert_sent_to @emails[0], order.recipient.email
    assert_not_sent_to @emails[0], order.user.email
  end

Monday, 9 June 2008

Rails Gotchas: Missing host error in unit tests

I finally got around to adding nice "and if you'd like to see your newly-created report, go to the URL: BLAH" links into my notification emails... a tiny nice-to-have that wasn't high on the priority list.

Suddenly my unit tests started spewing errors of the form:
ActionView::TemplateError: Missing host to link to! Please provide :host parameter or set default_url_options[:host]

The doco on default_url_options says you can add the "host" param on ActionController::Base, but no amount of monkey-patching in config/environment.rb seemed to do the trick.

Googling found a link talking about re-writing your URLs and it mentioned the problem is mainly seen in unit testing, and gave a couple of lines for a fix (as below)... but didn't say where to put them.

  include ActionController::UrlWriter
  default_url_options[:host] = 'localhost'

Experimentation showed that if you put them in your Notifer model (eg user_notifier.rb) they work just fine. Otherwise it just doesn't get into ActionMailer. :P