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.
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
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.
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.
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.
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
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
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.