Skip to content

Instantly share code, notes, and snippets.

@phillipkoebbe
Created August 20, 2012 19:45
Show Gist options
  • Save phillipkoebbe/3407135 to your computer and use it in GitHub Desktop.
Save phillipkoebbe/3407135 to your computer and use it in GitHub Desktop.
Small Rails code sample
class User < ActiveRecord::Base
UNKNOWN_EMAIL = "That email address isn't registered."
WRONG_PASSWORD = 'Password is incorrect.'
LOGIN_SUCCESSFUL = "You're in!"
attr_accessor :password
attr_accessible :email, :password, :country_id
belongs_to :country
has_many :budgets, :dependent => :destroy
validates :email, :presence => true, :uniqueness => true, :email => true
validates :password, :presence => true, :on => :create
before_save :ensure_password_digest, :ensure_uuid
def set_country!(ip)
return if self.country_id
geo_result = GeoIp.geolocation(ip, :precision => :country)
return unless geo_result
country = Country.find_by_code geo_result[:country_code] || ''
return unless country
self.update_attribute :country_id, country.id
end
def User.authenticate(email, password)
user = find_by_email(email)
return nil, User::UNKNOWN_EMAIL unless user
# have a user, does the password match?
return (BCrypt::Password.new(user.password_digest) == password) ? [user, User::LOGIN_SUCCESSFUL] : [nil, User::WRONG_PASSWORD]
end
protected
def ensure_password_digest
# since password is implemented using attr_accessor, password.present? should
# be true only when it has been changed through user interaction.
self.password_digest = BCrypt::Password.create(password) if password.present?
end
def ensure_uuid
# since password is implemented using attr_accessor, password.present? should
# be true only when it has been changed through user interaction, and we want
# to change the uuid whenever the password changes
self.uuid = UUIDTools::UUID.random_create.to_s if self.uuid.nil? || password.present?
# make sure uuid is not already used
while User.exists?(:uuid => self.uuid)
self.uuid = UUIDTools::UUID.random_create.to_s
end
end
end
require 'model_helper'
describe User do
it 'should belong to country' do
User.new.should respond_to(:country)
end
it 'should have many budgets' do
User.new.should respond_to(:budgets)
end
it 'should destroy budgets when self is destroyed' do
budget = Factory.create :budget
budget.user.destroy
Budget.count.should == 0
end
describe 'email' do
it 'should be required' do
user = Factory.build :user, :email => nil
user.should have_at_least(1).error_on(:email)
end
it 'should be unique' do
user_1 = Factory.create :user
user_2 = Factory.build :user, :email => user_1.email
user_2.should have_at_least(1).error_on(:email)
end
it 'should accept valid emails' do
valid_emails = [
'user@example.com',
'another.user@example.com',
'a.user.with.a.long.name@dept.example.com',
'user+something@example.com',
'user+something.else@example.com',
'user+something_else@example.com',
'user_something@example.com'
]
user = Factory.build :user
valid_emails.each do |email|
user.email = email
user.should have(:no).errors_on(:email)
end
end
it 'should not accept invalid emails' do
invalid_emails = [
'user',
'example.com',
'@example.com',
'user.@example.com',
'user..something@example.com',
'user@something@else@example.com'
]
user = Factory.build :user
invalid_emails.each do |email|
user.email = email
user.should have_at_least(1).error_on(:email)
end
end
end # describe 'email'
describe '#password' do
it 'should be required on create' do
user = Factory.build :user, :password => nil
user.should have_at_least(1).error_on(:password)
end
it 'should not be required on update' do
# password is implemented as an attr_accessor, so when a user object
# is loaded from the database, user.password should be nil. if something
# else is changed, user.password should still be nil and should not cause
# a validation error
email = 'user@example.com'
Factory.create :user, :email => email
user = User.find_by_email email
# sanity checks
user.password.should be_nil
user.password_digest.should_not be_nil
user.email = 'user@example.net'
user.should have(:no).errors_on(:password)
end
end # describe '#password'
describe 'password_digest' do
it 'should be generated automatically' do
email = 'user@example.com'
Factory.create :user, :email => email
user = User.find_by_email email
user.password_digest.should_not be_nil
end
it 'should be changed when password is changed' do
user = Factory.create :user
old_password_digest = user.password_digest
user.update_attributes :password => user.password.reverse
user.password_digest.to_s.should_not == old_password_digest.to_s
end
it 'should not be changed if new password is blank' do
user = Factory.create :user
old_password_digest = user.password_digest
user.update_attributes :password => nil
user.password_digest.to_s.should == old_password_digest.to_s
end
it 'should not be changed if new password is empty' do
user = Factory.create :user
old_password_digest = user.password_digest
user.update_attributes :password => ' '
user.password_digest.to_s.should == old_password_digest.to_s
end
end # describe 'password_digest'
describe 'uuid' do
it 'should be generated automatically' do
email = 'user@example.com'
Factory.create :user, :email => email
user = User.find_by_email email
user.uuid.should_not be_nil
end
it 'should be changed when password is changed' do
user = Factory.create :user
old_uuid = user.uuid
user.update_attributes :password => user.password.reverse
user.uuid.should_not == old_uuid
end
it 'should be unique' do
# how to test that a randomly generated value is unique? since the user is not supplying the value,
# we cannot do something like:
#
# it 'should be unique' do
# user_1 = Factory.create :user
# user_2 = Factory.build :user, :uuid => user_1.uuid
# user_2.should have_at_least(1).error_on(:uuid)
# end
user_1 = Factory.create :user
Factory.create :user
# look up user so attributes will not be in memory from object creation
user_2 = User.last
user_2.uuid = user_1.uuid
user_2.save
user_2.uuid.should_not == user_1.uuid
end
end # describe 'uuid'
describe 'authentication' do
context 'with valid credentials' do
before :each do
email = 'user@example.com'
password = 'user.at.example.com'
@saved_user = Factory.create :user, :email => email, :password => password
@authenticated_user, @message = User.authenticate(email, password)
end
it 'should return the correct user' do
@authenticated_user.id.should == @saved_user.id
end
it 'should return the LOGIN_SUCCESSFUL message' do
@message.should == User::LOGIN_SUCCESSFUL
end
end # context 'with valid credentials'
context 'with unknown email' do
before :each do
email = 'right@email.com'
password = 'right.password'
Factory.create :user, :email => email, :password => password
@authenticated_user, @message = User.authenticate('wrong@email.com', password)
end
it 'should return a nil user' do
@authenticated_user.should be_nil
end
it 'should return the UNKNOWN_EMAIL message' do
@message.should == User::UNKNOWN_EMAIL
end
end # context 'unknown email'
context 'with wrong password' do
before :each do
email = 'right@email.com'
password = 'right.password'
Factory.create :user, :email => email, :password => password
@authenticated_user, @message = User.authenticate(email, 'wrong.password')
end
it 'should return a nil user' do
@authenticated_user.should be_nil
end
it 'should return the WRONG_PASSWORD message' do
@message.should == User::WRONG_PASSWORD
end
end # context 'with wrong password'
end # describe 'authentication'
describe '#set_country!' do
context 'when country is already set' do
before(:each) { @user = Factory.create :user }
it 'should do nothing' do
GeoIp.should_not_receive(:geolocation)
@user.set_country! '127.0.0.1'
end
end
context 'when the country is not set' do
before(:each) { @user = Factory.create :user, :country => nil }
context 'and the IP is resolved' do
before(:each) do
GeoIp.stub(:geolocation).and_return({:country_code => 'US'})
country = mock_model Country
Country.stub(:find_by_code).and_return(country)
end
it 'should set the country' do
Country.should_receive :find_by_code
@user.set_country! '127.0.0.1'
@user.country_id.should_not be_nil
end
end # context 'when the IP is resolved'
context 'and the IP is not resolved' do
before(:each) do
GeoIp.stub(:geolocation).and_return({:country_code => '-'})
Country.stub(:find_by_code).and_return(nil)
end
it 'should not set the country' do
Country.should_receive :find_by_code
@user.set_country! '192.168.1.1'
@user.country_id.should be_nil
end
end # context 'when the IP is not resolved'
context 'and the resolution attempt times out' do
before(:each) do
GeoIp.stub(:geolocation).and_return(nil)
end
it 'should not set the country' do
Country.should_not_receive :find_by_code
@user.set_country! '127.0.0.1'
@user.country_id.should be_nil
end
end # context 'when the resolution attempt times out'
end # context 'when the country is not set'
end # describe '#set_country!'
end # describe User
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment