stocktwits 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +20 -0
  4. data/README +0 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +63 -0
  7. data/VERSION +1 -0
  8. data/app/controllers/sessions_controller.rb +68 -0
  9. data/app/models/stocktwits/basic_user.rb +64 -0
  10. data/app/models/stocktwits/generic_user.rb +123 -0
  11. data/app/models/stocktwits/oauth_user.rb +46 -0
  12. data/app/models/stocktwits/plain_user.rb +53 -0
  13. data/app/views/sessions/_login.html.erb +18 -0
  14. data/app/views/sessions/new.html.erb +5 -0
  15. data/config/routes.rb +6 -0
  16. data/generators/stocktwits/USAGE +12 -0
  17. data/generators/stocktwits/stocktwits_generator.rb +42 -0
  18. data/generators/stocktwits/templates/migration.rb +20 -0
  19. data/generators/stocktwits/templates/stocktwits.yml +66 -0
  20. data/generators/stocktwits/templates/user.rb +5 -0
  21. data/lib/stocktwits.rb +103 -0
  22. data/lib/stocktwits/controller_extensions.rb +72 -0
  23. data/lib/stocktwits/cryptify.rb +30 -0
  24. data/lib/stocktwits/dispatcher/basic.rb +46 -0
  25. data/lib/stocktwits/dispatcher/oauth.rb +26 -0
  26. data/lib/stocktwits/dispatcher/plain.rb +44 -0
  27. data/lib/stocktwits/dispatcher/shared.rb +42 -0
  28. data/rails/init.rb +6 -0
  29. data/spec/application.rb +1 -0
  30. data/spec/controllers/controller_extensions_spec.rb +162 -0
  31. data/spec/controllers/sessions_controller_spec.rb +221 -0
  32. data/spec/debug.log +397 -0
  33. data/spec/fixtures/config/twitter_auth.yml +17 -0
  34. data/spec/fixtures/factories.rb +28 -0
  35. data/spec/fixtures/fakeweb.rb +18 -0
  36. data/spec/fixtures/stocktwits.rb +5 -0
  37. data/spec/models/stocktwits/basic_user_spec.rb +138 -0
  38. data/spec/models/stocktwits/generic_user_spec.rb +146 -0
  39. data/spec/models/stocktwits/oauth_user_spec.rb +100 -0
  40. data/spec/schema.rb +25 -0
  41. data/spec/spec.opts +1 -0
  42. data/spec/spec_helper.rb +107 -0
  43. data/spec/stocktwits/cryptify_spec.rb +51 -0
  44. data/spec/stocktwits/dispatcher/basic_spec.rb +83 -0
  45. data/spec/stocktwits/dispatcher/oauth_spec.rb +72 -0
  46. data/spec/stocktwits/dispatcher/shared_spec.rb +26 -0
  47. data/spec/stocktwits_spec.rb +173 -0
  48. data/stocktwits.gemspec +116 -0
  49. metadata +158 -0
@@ -0,0 +1,46 @@
1
+ require 'net/http'
2
+
3
+ module Stocktwits
4
+ module Dispatcher
5
+ class Basic
6
+ include Stocktwits::Dispatcher::Shared
7
+
8
+ attr_accessor :user
9
+
10
+ def initialize(user)
11
+ raise Stocktwits::Error, 'Dispatcher must be initialized with a User.' unless user.is_a?(Stocktwits::BasicUser)
12
+ self.user = user
13
+ end
14
+
15
+ def request(http_method, path, body=nil, *arguments)
16
+ path = Stocktwits.path_prefix + path
17
+ path = append_extension_to(path)
18
+
19
+ response = Stocktwits.net.start{ |http|
20
+ req = "Net::HTTP::#{http_method.to_s.capitalize}".constantize.new(path, *arguments)
21
+ req.basic_auth user.login, user.password
22
+ req.set_form_data(body) unless body.nil?
23
+ http.request(req)
24
+ }
25
+
26
+ handle_response(response)
27
+ end
28
+
29
+ def get(path, *arguments)
30
+ request(:get, path, *arguments)
31
+ end
32
+
33
+ def post(path, body='', *arguments)
34
+ request(:post, path, body, *arguments)
35
+ end
36
+
37
+ def put(path, body='', *arguments)
38
+ request(:put, path, body, *arguments)
39
+ end
40
+
41
+ def delete(path, *arguments)
42
+ request(:delete, path, *arguments)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ require 'oauth'
2
+
3
+ module Stocktwits
4
+ module Dispatcher
5
+ class Oauth < OAuth::AccessToken
6
+ include Stocktwits::Dispatcher::Shared
7
+
8
+ attr_accessor :user
9
+
10
+ def initialize(user)
11
+ raise Stocktwits::Error, 'Dispatcher must be initialized with a User.' unless user.is_a?(Stocktwits::OauthUser)
12
+ self.user = user
13
+ super(Stocktwits.consumer, user.access_token, user.access_secret)
14
+ end
15
+
16
+ def request(http_method, path, *arguments)
17
+ path = Stocktwits.path_prefix + path
18
+ path = append_extension_to(path)
19
+
20
+ response = super
21
+
22
+ handle_response(response)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ require 'net/http'
2
+
3
+ module Stocktwits
4
+ module Dispatcher
5
+ class Plain
6
+ include Stocktwits::Dispatcher::Shared
7
+
8
+ attr_accessor :user
9
+
10
+ def initialize(user)
11
+ raise Stocktwits::Error, 'Dispatcher must be initialized with a User.' unless user.is_a?(Stocktwits::PlainUser)
12
+ self.user = user
13
+ end
14
+
15
+ def request(http_method, path, body=nil, *arguments)
16
+ path = Stocktwits.path_prefix + path
17
+ path = append_extension_to(path)
18
+ response = Stocktwits.net.start{ |http|
19
+ req = "Net::HTTP::#{http_method.to_s.capitalize}".constantize.new(path, *arguments)
20
+ req.set_form_data(body) unless body.nil?
21
+ http.request(req)
22
+ }
23
+
24
+ handle_response(response)
25
+ end
26
+
27
+ def get(path, *arguments)
28
+ request(:get, path, *arguments)
29
+ end
30
+
31
+ def post(path, body='', *arguments)
32
+ request(:post, path, body, *arguments)
33
+ end
34
+
35
+ def put(path, body='', *arguments)
36
+ request(:put, path, body, *arguments)
37
+ end
38
+
39
+ def delete(path, *arguments)
40
+ request(:delete, path, *arguments)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ module Stocktwits
2
+ module Dispatcher
3
+ module Shared
4
+
5
+ # def post!(status)
6
+ # self.post('/statuses/update.json', :status => status)
7
+ # end
8
+
9
+ def append_extension_to(path)
10
+ path, query_string = *(path.split("?"))
11
+ path << '.json' unless path.match(/\.(:?xml|json)\z/i)
12
+ "#{path}#{"?#{query_string}" if query_string}"
13
+ end
14
+
15
+ def handle_response(response)
16
+ case response
17
+ when Net::HTTPOK
18
+ begin
19
+ JSON.parse(response.body)
20
+ rescue JSON::ParserError
21
+ response.body
22
+ end
23
+ when Net::HTTPUnauthorized
24
+ raise Stocktwits::Dispatcher::Unauthorized, 'The credentials provided did not authorize the user.'
25
+ else
26
+ message = begin
27
+ JSON.parse(response.body)['error']
28
+ rescue JSON::ParserError
29
+ if match = response.body.match(/<error>(.*)<\/error>/)
30
+ match[1]
31
+ else
32
+ raise response.body
33
+ 'An error occurred processing your Stocktwits request. '
34
+ end
35
+ end
36
+
37
+ raise Stocktwits::Dispatcher::Error, message
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'json'
2
+ require 'oauth'
3
+ require 'ezcrypto'
4
+ require 'stocktwits'
5
+
6
+ RAILS_DEFAULT_LOGGER.info "** StockTwits initalized properly."
@@ -0,0 +1 @@
1
+ class ApplicationController < ActionController::Base; end
@@ -0,0 +1,162 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ ActionController::Routing::Routes.draw do |map|
4
+ map.connect ':controller/:action/:id'
5
+ end
6
+
7
+ class StocktwitsTestController < ApplicationController
8
+ before_filter :login_required, :only => [:login_required_action]
9
+
10
+ def login_required_action
11
+ render :text => "You are logged in!"
12
+ end
13
+
14
+ def fail_auth
15
+ authentication_failed('Auth FAIL.')
16
+ end
17
+
18
+ def pass_auth
19
+ if params[:message]
20
+ authentication_succeeded(params[:message])
21
+ else
22
+ authentication_succeeded
23
+ end
24
+ end
25
+
26
+ def access_denied_action
27
+ access_denied
28
+ end
29
+
30
+ def redirect_back_action
31
+ redirect_back_or_default(params[:to] || '/')
32
+ end
33
+
34
+ def logout_keeping_session_action
35
+ logout_keeping_session!
36
+ redirect_back_or_default('/')
37
+ end
38
+
39
+ def current_user_action
40
+ @user = current_user
41
+ render :nothing => true
42
+ end
43
+ end
44
+
45
+ describe StocktwitsTestController do
46
+ before do
47
+ controller.stub!(:cookies).and_return({})
48
+ end
49
+
50
+ %w(authentication_failed authentication_succeeded current_user authorized? login_required access_denied store_location redirect_back_or_default logout_keeping_session!).each do |m|
51
+ it "should respond to the extension method '#{m}'" do
52
+ controller.should respond_to(m)
53
+ end
54
+ end
55
+
56
+ describe "#authentication_failed" do
57
+ it 'should set the flash[:error] to the message passed in' do
58
+ get :fail_auth
59
+ flash[:error].should == 'Auth FAIL.'
60
+ end
61
+
62
+ it 'should redirect to the root' do
63
+ get :fail_auth
64
+ should redirect_to('/')
65
+ end
66
+ end
67
+
68
+ describe "#authentication_succeeded" do
69
+ it 'should set the flash[:notice] to a default success message' do
70
+ get :pass_auth
71
+ flash[:notice].should == 'You have logged in successfully.'
72
+ end
73
+
74
+ it 'should be able ot receive a custom message' do
75
+ get :pass_auth, :message => 'Eat at Joes.'
76
+ flash[:notice].should == 'Eat at Joes.'
77
+ end
78
+ end
79
+
80
+ describe '#current_user' do
81
+ it 'should find the user based on the session user_id' do
82
+ user = Factory.create(:stocktwits_oauth_user)
83
+ request.session[:user_id] = user.id
84
+ get(:current_user_action)
85
+ assigns[:user].should == user
86
+ end
87
+
88
+ it 'should log the user in through a cookie' do
89
+ user = Factory(:stocktwits_oauth_user, :remember_token => 'abc', :remember_token_expires_at => (Time.now + 10.days))
90
+ controller.stub!(:cookies).and_return({:remember_token => 'abc'})
91
+ get :current_user_action
92
+ assigns[:user].should == user
93
+ end
94
+
95
+ it 'should return nil if there is no user matching that id' do
96
+ request.session[:user_id] = 2345
97
+ get :current_user_action
98
+ assigns[:user].should be_nil
99
+ end
100
+ end
101
+
102
+ describe "#authorized?" do
103
+ it 'should be true if there is a current_user' do
104
+ user = Factory.create(:stocktwits_oauth_user)
105
+ controller.stub!(:current_user).and_return(user)
106
+ controller.send(:authorized?).should be_true
107
+ end
108
+
109
+ it 'should be false if there is not current_user' do
110
+ controller.stub!(:current_user).and_return(nil)
111
+ controller.send(:authorized?).should be_false
112
+ end
113
+ end
114
+
115
+ describe '#access_denied' do
116
+ it 'should redirect to the login path' do
117
+ get :access_denied_action
118
+ should redirect_to(login_path)
119
+ end
120
+
121
+ it 'should store the location first' do
122
+ controller.should_receive(:store_location).once
123
+ get :access_denied_action
124
+ end
125
+ end
126
+
127
+ describe '#redirect_back_or_default' do
128
+ it 'should redirect if there is a session[:return_to]' do
129
+ request.session[:return_to] = '/'
130
+ get :redirect_back_action, :to => '/notroot'
131
+ should redirect_to('/')
132
+ end
133
+
134
+ it 'should redirect to the default provided otherwise' do
135
+ get :redirect_back_action, :to => '/someurl'
136
+ should redirect_to('/someurl')
137
+ end
138
+ end
139
+
140
+ describe 'logout_keeping_session!' do
141
+ before do
142
+ @user = Factory.create(:stocktwits_oauth_user)
143
+ request.session[:user_id] = @user.id
144
+ end
145
+
146
+ it 'should unset session[:user_id]' do
147
+ get :logout_keeping_session_action
148
+ request.session[:user_id].should be_nil
149
+ end
150
+
151
+ it 'should unset current_user' do
152
+ controller.send(:current_user).should == @user
153
+ get :logout_keeping_session_action
154
+ controller.send(:current_user).should be_false
155
+ end
156
+
157
+ it 'should unset the cookie' do
158
+ controller.send(:cookies).should_receive(:delete).with(:remember_token)
159
+ get :logout_keeping_session_action
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,221 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe SessionsController do
4
+ integrate_views
5
+
6
+ describe 'routes' do
7
+ it 'should route /session/new to SessionsController#new' do
8
+ params_from(:get, '/session/new').should == {:controller => 'sessions', :action => 'new'}
9
+ end
10
+
11
+ it 'should route /login to SessionsController#new' do
12
+ params_from(:get, '/login').should == {:controller => 'sessions', :action => 'new'}
13
+ end
14
+
15
+ it 'should route /logout to SessionsController#destroy' do
16
+ params_from(:get, '/logout').should == {:controller => 'sessions', :action => 'destroy'}
17
+ end
18
+
19
+ it 'should route DELETE /session to SessionsController#destroy' do
20
+ params_from(:delete, '/session').should == {:controller => 'sessions', :action => 'destroy'}
21
+ end
22
+
23
+ it 'should route /oauth_callback to SessionsController#oauth_callback' do
24
+ params_from(:get, '/oauth_callback').should == {:controller => 'sessions', :action => 'oauth_callback'}
25
+ end
26
+
27
+ it 'should route POST /session to SessionsController#create' do
28
+ params_from(:post, '/session').should == {:controller => 'sessions', :action => 'create'}
29
+ end
30
+ end
31
+
32
+ describe 'with OAuth strategy' do
33
+ before do
34
+ stub_oauth!
35
+ end
36
+
37
+ describe '#new' do
38
+ it 'should retrieve a request token' do
39
+ get :new
40
+ assigns[:request_token].token.should == 'faketoken'
41
+ assigns[:request_token].secret.should == 'faketokensecret'
42
+ end
43
+
44
+ it 'should set session variables for the request token' do
45
+ get :new
46
+ session[:request_token].should == 'faketoken'
47
+ session[:request_token_secret].should == 'faketokensecret'
48
+ end
49
+
50
+ it 'should redirect to the oauth authorization url' do
51
+ get :new
52
+ response.should redirect_to('https://twitter.com/oauth/authorize?oauth_token=faketoken')
53
+ end
54
+
55
+ it 'should redirect to the oauth_callback if one is specified' do
56
+ Stocktwits.stub!(:oauth_callback).and_return('http://localhost:3000/development')
57
+ Stocktwits.stub!(:oauth_callback?).and_return(true)
58
+
59
+ get :new
60
+ response.should redirect_to('https://twitter.com/oauth/authorize?oauth_token=faketoken&oauth_callback=' + CGI.escape(Stocktwits.oauth_callback))
61
+ end
62
+ end
63
+
64
+ describe '#oauth_callback' do
65
+ describe 'with no session info' do
66
+ it 'should set the flash[:error]' do
67
+ get :oauth_callback, :oauth_token => 'faketoken'
68
+ flash[:error].should == 'No authentication information was found in the session. Please try again.'
69
+ end
70
+
71
+ it 'should redirect to "/" by default' do
72
+ get :oauth_callback, :oauth_token => 'faketoken'
73
+ response.should redirect_to('/')
74
+ end
75
+
76
+ it 'should call authentication_failed' do
77
+ controller.should_receive(:authentication_failed).any_number_of_times
78
+ get :oauth_callback, :oauth_token => 'faketoken'
79
+ end
80
+ end
81
+
82
+ describe 'with proper info' do
83
+ before do
84
+ @user = Factory.create(:twitter_oauth_user, :twitter_id => '123')
85
+ @time = Time.now
86
+ @remember_token = ActiveSupport::SecureRandom.hex(10)
87
+
88
+ Time.stub!(:now).and_return(@time)
89
+ ActiveSupport::SecureRandom.stub!(:hex).and_return(@remember_token)
90
+
91
+ request.session[:request_token] = 'faketoken'
92
+ request.session[:request_token_secret] = 'faketokensecret'
93
+ get :oauth_callback, :oauth_token => 'faketoken'
94
+ end
95
+
96
+ describe 'building the access token' do
97
+ it 'should rebuild the request token' do
98
+ correct_token = OAuth::RequestToken.new(Stocktwits.consumer,'faketoken','faketokensecret')
99
+
100
+ %w(token secret).each do |att|
101
+ assigns[:request_token].send(att).should == correct_token.send(att)
102
+ end
103
+ end
104
+
105
+ it 'should exchange the request token for an access token' do
106
+ assigns[:access_token].should be_a(OAuth::AccessToken)
107
+ assigns[:access_token].token.should == 'fakeaccesstoken'
108
+ assigns[:access_token].secret.should == 'fakeaccesstokensecret'
109
+ end
110
+
111
+ it 'should wipe the request token after exchange' do
112
+ session[:request_token].should be_nil
113
+ session[:request_token_secret].should be_nil
114
+ end
115
+ end
116
+
117
+ describe 'identifying the user' do
118
+ it "should find the user" do
119
+ assigns[:user].should == @user
120
+ end
121
+
122
+ it "should assign the user id to the session" do
123
+ session[:user_id].should == @user.id
124
+ end
125
+
126
+ it "should call remember me" do
127
+ @user.reload
128
+ @user.remember_token.should == @remember_token
129
+ end
130
+
131
+ it "should set a cookie" do
132
+ cookies[:remember_token].should == @remember_token
133
+ end
134
+ end
135
+
136
+ describe "when OAuth doesn't work" do
137
+ before do
138
+ request.session[:request_token] = 'faketoken'
139
+ request.session[:request_token_secret] = 'faketokensecret'
140
+ @request_token = OAuth::RequestToken.new(Stocktwits.consumer, session[:request_token], session[:request_token_secret])
141
+ OAuth::RequestToken.stub!(:new).and_return(@request_token)
142
+ end
143
+
144
+ it 'should call authentication_failed when it gets a 401 from OAuth' do
145
+ @request_token.stub!(:get_access_token).and_raise(Net::HTTPServerException.new('401 "Unauthorized"', '401 "Unauthorized"'))
146
+ controller.should_receive(:authentication_failed).with('This authentication request is no longer valid. Please try again.')
147
+ # the should raise_error is hacky because of the expectation
148
+ # stubbing the proper behavior :-(
149
+ lambda{get :oauth_callback, :oauth_token => 'faketoken'}.should raise_error(ActionView::MissingTemplate)
150
+ end
151
+
152
+ it 'should call authentication_failed when it gets a different HTTPServerException' do
153
+ @request_token.stub!(:get_access_token).and_raise(Net::HTTPServerException.new('404 "Not Found"', '404 "Not Found"'))
154
+ controller.should_receive(:authentication_failed).with('There was a problem trying to authenticate you. Please try again.')
155
+ lambda{get :oauth_callback, :oauth_token => 'faketoken'}.should raise_error(ActionView::MissingTemplate)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ describe 'with Basic strategy' do
163
+ before do
164
+ stub_basic!
165
+ end
166
+
167
+ describe '#new' do
168
+ it 'should render the new action' do
169
+ get :new
170
+ response.should render_template('sessions/new')
171
+ end
172
+
173
+ it 'should render the login form' do
174
+ get :new
175
+ response.should have_tag('form[action=/session][id=login_form][method=post]')
176
+ end
177
+
178
+ describe '#create' do
179
+ before do
180
+ @user = Factory.create(:twitter_basic_user, :twitter_id => '123')
181
+ end
182
+
183
+ it 'should call logout_keeping_session! to remove session info' do
184
+ controller.should_receive(:logout_keeping_session!)
185
+ post :create
186
+ end
187
+
188
+ it 'should try to authenticate the user' do
189
+ User.should_receive(:authenticate)
190
+ post :create
191
+ end
192
+
193
+ it 'should call authentication_failed on authenticate failure' do
194
+ User.should_receive(:authenticate).and_return(nil)
195
+ post :create, :login => 'wrong', :password => 'false'
196
+ response.should redirect_to('/login')
197
+ end
198
+
199
+ it 'should call authentication_succeeded on authentication success' do
200
+ User.should_receive(:authenticate).and_return(@user)
201
+ post :create, :login => 'twitterman', :password => 'cool'
202
+ response.should redirect_to('/')
203
+ flash[:notice].should_not be_blank
204
+ end
205
+ end
206
+ end
207
+
208
+ end
209
+
210
+ describe '#destroy' do
211
+ it 'should call logout_keeping_session!' do
212
+ controller.should_receive(:logout_keeping_session!).once
213
+ get :destroy
214
+ end
215
+
216
+ it 'should redirect to the root' do
217
+ get :destroy
218
+ response.should redirect_to('/')
219
+ end
220
+ end
221
+ end