Skip to content

Commit

Permalink
Basic OAuth authorization and identification flow is in and spec'ed.
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Bleigh committed Mar 17, 2009
1 parent d3ae9b1 commit 689ab05
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 13 deletions.
8 changes: 6 additions & 2 deletions app/controllers/sessions_controller.rb
Expand Up @@ -12,12 +12,16 @@ def oauth_callback
unless session[:request_token] && session[:request_token_secret]
flash[:error] = 'No authentication information was found in the session. Please try again.'
redirect_to '/' and return
end
end

@request_token = OAuth::RequestToken.new(TwitterAuth.consumer, session[:request_token], session[:request_token_secret])

@access_token = @request_token.get_access_token

render :text => @request_token.inspect
@user = User.identify_or_create_from_access_token(@access_token)

session[:user_id] = @user.id

render :nothing => true
end
end
6 changes: 6 additions & 0 deletions app/models/twitter_auth/basic_user.rb
@@ -0,0 +1,6 @@
module TwitterAuth
class BasicUser < TwitterAuth::GenericUser

end
end

52 changes: 52 additions & 0 deletions app/models/twitter_auth/generic_user.rb
@@ -0,0 +1,52 @@
module TwitterAuth
class GenericUser < ActiveRecord::Base
TWITTER_ATTRIBUTES = [
:name,
:location,
:description,
:profile_image_url,
:url,
:protected,
:profile_background_color,
:profile_sidebar_fill_color,
:profile_link_color,
:profile_sidebar_border_color,
:profile_text_color,
:friends_count,
:statuses_count,
:followers_count,
:favourites_count,
:utc_offset
]

validates_presence_of :login
validates_format_of :login, :with => /\A[a-z0-9_]+\z/
validates_length_of :login, :in => 1..15
validates_uniqueness_of :login

def self.table_name; 'users' end

def self.new_from_twitter_hash(hash)
raise ArgumentError, 'Invalid hash: must include screen_name.' unless hash.key?('screen_name')

user = User.new(:login => hash.delete('screen_name'))

TWITTER_ATTRIBUTES.each do |att|
user.send("#{att}=", hash[att.to_s]) if user.respond_to?("#{att}=")
end

user
end

def assign_twitter_attributes(hash)
TWITTER_ATTRIBUTES.each do |att|
send("#{att}=", hash[att.to_s]) if respond_to?("#{att}=")
end
end

def update_twitter_attributes(hash)
assign_twitter_attributes(hash)
save
end
end
end
24 changes: 24 additions & 0 deletions app/models/twitter_auth/oauth_user.rb
@@ -0,0 +1,24 @@
module TwitterAuth
class OauthUser < TwitterAuth::GenericUser
def self.identify_or_create_from_access_token(token, secret=nil)
raise ArgumentError, 'Must authenticate with an OAuth::AccessToken or the string access token and secret.' unless (token && secret) || token.is_a?(OAuth::AccessToken)

user_info = JSON.parse(token.get('/account/verify_credentials.json').body)

if user = User.find_by_login(user_info['screen_name'])
user.update_twitter_attributes(user_info)
user
else
User.create_from_twitter_hash_and_token(user_info, token)
end
end

def self.create_from_twitter_hash_and_token(user_info, access_token)
user = User.new_from_twitter_hash(user_info)
user.access_token = access_token.token
user.access_secret = access_token.secret
user.save
user
end
end
end
39 changes: 39 additions & 0 deletions generators/twitter_auth/templates/migration.rb
@@ -0,0 +1,39 @@
class TwitterAuth < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :login
<% if options[:strategy] == 'basic' %>
t.string :access_token
t.string :access_secret
<% else %>
t.string :crypted_password
t.string :salt
<% end %>

# This information is automatically kept
# in-sync at each login of the user. You
# may remove any/all of these columns.
t.string :name
t.string :location
t.string :description
t.string :profile_image_url
t.string :url
t.boolean :protected
t.string :profile_background_color
t.string :profile_sidebar_fill_color
t.string :profile_link_color
t.string :profile_sidebar_border_color
t.string :profile_text_color
t.integer :friends_count
t.integer :statuses_count
t.integer :followers_count
t.integer :favourites_count
t.integer :utc_offset
t.string :time_zone # 'Magic field' Rails-compatible time zone pulled from UTC Offset
end
end

def self.down
drop_table :users
end
end
1 change: 1 addition & 0 deletions rails/init.rb
@@ -1,4 +1,5 @@
# Gem Dependencies
config.gem 'oauth'

require 'json'
require 'twitter_auth'
36 changes: 25 additions & 11 deletions spec/controllers/sessions_controller_spec.rb
Expand Up @@ -50,23 +50,37 @@

describe 'with proper info' do
before do
session.should_receive(:[]).any_number_of_times.with(:request_token).and_return('faketoken')
session.should_receive(:[]).any_number_of_times.with(:request_token_secret).and_return('faketokensecret')
@user = Factory.create(:twitter_oauth_user)
request.session[:request_token] = 'faketoken'
request.session[:request_token_secret] = 'faketokensecret'
get :oauth_callback, :oauth_token => 'faketoken'
end

it 'should rebuild the request token' do
correct_token = OAuth::RequestToken.new(TwitterAuth.consumer,'faketoken','faketokensecret')

%w(token secret).each do |att|
assigns[:request_token].send(att).should == correct_token.send(att)
describe 'building the access token' do

it 'should rebuild the request token' do
correct_token = OAuth::RequestToken.new(TwitterAuth.consumer,'faketoken','faketokensecret')

%w(token secret).each do |att|
assigns[:request_token].send(att).should == correct_token.send(att)
end
end

it 'should exchange the request token for an access token' do
assigns[:access_token].should be_a(OAuth::AccessToken)
assigns[:access_token].token.should == 'fakeaccesstoken'
assigns[:access_token].secret.should == 'fakeaccesstokensecret'
end
end

describe 'identifying the user' do
it "should find the user" do
assigns[:user].should == @user
end

it 'should exchange the request token for an access token' do
assigns[:access_token].should be_a(OAuth::AccessToken)
assigns[:access_token].token.should == 'fakeaccesstoken'
assigns[:access_token].secret.should == 'fakeaccesstokensecret'
it "should assign the user id to the session" do
session[:user_id].should == @user.id
end
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions spec/fixtures/factories.rb
@@ -0,0 +1,19 @@
require 'factory_girl'

Factory.define(:twitter_oauth_user, :class => User) do |u|
u.login 'twitterman'
u.access_token 'fakeaccesstoken'
u.access_secret 'fakeaccesstokensecret'

u.name 'Twitter Man'
u.description 'Saving the world for all Twitter kind.'
end

Factory.define(:twitter_basic_user, :class => User) do |u|
u.login 'tweetkid'
u.crypted_password 'fixthislater'
u.salt 'ohsosalty'

u.name 'Tweet Kid'
u.description 'Twitter Man\'s trusty sidekick.'
end
4 changes: 4 additions & 0 deletions spec/fixtures/fakeweb.rb
Expand Up @@ -12,3 +12,7 @@
FakeWeb.register_uri(:post, 'https://twitter.com:443/oauth/request_token', :string => 'oauth_token=faketoken&oauth_token_secret=faketokensecret')

FakeWeb.register_uri(:post, 'https://twitter.com:443/oauth/access_token', :string => 'oauth_token=fakeaccesstoken&oauth_token_secret=fakeaccesstokensecret')

FakeWeb.register_uri(:get, 'https://twitter.com:443/account/verify_credentials.json', :string => "{\"profile_image_url\":\"http:\\/\\/static.twitter.com\\/images\\/default_profile_normal.png\",\"description\":\"Saving the world for all Twitter kind.\",\"utc_offset\":null,\"favourites_count\":0,\"profile_sidebar_fill_color\":\"e0ff92\",\"screen_name\":\"twitterman\",\"statuses_count\":0,\"profile_background_tile\":false,\"profile_sidebar_border_color\":\"87bc44\",\"friends_count\":2,\"url\":null,\"name\":\"Twitter Man\",\"time_zone\":null,\"protected\":false,\"profile_background_image_url\":\"http:\\/\\/static.twitter.com\\/images\\/themes\\/theme1\\/bg.gif\",\"profile_background_color\":\"9ae4e8\",\"created_at\":\"Fri Feb 06 18:10:32 +0000 2009\",\"profile_text_color\":\"000000\",\"followers_count\":2,\"location\":null,\"id\":20256865,\"profile_link_color\":\"0000ff\"}")

#FakeWeb.register_uri(:get, 'https://twitter.com:443/)
5 changes: 5 additions & 0 deletions spec/fixtures/twitter.rb
@@ -0,0 +1,5 @@
require 'net/http'

TWITTER_JSON = {
:verify_credentials => "{\"profile_image_url\":\"http:\\/\\/static.twitter.com\\/images\\/default_profile_normal.png\",\"description\":\"Saving the world for all Twitter kind.\",\"utc_offset\":null,\"favourites_count\":0,\"profile_sidebar_fill_color\":\"e0ff92\",\"screen_name\":\"twitterman\",\"statuses_count\":0,\"profile_background_tile\":false,\"profile_sidebar_border_color\":\"87bc44\",\"friends_count\":2,\"url\":null,\"name\":\"Twitter Man\",\"time_zone\":null,\"protected\":false,\"profile_background_image_url\":\"http:\\/\\/static.twitter.com\\/images\\/themes\\/theme1\\/bg.gif\",\"profile_background_color\":\"9ae4e8\",\"created_at\":\"Fri Feb 06 18:10:32 +0000 2009\",\"profile_text_color\":\"000000\",\"followers_count\":2,\"location\":null,\"id\":20256865,\"profile_link_color\":\"0000ff\"}"
}
48 changes: 48 additions & 0 deletions spec/models/twitter_auth/generic_user_spec.rb
@@ -0,0 +1,48 @@
require File.dirname(__FILE__) + '/../../spec_helper'

describe TwitterAuth::GenericUser do
should_validate_presence_of :login
should_validate_format_of :login, 'some_guy', 'awesome', 'cool_man'
should_not_validate_format_of :login, 'with-dashes', 'with.periods', 'with spaces'
should_validate_length_of :login, :in => 1..15

it 'should validate uniqueness of login' do
Factory.create(:twitter_oauth_user)
Factory.build(:twitter_oauth_user).should have_at_least(1).errors_on(:login)
end

describe '.new_from_twitter_hash' do
it 'should raise an argument error if the hash does not have a screen_name attribute' do
lambda{User.new_from_twitter_hash({})}.should raise_error(ArgumentError, 'Invalid hash: must include screen_name.')
end

it 'should return a user' do
User.new_from_twitter_hash({'screen_name' => 'twitterman'}).should be_a(User)
end

it 'should assign login to the screen_name' do
User.new_from_twitter_hash({'screen_name' => 'twitterman'}).login.should == 'twitterman'
end

it 'should assign twitter attributes that are provided' do
u = User.new_from_twitter_hash({'screen_name' => 'twitterman', 'name' => 'Twitter Man', 'description' => 'Saving the world for all Tweet kind.'})
u.name.should == 'Twitter Man'
u.description.should == 'Saving the world for all Tweet kind.'
end
end

describe '#update_twitter_attributes' do
it 'should assign values to the user' do
user = Factory.create(:twitter_oauth_user, :name => "Dude", :description => "Awesome, man.")
user.update_twitter_attributes({'name' => 'Twitter Man', 'description' => 'Works.'})
user.reload
user.name.should == 'Twitter Man'
user.description.should == 'Works.'
end

it 'should not throw an error with extraneous info' do
user = Factory.create(:twitter_oauth_user, :name => "Dude", :description => "Awesome, man.")
lambda{user.update_twitter_attributes({'name' => 'Twitter Man', 'description' => 'Works.', 'whoopsy' => 'noworks.'})}.should_not raise_error
end
end
end
55 changes: 55 additions & 0 deletions spec/models/twitter_auth/oauth_user_spec.rb
@@ -0,0 +1,55 @@
require File.dirname(__FILE__) + '/../../spec_helper'

describe TwitterAuth::OauthUser do
before do
stub_oauth!
end

describe '.identify_or_create_from_access_token' do
before do
@token = OAuth::AccessToken.new(TwitterAuth.consumer, 'faketoken', 'fakesecret')
end

it 'should accept an OAuth::AccessToken' do
lambda{ User.identify_or_create_from_access_token(@token) }.should_not raise_error(ArgumentError)
end

it 'should accept two strings' do
lambda{ User.identify_or_create_from_access_token('faketoken', 'fakesecret') }.should_not raise_error(ArgumentError)
end

it 'should not accept one string' do
lambda{ User.identify_or_create_from_access_token('faketoken') }.should raise_error(ArgumentError, 'Must authenticate with an OAuth::AccessToken or the string access token and secret.')
end

it 'should make a call to verify_credentials' do
# this is in the before, just making it explicit
User.identify_or_create_from_access_token(@token)
end

it 'should try to find the user with that login' do
User.should_receive(:find_by_login).once.with('twitterman')
User.identify_or_create_from_access_token(@token)
end

it 'should return the user if he/she exists' do
user = Factory.create(:twitter_oauth_user, :login => 'twitterman')
User.identify_or_create_from_access_token(@token).should == user
end

it 'should update the user\'s attributes based on the twitter info' do
user = Factory.create(:twitter_oauth_user, :login => 'twitterman', :name => 'Not Twitter Man')
User.identify_or_create_from_access_token(@token).name.should == 'Twitter Man'
end

it 'should create a user if one does not exist' do
lambda{User.identify_or_create_from_access_token(@token)}.should change(User, :count).by(1)
end

it 'should assign the oauth access token and secret' do
user = User.identify_or_create_from_access_token(@token)
user.access_token.should == @token.token
user.access_secret.should == @token.secret
end
end
end
36 changes: 36 additions & 0 deletions spec/schema.rb
@@ -0,0 +1,36 @@
ActiveRecord::Schema.define :version => 0 do
create_table :twitter_auth_users, :force => true do |t|
t.string :login

# OAuth fields
t.string :access_token
t.string :access_secret

# Basic fields
t.string :crypted_password
t.string :salt

# This information is automatically kept
# in-sync at each login of the user. You
# may remove any/all of these columns.
t.string :name
t.string :location
t.string :description
t.string :profile_image_url
t.string :url
t.boolean :protected
t.string :profile_background_color
t.string :profile_sidebar_fill_color
t.string :profile_link_color
t.string :profile_sidebar_border_color
t.string :profile_text_color
t.integer :friends_count
t.integer :statuses_count
t.integer :followers_count
t.integer :favourites_count
t.integer :utc_offset
t.string :time_zone # 'Magic field' Rails-compatible time zone pulled from UTC Offset
end

end

0 comments on commit 689ab05

Please sign in to comment.