twurl 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ module Twurl
2
+ class ConfigurationController < AbstractCommandController
3
+ UNRECOGNIZED_SETTING_MESSAGE = "Unknown configuration setting: '%s'"
4
+ def dispatch
5
+ case options.subcommands.first
6
+ when 'default'
7
+ if profile = case options.subcommands.size
8
+ when 2
9
+ OAuthClient.load_client_for_username(options.subcommands.last)
10
+ when 3
11
+ OAuthClient.load_client_for_username_and_consumer_key(*options.subcommands[-2, 2])
12
+ end
13
+
14
+ OAuthClient.rcfile.default_profile = profile
15
+ OAuthClient.rcfile.save
16
+ end
17
+ else
18
+ CLI.puts(UNRECOGNIZED_SETTING_MESSAGE % options.subcommands.first)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,157 @@
1
+ module Twurl
2
+ class OAuthClient
3
+ class << self
4
+ def rcfile(reload = false)
5
+ if reload || @rcfile.nil?
6
+ @rcfile = RCFile.new
7
+ end
8
+ @rcfile
9
+ end
10
+
11
+ def load_from_options(options)
12
+ if rcfile.has_oauth_profile_for_username_with_consumer_key?(options.username, options.consumer_key)
13
+ load_client_for_username_and_consumer_key(options.username, options.consumer_key)
14
+ else
15
+ options.username ? load_new_client_from_options(options) : load_default_client
16
+ end
17
+ end
18
+
19
+ def load_client_for_username_and_consumer_key(username, consumer_key)
20
+ user_profiles = rcfile[username]
21
+ if user_profiles && attributes = user_profiles[consumer_key]
22
+ new(attributes)
23
+ else
24
+ raise Exception, "No profile for #{username}"
25
+ end
26
+ end
27
+
28
+ def load_client_for_username(username)
29
+ if user_profiles = rcfile[username]
30
+ if user_profiles.values.size == 1
31
+ new(user_profiles.values.first)
32
+ else
33
+ raise Exception, "There is more than one consumer key associated with #{username}. Please specify which consumer key you want as well."
34
+ end
35
+ else
36
+ raise Exception, "No profile for #{username}"
37
+ end
38
+ end
39
+
40
+ def load_new_client_from_options(options)
41
+ new(options.oauth_client_options.merge('password' => options.password))
42
+ end
43
+
44
+ def load_default_client
45
+ raise Exception, "You must authorize first" unless rcfile.default_profile
46
+ load_client_for_username_and_consumer_key(*rcfile.default_profile)
47
+ end
48
+ end
49
+
50
+ OAUTH_CLIENT_OPTIONS = %w[username consumer_key consumer_secret token secret]
51
+ attr_reader *OAUTH_CLIENT_OPTIONS
52
+ attr_reader :password
53
+ def initialize(options = {})
54
+ @username = options['username']
55
+ @password = options['password']
56
+ @consumer_key = options['consumer_key']
57
+ @consumer_secret = options['consumer_secret']
58
+ @token = options['token']
59
+ @secret = options['secret']
60
+ configure_http!
61
+ end
62
+
63
+ [:get, :post, :put, :delete, :options, :head, :copy].each do |request_method|
64
+ class_eval(<<-EVAL, __FILE__, __LINE__)
65
+ def #{request_method}(url, options = {})
66
+ # configure_http!
67
+ access_token.#{request_method}(url, options)
68
+ end
69
+ EVAL
70
+ end
71
+
72
+ def perform_request_from_options(options)
73
+ send(options.request_method, options.path, options.data)
74
+ end
75
+
76
+ def exchange_credentials_for_access_token
77
+ response = begin
78
+ consumer.token_request(:post, consumer.access_token_path, nil, {}, client_auth_parameters)
79
+ rescue OAuth::Unauthorized
80
+ perform_pin_authorize_workflow
81
+ end
82
+ @token = response[:oauth_token]
83
+ @secret = response[:oauth_token_secret]
84
+ end
85
+
86
+ def client_auth_parameters
87
+ {:x_auth_username => username, :x_auth_password => password, :x_auth_mode => 'client_auth'}
88
+ end
89
+
90
+ def perform_pin_authorize_workflow
91
+ @request_token = consumer.get_request_token
92
+ CLI.puts("Go to #{generate_authorize_url} and paste in the supplied PIN")
93
+ pin = gets
94
+ access_token = @request_token.get_access_token(:oauth_verifier => pin.chomp)
95
+ {:oauth_token => access_token.token, :oauth_token_secret => access_token.secret}
96
+ end
97
+
98
+ def generate_authorize_url
99
+ request = consumer.create_signed_request(:get, consumer.authorize_path, @request_token, pin_auth_parameters)
100
+ params = request['Authorization'].sub(/^OAuth\s+/, '').split(/,\s+/).map { |p|
101
+ k, v = p.split('=')
102
+ v =~ /"(.*?)"/
103
+ "#{k}=#{CGI::escape($1)}"
104
+ }.join('&')
105
+ "#{Twurl.options.base_url}#{request.path}?#{params}"
106
+ end
107
+
108
+ def pin_auth_parameters
109
+ {:oauth_callback => 'oob'}
110
+ end
111
+
112
+ def authorized?
113
+ oauth_response = access_token.get('/1/account/verify_credentials.json')
114
+ oauth_response.class == Net::HTTPOK
115
+ end
116
+
117
+ def needs_to_authorize?
118
+ token.nil? || secret.nil?
119
+ end
120
+
121
+ def save
122
+ self.class.rcfile << self
123
+ end
124
+
125
+ def to_hash
126
+ OAUTH_CLIENT_OPTIONS.inject({}) do |hash, attribute|
127
+ if value = send(attribute)
128
+ hash[attribute] = value
129
+ end
130
+ hash
131
+ end
132
+ end
133
+
134
+ def configure_http!
135
+ consumer.http.set_debug_output(Twurl.options.debug_output_io) if Twurl.options.trace
136
+ if Twurl.options.ssl?
137
+ consumer.http.use_ssl = true
138
+ consumer.http.verify_mode = OpenSSL::SSL::VERIFY_NONE
139
+ end
140
+ end
141
+
142
+ def consumer
143
+ @consumer ||=
144
+ begin
145
+ OAuth::Consumer.new(
146
+ consumer_key,
147
+ consumer_secret,
148
+ :site => Twurl.options.base_url
149
+ )
150
+ end
151
+ end
152
+
153
+ def access_token
154
+ @access_token ||= OAuth::AccessToken.new(consumer, token, secret)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,93 @@
1
+ module Twurl
2
+ class RCFile
3
+ FILE = '.twurlrc'
4
+ @directory ||= ENV['HOME']
5
+ class << self
6
+ attr_accessor :directory
7
+
8
+ def file_path
9
+ File.join(directory, FILE)
10
+ end
11
+
12
+ def load
13
+ YAML.load_file(file_path)
14
+ rescue Errno::ENOENT
15
+ default_rcfile_structure
16
+ end
17
+
18
+ def default_rcfile_structure
19
+ {'profiles' => {}, 'configuration' => {}}
20
+ end
21
+ end
22
+
23
+ attr_reader :data
24
+ def initialize
25
+ @data = self.class.load
26
+ end
27
+
28
+ def empty?
29
+ data == self.class.default_rcfile_structure
30
+ end
31
+
32
+ def save
33
+ File.open(self.class.file_path, 'w') do |rcfile|
34
+ rcfile.write data.to_yaml
35
+ end
36
+ end
37
+
38
+ def [](username)
39
+ profiles[username]
40
+ end
41
+
42
+ def profiles
43
+ data['profiles']
44
+ end
45
+
46
+ def default_profile
47
+ configuration['default_profile']
48
+ end
49
+
50
+ def default_profile=(profile)
51
+ configuration['default_profile'] = [profile.username, profile.consumer_key]
52
+ end
53
+
54
+ def configuration
55
+ data['configuration']
56
+ end
57
+
58
+ def alias(name, path)
59
+ data['aliases'] ||= {}
60
+ data['aliases'][name] = path
61
+ save
62
+ end
63
+
64
+ def aliases
65
+ data['aliases']
66
+ end
67
+
68
+ def alias_from_options(options)
69
+ options.subcommands.each do |potential_alias|
70
+ if path = alias_from_name(potential_alias)
71
+ break path
72
+ end
73
+ end
74
+ end
75
+
76
+ def alias_from_name(name)
77
+ aliases[name]
78
+ end
79
+
80
+ def has_oauth_profile_for_username_with_consumer_key?(username, consumer_key)
81
+ user_profiles = self[username]
82
+ !user_profiles.nil? && !user_profiles[consumer_key].nil?
83
+ end
84
+
85
+ def <<(oauth_client)
86
+ client_from_file = self[oauth_client.username] || {}
87
+ client_from_file[oauth_client.consumer_key] = oauth_client.to_hash
88
+ (profiles[oauth_client.username] ||= {}).update(client_from_file)
89
+ self.default_profile = oauth_client unless default_profile
90
+ save
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,19 @@
1
+ module Twurl
2
+ class RequestController < AbstractCommandController
3
+ NO_URI_MESSAGE = "No URI specified"
4
+ def dispatch
5
+ if client.needs_to_authorize?
6
+ raise Exception, "You need to authorize first."
7
+ end
8
+ options.path ||= OAuthClient.rcfile.alias_from_options(options)
9
+ perform_request
10
+ end
11
+
12
+ def perform_request
13
+ response = client.perform_request_from_options(options)
14
+ CLI.puts response.body
15
+ rescue URI::InvalidURIError
16
+ CLI.puts NO_URI_MESSAGE
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ module Twurl
2
+ module VERSION
3
+ MAJOR = '0'
4
+ MINOR = '6'
5
+ TINY = '0'
6
+ BETA = nil # Time.now.to_i.to_s
7
+ end
8
+
9
+ Version = [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY, VERSION::BETA].compact * '.'
10
+ end
@@ -0,0 +1,61 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class Twurl::AccountInformationController::DispatchWithNoAuthorizedAccountsTest < Test::Unit::TestCase
4
+ attr_reader :options, :client, :controller
5
+ def setup
6
+ @options = Twurl::Options.new
7
+ @client = Twurl::OAuthClient.load_new_client_from_options(options)
8
+ @controller = Twurl::AccountInformationController.new(client, options)
9
+ mock(Twurl::OAuthClient.rcfile).empty? { true }
10
+ end
11
+
12
+ def test_message_indicates_when_no_accounts_are_authorized
13
+ mock(Twurl::CLI).puts(Twurl::AccountInformationController::NO_AUTHORIZED_ACCOUNTS_MESSAGE).times(1)
14
+
15
+ controller.dispatch
16
+ end
17
+ end
18
+
19
+ class Twurl::AccountInformationController::DispatchWithOneAuthorizedAccountTest < Test::Unit::TestCase
20
+ attr_reader :options, :client, :controller
21
+ def setup
22
+ @options = Twurl::Options.test_exemplar
23
+ @client = Twurl::OAuthClient.load_new_client_from_options(options)
24
+ mock(Twurl::OAuthClient.rcfile).save.times(1)
25
+ Twurl::OAuthClient.rcfile << client
26
+ @controller = Twurl::AccountInformationController.new(client, options)
27
+ end
28
+
29
+ def test_authorized_account_is_displayed_and_marked_as_the_default
30
+ mock(Twurl::CLI).puts(client.username).times(1).ordered
31
+ mock(Twurl::CLI).puts(" #{client.consumer_key} (default)").times(1).ordered
32
+
33
+ controller.dispatch
34
+ end
35
+ end
36
+
37
+ class Twurl::AccountInformationController::DispatchWithOneUsernameThatHasAuthorizedMultipleAccountsTest < Test::Unit::TestCase
38
+ attr_reader :default_client_options, :default_client, :other_client_options, :other_client, :controller
39
+ def setup
40
+ @default_client_options = Twurl::Options.test_exemplar
41
+ @default_client = Twurl::OAuthClient.load_new_client_from_options(default_client_options)
42
+
43
+ @other_client_options = Twurl::Options.test_exemplar
44
+ other_client_options.consumer_key = default_client_options.consumer_key.reverse
45
+ @other_client = Twurl::OAuthClient.load_new_client_from_options(other_client_options)
46
+ mock(Twurl::OAuthClient.rcfile).save.times(2)
47
+
48
+ Twurl::OAuthClient.rcfile << default_client
49
+ Twurl::OAuthClient.rcfile << other_client
50
+
51
+ @controller = Twurl::AccountInformationController.new(other_client, other_client_options)
52
+ end
53
+
54
+ def test_authorized_account_is_displayed_and_marked_as_the_default
55
+ mock(Twurl::CLI).puts(default_client.username).times(1)
56
+ mock(Twurl::CLI).puts(" #{default_client.consumer_key} (default)").times(1)
57
+ mock(Twurl::CLI).puts(" #{other_client.consumer_key}").times(1)
58
+
59
+ controller.dispatch
60
+ end
61
+ end
@@ -0,0 +1,53 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class Twurl::AliasesController::DispatchTest < Test::Unit::TestCase
4
+ attr_reader :options, :client
5
+ def setup
6
+ @options = Twurl::Options.test_exemplar
7
+ @client = Twurl::OAuthClient.test_exemplar
8
+
9
+ # Clean slate
10
+ if Twurl::OAuthClient.rcfile.aliases
11
+ Twurl::OAuthClient.rcfile.aliases.clear
12
+ end
13
+
14
+ stub(Twurl::OAuthClient.rcfile).save
15
+ end
16
+
17
+ def test_when_no_subcommands_are_provided_and_no_aliases_exist_nothing_is_displayed
18
+ assert options.subcommands.empty?
19
+ mock(Twurl::CLI).puts(Twurl::AliasesController::NO_ALIASES_MESSAGE).times(1)
20
+
21
+ controller = Twurl::AliasesController.new(client, options)
22
+ controller.dispatch
23
+ end
24
+
25
+ def test_when_no_subcommands_are_provided_and_aliases_exist_they_are_displayed
26
+ assert options.subcommands.empty?
27
+
28
+ Twurl::OAuthClient.rcfile.alias('h', '/1/statuses/home_timeline.xml')
29
+ mock(Twurl::CLI).puts("h: /1/statuses/home_timeline.xml").times(1)
30
+
31
+ controller = Twurl::AliasesController.new(client, options)
32
+ controller.dispatch
33
+ end
34
+
35
+ def test_when_alias_and_value_are_provided_they_are_added
36
+ options.subcommands = ['h']
37
+ options.path = '/1/statuses/home_timeline.xml'
38
+ mock(Twurl::OAuthClient.rcfile).alias('h', '/1/statuses/home_timeline.xml').times(1)
39
+
40
+ controller = Twurl::AliasesController.new(client, options)
41
+ controller.dispatch
42
+ end
43
+
44
+ def test_when_no_path_is_provided_nothing_happens
45
+ options.subcommands = ['a']
46
+ assert_nil options.path
47
+
48
+ mock(Twurl::CLI).puts(Twurl::AliasesController::NO_PATH_PROVIDED_MESSAGE).times(1)
49
+
50
+ controller = Twurl::AliasesController.new(client, options)
51
+ controller.dispatch
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class Twurl::AuthorizationController::DispatchTest < Test::Unit::TestCase
4
+ attr_reader :options, :client, :controller
5
+ def setup
6
+ @options = Twurl::Options.new
7
+ @client = Twurl::OAuthClient.load_new_client_from_options(options)
8
+ @controller = Twurl::AuthorizationController.new(client, options)
9
+ end
10
+
11
+ def test_successful_authentication_saves_retrieved_access_token
12
+ mock(client).exchange_credentials_for_access_token.times(1)
13
+ mock(client).save.times(1)
14
+ mock(controller).raise(Twurl::Exception, Twurl::AuthorizationController::AUTHORIZATION_FAILED_MESSAGE).never
15
+ mock(Twurl::CLI).puts(Twurl::AuthorizationController::AUTHORIZATION_SUCCEEDED_MESSAGE).times(1)
16
+
17
+ controller.dispatch
18
+ end
19
+
20
+ module ErrorCases
21
+ def test_failed_authorization_does_not_save_client
22
+ mock(client).exchange_credentials_for_access_token { raise OAuth::Unauthorized }
23
+ mock(client).save.never
24
+ mock(controller).raise(Twurl::Exception, Twurl::AuthorizationController::AUTHORIZATION_FAILED_MESSAGE).times(1)
25
+
26
+ controller.dispatch
27
+ end
28
+ end
29
+ include ErrorCases
30
+ end