- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index
RECEIVING E-MAIL 406
Figure 19.1: An HTML-Format E-mail
|
And finally we’ll test this using an action method that renders the e-mail, |
|
sets the content type to text/html, and calls the mailer to deliver it. |
File 142 |
class TestController < ApplicationController |
|
def ship_order |
|
order = Order.find_by_name("Dave Thomas") |
|
email = OrderMailer.create_sent(order) |
|
email.set_content_type("text/html") |
|
OrderMailer.deliver(email) |
|
render(:text => "Thank you...") |
|
end |
|
end |
|
The resulting e-mail will look something like Figure 19.1 . |
19.2 Receiving E-mail
Action Mailer makes it easy to write Rails applications that handle incoming e-mail. Unfortunately, you also need to find a way of getting appropriate e-mails from your server environment and injecting them into the application; this requires a bit more work.
The easy part is handling an e-mail within your application. In your Action Mailer class, write an instance method called receive( ) that takes a single parameter. This parameter will be a TMail::Mail object corresponding to the incoming e-mail. You can extract fields, the body text, and/or attachments and use them in your application.
For example, a bug tracking system might accept trouble tickets by e-mail.
Prepared exclusively for Rida Al Barazi
Report erratum
RECEIVING E-MAIL 407
From each e-mail, it constructs a Ticket model object containing the basic ticket information. If the e-mail contains attachments, each will be copied into a new TicketCollateral object, which is associated with the new ticket.
File 143 |
class IncomingTicketHandler < ActionMailer::Base |
|
def receive(email) |
|
ticket = Ticket.new |
|
ticket.from_email = email.from[0] |
|
ticket.initial_report = email.body |
|
if email.has_attachments? |
email.attachments.each do |attachment| collateral = TicketCollateral.new(
:name |
=> |
attachment.original_filename, |
:body |
=> |
attachment.read) |
ticket.ticket_collaterals << collateral end
end ticket.save
end end
So now we have the problem of feeding an e-mail received by our server computer into the receive( ) instance method of our IncomingTicketHandler. This problem is actually two problems in one: first we have to arrange to intercept the reception of e-mails that meet some kind of criteria, and then we have to feed those e-mails into our application.
If you have control over the configuration of your mail server (such as a Postfix or sendmail installation on Unix-based systems), you might be able to arrange to run a script when an e-mail address to a particular mailbox or virtual host is received. Mail systems are complex, though, and we don’t have room to go into all the possible configuration permutations here. There’s a good introduction to this on the Ruby development Wiki at http://wiki.rubyonrails.com/rails/show/HowToReceiveEmailsWithActionMailer.
If you don’t have this kind of system-level access but you are on a Unix system, you could intercept e-mail at the user level by adding a rule to your .procmailrc file. We’ll see an example of this shortly.
The objective of intercepting incoming e-mail is to pass it to our application. To do this, we use the Rails runner facility. This allows us to invoke code within our application’s code base without going through the web. Instead, the runner loads up the application in a separate process and invokes code that we specify in the application.
All of the normal techniques for intercepting incoming e-mail end up running a command, passing that command the content of the e-mail as standard input. If we make the Rails runner script the command that’s invoked whenever an e-mail arrives, we can arrange to pass that e-mail into our
Prepared exclusively for Rida Al Barazi
Report erratum
TESTING E-MAIL 408
application’s e-mail handling code. For example, using procmail-based interception, we could write a rule that looks something like the example that follows. Using the arcane syntax of procmail, this rule copies any incoming e-mail whose subject line contains Bug Report through our runner script.
RUBY=/Users/dave/ruby1.8/bin/ruby
TICKET_APP_DIR=/Users/dave/Work/BS2/titles/RAILS/Book/code/mailer
HANDLER='IncomingTicketHandler.receive(STDIN.read)'
:0 c
* ^Subject:.*Bug Report.*
| cd $TICKET_APP_DIR && $RUBY script/runner $HANDLER
The receive( ) class method is available to all Action Mailer classes. It takes the e-mail text passed as a parameter, parses it into a TMail object, creates a new instance of the receiver’s class, and passes the TMail object to the receive( ) instance method in that class. This is the method we wrote on page 406. The upshot is that an e-mail received from the outside world ends up creating a Rails model object, which in turn stores a new trouble ticket in the database.
19.3 Testing E-mail
There are two levels of e-mail testing. At the unit test level you can verify that your Action Mailer classes correctly generate e-mails. At the functional level, you can test that your application sends these e-mails when you expect it to.
Unit Testing E-mail
When we used the generate script to create our order mailer, it automatically constructed a corresponding order_mailer_test.rb file in the application’s test/unit directory. If you were to look at this file, you’d see that it is fairly complex. That’s because it tries to arrange things so that you can read the expected content of e-mails from fixture files and compare this content to the e-mail produced by your mailer class. However, this is fairly fragile testing. Any time you change the template used to generate an e-mail you’ll need to change the corresponding fixture.
If exact testing of the e-mail content is important to you, then use the pregenerated test class. Create the expected content in a subdirectory of the test/fixtures directory named for the test (so our OrderMailer fixtures would be in test/fixtures/order_mailer). Use the read_fixture( ) method included in the generated code to read in a particular fixure file and compare it with the e-mail generated by your model.
Prepared exclusively for Rida Al Barazi
Report erratum
TESTING E-MAIL 409
However, I prefer something simpler. In the same way that I don’t test every byte of the web pages produced by templates, I won’t normally bother to test the entire content of a generated e-mail. Instead, I test the thing that’s likely to break: the dynamic content. This simplifies the unit test code and makes it more resilient to small changes in the template. Here’s a typical e-mail unit test.
File 151 |
require File.dirname(__FILE__) + '/../test_helper' |
|
require 'order_mailer' |
|
class OrderMailerTest < Test::Unit::TestCase |
def setup
@order = Order.new(:name =>"Dave Thomas", :email => "dave@pragprog.com") end
def test_confirm
response = OrderMailer.create_confirm(@order) assert_equal("Pragmatic Store Order Confirmation", response.subject) assert_equal("dave@pragprog.com", response.to[0]) assert_match(/Dear Dave Thomas/, response.body)
end end
The setup( ) method creates an order object for the mail sender to use. In the test method we get the mail class to create (but not to send) an e-mail, and we use assertions to verify that the dynamic content is what we expect. Note the use of assert_match( ) to validate just part of the body content.
Functional Testing of E-mail
Now that we know that e-mails can be created for orders, we’d like to make sure that our application sends the correct e-mail at the right time. This is a job for functional testing.
Let’s start by generating a new controller for our application.
depot> ruby script/generate controller Order confirm
We’ll implement the single action, confirm, which sends the confirmation e-mail for a new order.
File 141 |
class |
OrderController < ApplicationController |
|
def |
confirm |
|
order = Order.find(params[:id]) |
OrderMailer.deliver_confirm(order) redirect_to(:action => :index)
end
end
We saw how Rails constructs a stub functional test for generated controllers back in Section 12.3, Testing Controllers, on page 148. We’ll add our mail testing to this generated test.
Prepared exclusively for Rida Al Barazi
Report erratum
TESTING E-MAIL 410
Action Mailer does not deliver e-mail in the test environment. Instead, it appends each e-mail it generates to an array, ActionMailer::base.deliveries. We’ll use this to get at the e-mail generated by our controller. We’ll add a couple of lines to the generated test’s setup( ) method. One line aliases this array to the more manageable name @emails. The second clears the array at the start of each test.
File 150 |
@emails |
= ActionMailer::Base.deliveries |
|
@emails.clear |
We’ll also need a fixture holding a sample order. We’ll create a file called orders.yml in the test/fixtures directory.
File 149 |
daves_order: |
|
|
id: |
1 |
|
name: |
Dave Thomas |
|
address: |
123 Main St |
|
email: |
dave@pragprog.com |
Now we can write a test for our action. Here’s the full source for the test class.
File 150 |
require File.dirname(__FILE__) + '/../test_helper' |
|
require 'order_controller' |
|
# Re-raise errors caught by the controller. |
|
class OrderController; def rescue_action(e) raise e end; end |
|
class OrderControllerTest < Test::Unit::TestCase |
fixtures :orders
def setup |
|
|
@controller = |
OrderController.new |
|
@request |
= |
ActionController::TestRequest.new |
@response |
= |
ActionController::TestResponse.new |
@emails |
= |
ActionMailer::Base.deliveries |
@emails.clear end
def test_confirm
get(:confirm, :id => @daves_order.id) assert_redirected_to(:action => :index) assert_equal(1, @emails.size)
email = @emails.first
assert_equal("Pragmatic Store Order Confirmation", email.subject) assert_equal("dave@pragprog.com", email.to[0]) assert_match(/Dear Dave Thomas/, email.body)
end end
It uses the @emails alias to access the array of e-mails generated by Action Mailer since the test started running. Having checked that exactly one e-mail is in the list, it then validates the contents are what we expect.
We can run this test either by using the test_functional target of rake or by executing the script directly.
depot> ruby test/functional/order_controller_test.rb
Prepared exclusively for Rida Al Barazi
Report erratum