In this post I’ll do some refactoring of the project started in Part 1. I’ll be adding Rspec to do some testing, and then writing  some specs to expose some problems that exist in the previous implementation, then finally fixing those problems.

In Part 1 I covered the creation of a new project, installing and configuring Devise, and building a User model that is used in the authentication model provided by Devise. In that post I included a few dirty hacks that were there just to keep Devise from throwing errors. Unfortunately, that also removed some functionality from the app that we really need, namely the ability to validate that only one User can have any given email address. Let’s remedy that situation.

Install Rspec

First let’s install Rspec and write a failing test to illustrate the problem that we have. So, add this to the Gemfile.

group :test, :development do
  gem "rspec-rails", "~> 2.0"
end

The run :

$ bundle install
$ rails generate rspec:install

That should generate a spec/ directory and a couple of helper files. Since we’re not using ActiveRecord we need to edit spec/spec_helper.rb and remove or comment out the following lines:

config.fixture_path = "#{::Rails.root}/spec/fixtures"
...
config.use_transactional_fixtures = true

Write a failing test

Last time we included and empty method for validates_uniqueness_of, which means that Devise thinks that it can ensure that email addresses are unique, but it really can’t. So we’ll write a small test to illustrate that hole in the integration. Add a new file at spec/models/user_spec.rb and add this code:

require 'spec_helper'

# NOTE : This is really an integration test and it runs against SimpleDB
# with the credentials and prefix set up in config/intializers/aws.rb
describe User do
  describe 'validates_uniqueness_of' do
    before(:each) do
      User.create_domain
      User.all.each{|u| u.destroy }
      sleep(2) # allow a couple of seconds for SimpleDB to propagate
    end
    it "should only allow one user with the email address of 'jon@doh.com'" do
      # First create a new user and save it
      u = User.new(:email => 'jon@doh.com', :password => 'testpassword', :password_confirmation => 'testpassword')
      u.should be_valid
      u.save
      sleep(2) # we need to allow a couple of seconds for SimpleDB to propagate
      
      # Now create another user with the same email address
      # and make sure that it is not valid
      u2 = User.new(:email => 'jon@doh.com', :password => 'anotherpass', :password_confirmation => 'anotherpass')
      u2.should_not be_valid
      u2.errors[:email].should == ["has already been taken"]
    end
  end
end

Now when you run rspec spec you should see this :

Failures:

  1) User validates_uniqueness_of should only allow one user with the email address of 'jon@doh.com'
     Failure/Error: u.should_not be_valid
       expected valid? to return false, got true
     # ./spec/models/user_spec.rb:20:in `block (3 levels) in <top (required)>'

Finished in 3.16 seconds
1 example, 1 failure

So, that shows us that the uniqueness validation is definitely not working. SimpleUnique will help us fix that.

Installing SimpleUnique

We’re going to use the SimpleUnique gem to add a real version of the validates_uniqueness_of method that we stubbed out last time. See the blog post about SimpleUnique for additional info about the gem.

First, you should add this to the Gemfile.

gem "simple_unique", "~> 0.0.2"

Then you should run

$ bundle install

That’s it for installation. Easy.

Now we’re ready to start modifying the code.

Remove the dirty hacks

Open the User model in app/models/user.rb and remove these lines (just the lines listed, not the ones between).

include ActiveModel::Validations
...
def self.validates_uniqueness_of(arg1,arg2)
end

The AWS::Record::Validations modules provides the validations we need, so the ones from ActiveModel can just go away.

The validates_uniqueness_of method is now available thanks to SimpleUnique. So that means Devise will be happy since it calls something close to:

validates_uniqueness_of :email

You don’t need to add that anywhere, I’m just illustrating what Devise is already doing.

Add a less dirty hack

Devise needs to be able to save the User model without running the validations. It does this by calling

user.save(:validate => false)

Unfortunately, the #save method on AWS::Record::Model doesn’t accept any arguments, and it always validates. I have a pull request in to the aws-sdk project that allows this to happen. In the mean time you can just add this to your User model

  def save opts = {}
    if valid?(opts)
      persisted? ? update : create
      clear_changes!
      true
    else
      false
    end
  end

  def valid? opts = {}
    opts = {} if opts.nil?
    opts = {:validate => true}.merge(opts)
    run_validations if opts[:validate]
    errors.empty?
  end

Update the AWS config

Aside from missing the validates_uniqueness_of validation, the a couple of the validations from the aws-sdk gem don’t accept the :allow_blank parameter. Devise tries to pass :allow_blank => true to both validations, and they both already behave as if :allow_blank is always true. So, we’re just going to make them accept that option.

Add this to config/initializers/aws.rb

AWS::Record::FormatValidator::ACCEPTED_OPTIONS << :allow_blank
AWS::Record::LengthValidator::ACCEPTED_OPTIONS << :allow_blank

Grab a beverage

That’s it! We’re done. Now you can run the specs and you should see that things are passing:

$ rspec spec
.

Finished in 6.21 seconds
1 example, 0 failures

Now when somebody tries to sign up for the app, we’ll check to make sure that the email address has not already been used. If so we’ll show an error and ask the user to choose a different address.

You can find the source code for this example project on Github.

Next time I’ll be looking at polishing up the user interface so that we can easily navigate through the different states of the system.