tweetwine 0.2.12 → 0.3.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.
Files changed (67) hide show
  1. data/CHANGELOG.rdoc +7 -0
  2. data/Gemfile +17 -0
  3. data/README.md +57 -47
  4. data/Rakefile +17 -26
  5. data/bin/tweetwine +11 -12
  6. data/contrib/tweetwine-completion.bash +2 -3
  7. data/example/application_behavior_example.rb +173 -0
  8. data/example/example_helper.rb +44 -28
  9. data/example/fixture/config.yaml +8 -0
  10. data/example/fixture/shorten_rubygems.html +5 -0
  11. data/example/fixture/shorten_rubylang.html +5 -0
  12. data/example/fixture/update_utf8.json +1 -0
  13. data/example/fixture/update_with_urls.json +1 -0
  14. data/example/fixture/{update.json → update_without_urls.json} +0 -0
  15. data/example/search_statuses_example.rb +49 -16
  16. data/example/show_followers_example.rb +7 -8
  17. data/example/show_friends_example.rb +7 -8
  18. data/example/show_home_example.rb +19 -16
  19. data/example/show_mentions_example.rb +8 -9
  20. data/example/show_user_example.rb +16 -13
  21. data/example/update_status_example.rb +143 -26
  22. data/example/use_http_proxy_example.rb +40 -20
  23. data/lib/tweetwine/basic_object.rb +19 -0
  24. data/lib/tweetwine/character_encoding.rb +59 -0
  25. data/lib/tweetwine/cli.rb +354 -230
  26. data/lib/tweetwine/config.rb +65 -0
  27. data/lib/tweetwine/http.rb +120 -0
  28. data/lib/tweetwine/oauth.rb +104 -0
  29. data/lib/tweetwine/obfuscate.rb +21 -0
  30. data/lib/tweetwine/option_parser.rb +31 -0
  31. data/lib/tweetwine/promise.rb +39 -0
  32. data/lib/tweetwine/twitter.rb +211 -0
  33. data/lib/tweetwine/{io.rb → ui.rb} +30 -21
  34. data/lib/tweetwine/url_shortener.rb +15 -9
  35. data/lib/tweetwine/util.rb +30 -15
  36. data/lib/tweetwine.rb +72 -12
  37. data/man/tweetwine.7 +43 -69
  38. data/man/tweetwine.7.ronn +57 -47
  39. data/test/character_encoding_test.rb +87 -0
  40. data/test/cli_test.rb +19 -6
  41. data/test/config_test.rb +244 -0
  42. data/test/fixture/oauth.rb +21 -0
  43. data/test/fixture/test_config.yaml +4 -4
  44. data/test/http_test.rb +199 -0
  45. data/test/oauth_test.rb +77 -0
  46. data/test/obfuscate_test.rb +16 -0
  47. data/test/option_parser_test.rb +60 -0
  48. data/test/promise_test.rb +56 -0
  49. data/test/test_helper.rb +76 -8
  50. data/test/twitter_test.rb +625 -0
  51. data/test/{io_test.rb → ui_test.rb} +92 -74
  52. data/test/url_shortener_test.rb +115 -135
  53. data/test/util_test.rb +136 -85
  54. data/tweetwine.gemspec +53 -0
  55. metadata +112 -56
  56. data/example/show_metadata_example.rb +0 -86
  57. data/lib/tweetwine/client.rb +0 -187
  58. data/lib/tweetwine/meta.rb +0 -5
  59. data/lib/tweetwine/options.rb +0 -24
  60. data/lib/tweetwine/retrying_http.rb +0 -99
  61. data/lib/tweetwine/startup_config.rb +0 -50
  62. data/man/tweetwine.1 +0 -109
  63. data/man/tweetwine.1.ronn +0 -69
  64. data/test/client_test.rb +0 -544
  65. data/test/options_test.rb +0 -45
  66. data/test/retrying_http_test.rb +0 -147
  67. data/test/startup_config_test.rb +0 -162
@@ -0,0 +1,87 @@
1
+ # coding: utf-8
2
+
3
+ require "test_helper"
4
+
5
+ module Tweetwine::Test
6
+
7
+ class CharacterEncodingTest < UnitTestCase
8
+ if "".respond_to?(:encode)
9
+ context "when transcoding to UTF-8 when String supports encoding" do
10
+ should "transcode string to UTF-8" do
11
+ str_utf8 = "groß résumé"
12
+ str_latin1 = str_utf8.encode('ISO-8859-1')
13
+ assert_equal str_utf8, CharacterEncoding.to_utf8(str_latin1)
14
+ end
15
+
16
+ should "raise exception if result is invalid UTF-8" do
17
+ assert_raise(TranscodeError) { CharacterEncoding.to_utf8("\xa4") }
18
+ end
19
+ end
20
+ else
21
+ context "when transcoding to UTF-8 when String does not support encoding" do
22
+ # résumé
23
+ RESUME_EUC = "r\x8F\xAB\xB1sum\x8F\xAB\xB1"
24
+ RESUME_LATIN1 = "r\xe9sum\xe9"
25
+ RESUME_UTF8 = "r\xc3\xa9sum\xc3\xa9"
26
+
27
+ # ホーム ("home" in Japanese)
28
+ HOME_SJIS = "\x83\x7a\x81\x5b\x83\x80"
29
+ HOME_UTF8 = "\xe3\x83\x9b\xe3\x83\xbc\xe3\x83\xa0"
30
+
31
+ setup do
32
+ Tweetwine::CharacterEncoding.forget_guess
33
+ end
34
+
35
+ [
36
+ ['EUC', RESUME_EUC, RESUME_UTF8],
37
+ ['SJIS', HOME_SJIS, HOME_UTF8]
38
+ ].each do |(kcode, original, expected)|
39
+ should "transcode with Iconv, guessing first from $KCODE, case #{kcode}" do
40
+ tmp_kcode(kcode) do
41
+ assert_equal expected, CharacterEncoding.to_utf8(original)
42
+ end
43
+ end
44
+ end
45
+
46
+ [
47
+ ['latin1', RESUME_LATIN1, RESUME_UTF8],
48
+ ['EUC-JP', RESUME_EUC, RESUME_UTF8],
49
+ ['SHIFT_JIS', HOME_SJIS, HOME_UTF8]
50
+ ].each do |(lang, original, expected)|
51
+ should "transcode with Iconv, guessing second from envar $LANG, case #{lang}" do
52
+ tmp_kcode('NONE') do
53
+ tmp_env(:LANG => lang) do
54
+ assert_equal expected, CharacterEncoding.to_utf8(original)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ should "pass string as is, if guess is UTF-8, case $KCODE is UTF-8" do
61
+ tmp_kcode('UTF8') do
62
+ assert_same RESUME_UTF8, CharacterEncoding.to_utf8(RESUME_UTF8)
63
+ end
64
+ end
65
+
66
+ %w{utf8 UTF-8 en_US.UTF-8 fi_FI.utf-8 fi_FI.utf8}.each do |lang|
67
+ should "pass string as is, if guess is UTF-8, case envar $LANG is '#{lang}'" do
68
+ tmp_kcode('NONE') do
69
+ tmp_env(:LANG => lang) do
70
+ assert_same RESUME_UTF8, CharacterEncoding.to_utf8(RESUME_UTF8)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ should "raise exception if conversion cannot be done because we couldn't guess external encoding" do
77
+ tmp_kcode('NONE') do
78
+ tmp_env(:LANG => nil) do
79
+ assert_raise(Tweetwine::TranscodeError) { CharacterEncoding.to_utf8(RESUME_LATIN1) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ end
data/test/cli_test.rb CHANGED
@@ -1,18 +1,31 @@
1
1
  # coding: utf-8
2
2
 
3
3
  require "test_helper"
4
+ require "stringio"
4
5
 
5
- module Tweetwine
6
+ module Tweetwine::Test
6
7
 
7
- class CLITest < TweetwineTestCase
8
- context "A CLI, upon initialization" do
8
+ # See +example+ directory for integration tests.
9
+ class CLITest < UnitTestCase
10
+ context "for initialization" do
9
11
  should "disallow using #new to create a new instance" do
10
- assert_raise(NoMethodError) { CLI.new("-v", "test", "") {} }
12
+ assert_raise(NoMethodError) { CLI.new }
13
+ end
14
+
15
+ should "allow defining same option multiple times, last value winning" do
16
+ winning_option_value = 'second'
17
+ start_cli %W{-f first -f #{winning_option_value} -v}
18
+ assert_equal winning_option_value, CLI.config[:config_file]
11
19
  end
12
20
  end
13
21
 
14
- # Other unit tests are meaningless. See /example directory for tests
15
- # about the functionality of the application.
22
+ private
23
+
24
+ def start_cli(args)
25
+ output = StringIO.new
26
+ CLI.start(args, { :out => output })
27
+ output.string.split("\n")
28
+ end
16
29
  end
17
30
 
18
31
  end
@@ -0,0 +1,244 @@
1
+ # coding: utf-8
2
+
3
+ require "test_helper"
4
+
5
+ require "fileutils"
6
+ require "tempfile"
7
+ require "yaml"
8
+
9
+ module Tweetwine::Test
10
+
11
+ class ConfigTest < UnitTestCase
12
+ CONFIG_FILE = Helper.fixture_file("test_config.yaml")
13
+
14
+ context "when given command line arguments, no environment variables, no config file" do
15
+ setup do
16
+ @args = %w{--opt cmd_opt --defopt cmd_defopt left overs}
17
+ default_config = {:defopt => 'defopt'}
18
+ @config = Config.read(@args, default_config) do |args|
19
+ args.slice!(0..3)
20
+ {:opt => 'cmd_opt', :defopt => 'cmd_defopt'}
21
+ end
22
+ end
23
+
24
+ should "have option defined from command line" do
25
+ assert_equal 'cmd_opt', @config[:opt]
26
+ end
27
+
28
+ should "override option default value from command line" do
29
+ assert_equal 'cmd_defopt', @config[:defopt]
30
+ end
31
+
32
+ should "leave remaining command line arguments unconsumed" do
33
+ assert_equal %w{left overs}, @args
34
+ end
35
+ end
36
+
37
+ context "when given command line arguments, environment variables, no config file" do
38
+ setup do
39
+ ENV['opt'] = 'env_opt'
40
+ ENV['defopt'] = 'env_defopt'
41
+ ENV['envopt'] = 'env_envopt'
42
+ @args = %w{--opt cmd_opt}
43
+ default_config = {:defopt => 'defopt', :env_lookouts => [:defopt, :envopt, :opt]}
44
+ @config = Config.read(@args, default_config) do |args|
45
+ args.slice!(0..1)
46
+ {:opt => 'cmd_opt'}
47
+ end
48
+ end
49
+
50
+ teardown do
51
+ ENV['opt'] = nil
52
+ ENV['defopt'] = nil
53
+ ENV['envopt'] = nil
54
+ end
55
+
56
+ should "have option defined from environment variable" do
57
+ assert_equal 'env_envopt', @config[:envopt]
58
+ end
59
+
60
+ should "override option value from command line over environment variable" do
61
+ assert_equal 'cmd_opt', @config[:opt]
62
+ end
63
+
64
+ should "override option default value from environment variable" do
65
+ assert_equal 'env_defopt', @config[:defopt]
66
+ end
67
+ end
68
+
69
+ context "when given command line arguments, no environment variables, config file" do
70
+ setup do
71
+ @args = %w{--opt cmd_opt}
72
+ default_config = {:config_file => CONFIG_FILE, :defopt => 'defopt'}
73
+ @config = Config.read(@args, default_config) do |args|
74
+ args.slice!(0..1)
75
+ {:opt => 'cmd_opt'}
76
+ end
77
+ end
78
+
79
+ should "have option defined from config file" do
80
+ assert_equal 'file_fileopt', @config[:fileopt]
81
+ end
82
+
83
+ should "override option value from command line over config file" do
84
+ assert_equal 'cmd_opt', @config[:opt]
85
+ end
86
+
87
+ should "override option default value from config file" do
88
+ assert_equal 'file_defopt', @config[:defopt]
89
+ end
90
+ end
91
+
92
+ context "when given command line arguments, environment variables, config file" do
93
+ setup do
94
+ @args = %w{--opt2 cmd_opt2}
95
+ ENV['opt'] = 'env_opt'
96
+ ENV['opt2'] = 'env_opt2'
97
+ @config = Config.read(@args, :config_file => CONFIG_FILE, :env_lookouts => [:opt, :opt2]) do |args|
98
+ args.slice!(0..2)
99
+ {:opt2 => 'cmd_opt2'}
100
+ end
101
+ end
102
+
103
+ teardown do
104
+ ENV['opt'] = nil
105
+ ENV['opt2'] = nil
106
+ end
107
+
108
+ should "override option value from environment variable over config file" do
109
+ assert_equal 'env_opt', @config[:opt]
110
+ end
111
+
112
+ should "override option value from command line over environment variable and config file" do
113
+ assert_equal 'cmd_opt2', @config[:opt2]
114
+ end
115
+ end
116
+
117
+ context "when handling command line arguments without parser" do
118
+ setup do
119
+ ENV['opt'] = 'env_opt'
120
+ @args = %w{--opt cmd_opt --defopt cmd_defopt}
121
+ default_config = {:config_file => CONFIG_FILE, :defopt => 'defopt', :env_lookouts => [:opt]}
122
+ @config = Config.read(@args, default_config)
123
+ end
124
+
125
+ teardown do
126
+ ENV['opt'] = nil
127
+ end
128
+
129
+ should "ignore command line arguments, using environment variables and config file for options if available" do
130
+ assert_equal 'env_opt', @config[:opt]
131
+ assert_equal 'file_defopt', @config[:defopt]
132
+ end
133
+ end
134
+
135
+ context "when handling environment variables" do
136
+ setup do
137
+ ENV['visible'] = 'env_visible'
138
+ ENV['hidden'] = 'env_hidden'
139
+ ENV['empty'] = ''
140
+ @config = Config.read([], :env_lookouts => [:visible, :empty])
141
+ end
142
+
143
+ teardown do
144
+ ENV['visible'] = nil
145
+ ENV['hidden'] = nil
146
+ ENV['empty'] = nil
147
+ end
148
+
149
+ should "consider only specified environment variables that are nonempty" do
150
+ assert_equal 'env_visible', @config[:visible]
151
+ end
152
+
153
+ should "not consider empty environment variables" do
154
+ assert_equal nil, @config[:empty]
155
+ end
156
+
157
+ should "not consider unspecified environment variables" do
158
+ assert_equal nil, @config[:hidden]
159
+ end
160
+ end
161
+
162
+ context "when handling the config file" do
163
+ should "allow specifying configuration file from command line arguments" do
164
+ @args = ['-f', CONFIG_FILE]
165
+ @config = Config.read(@args, {}) do
166
+ @args.slice!(0..1)
167
+ {:config_file => CONFIG_FILE}
168
+ end
169
+ assert_equal %w{config_file defopt fileopt opt}, @config.keys.map { |k| k.to_s }.sort
170
+ assert_equal 'file_defopt', @config[:defopt]
171
+ end
172
+
173
+ should "raise exception when trying to save and no config file is specified" do
174
+ @config = Config.read()
175
+ assert_raise(RuntimeError) { @config.save }
176
+ end
177
+
178
+ context "when config file does not exist" do
179
+ setup do
180
+ @tmp_dir = Dir.mktmpdir
181
+ @file = @tmp_dir + '/.tweetwine'
182
+ @excludes = [:secret]
183
+ @default_config = {:config_file => @file, :env_lookouts => [:envopt], :excludes => @excludes}
184
+ @config = Config.read([], @default_config)
185
+ @expected_config = {'new_opt' => 'to_file'}
186
+ end
187
+
188
+ teardown do
189
+ FileUtils.remove_entry_secure @tmp_dir
190
+ end
191
+
192
+ should "ignore nonexisting config file for initial read" do
193
+ assert_contains_in_order(@default_config.keys, @config.keys) do |a, b|
194
+ # On Ruby 1.8, Symbol does not have #<=> operator for comparison.
195
+ a.to_s <=> b.to_s
196
+ end
197
+ end
198
+
199
+ should "save config to the file, implicitly without config file, env lookouts, and excludes set itself" do
200
+ @config[:new_opt] = 'to_file'
201
+ @config.save
202
+ stored = YAML.load_file @file
203
+ assert_equal(@expected_config, stored)
204
+ end
205
+
206
+ should "save config to the file, explicitly without excluded entries" do
207
+ @config[@excludes.first] = 'password'
208
+ @config[:new_opt] = 'to_file'
209
+ @config.save
210
+ stored = YAML.load_file @file
211
+ assert_equal(@expected_config, stored)
212
+ end
213
+
214
+ should "modifying exclusions after initial read has no effect on config file location and exclusions" do
215
+ @config[:config_file] = @tmp_dir + '/.tweetwine.another'
216
+ @config[:excludes] << :new_opt
217
+ @config[:new_opt] = 'to_file'
218
+ @config.save
219
+ stored = YAML.load_file @file
220
+ assert_equal(@expected_config, stored)
221
+ end
222
+
223
+ should "set config file permissions accessible only to the user when saving" do
224
+ @config.save
225
+ assert_equal 0600, file_mode(@file)
226
+ end
227
+
228
+ context "when config file exists" do
229
+ setup do
230
+ FileUtils.touch @file
231
+ @original_mode = 0644
232
+ File.chmod @original_mode, @file
233
+ end
234
+
235
+ should "not set config file permissions when saving" do
236
+ @config.save
237
+ assert_equal @original_mode, file_mode(@file)
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ end
@@ -0,0 +1,21 @@
1
+ # coding: utf-8
2
+
3
+ module Tweetwine::Test
4
+
5
+ module Fixture
6
+ module OAuth
7
+ METHOD = :post
8
+ REQUEST_TOKEN_KEY = 'ManManManManManManManManManManManManManM'
9
+ REQUEST_TOKEN_SECRET = '3x3x3x3x3x3x3x3x3x3x3x3x3x3x3x3x3x3x3x3x3'
10
+ REQUEST_TOKEN_RESPONSE = "oauth_token=#{REQUEST_TOKEN_KEY}&oauth_token_secret=#{REQUEST_TOKEN_SECRET}&oauth_callback_confirmed=true"
11
+ REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
12
+ ACCESS_TOKEN_KEY = '111111111-XyzXyzXyzXyzXyzXyzXyzXyzXyzXyzXyzXyzXyzX'
13
+ ACCESS_TOKEN_SECRET = '4x4x4x4x4x4x4x4x4x4x4x4x4x4x4x4x4x4x4x4x4x'
14
+ ACCESS_TOKEN_RESPONSE = "oauth_token=#{ACCESS_TOKEN_KEY}&oauth_token_secret=#{ACCESS_TOKEN_SECRET}&user_id=42&screen_name=fooman"
15
+ ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
16
+ AUTHORIZE_URL = "https://api.twitter.com/oauth/authorize?oauth_token=#{REQUEST_TOKEN_KEY}"
17
+ PIN = '12345678'
18
+ end
19
+ end
20
+
21
+ end
@@ -1,4 +1,4 @@
1
- username: foo
2
- password: bar
3
- colors: false
4
- defopt: 78
1
+ ---
2
+ opt: file_opt
3
+ fileopt: file_fileopt
4
+ defopt: file_defopt
data/test/http_test.rb ADDED
@@ -0,0 +1,199 @@
1
+ # coding: utf-8
2
+
3
+ require "test_helper"
4
+
5
+ module Tweetwine::Test
6
+
7
+ class HttpTest < UnitTestCase
8
+ RESPONSE_BODY = "resp"
9
+ CUSTOM_HEADERS = {'X-Custom' => 'true'}
10
+ SITE_URL = "https://site.org"
11
+ LATEST_ARTICLES_URL = "#{SITE_URL}/articles/latest"
12
+ LATEST_ARTICLES_URL_SORTED = "#{LATEST_ARTICLES_URL}?sort=date"
13
+ NEW_ARTICLE_URL = "#{SITE_URL}/articles/new"
14
+ PAYLOAD = {:msg => 'gloomy night'}
15
+
16
+ setup do
17
+ stub_sleep
18
+ mock_ui
19
+ @client = Http::Client.new
20
+ end
21
+
22
+ teardown do
23
+ restore_sleep
24
+ end
25
+
26
+ %w{http https}.each do |scheme|
27
+ should "support #{scheme} scheme" do
28
+ url = "#{scheme}://site.org/"
29
+ stub_request(:get, url)
30
+ @client.get(url)
31
+ assert_requested(:get, url)
32
+ end
33
+ end
34
+
35
+ should "return response body when successful response" do
36
+ stub_request(:get, LATEST_ARTICLES_URL).to_return(:body => RESPONSE_BODY)
37
+ assert_equal(RESPONSE_BODY, @client.get(LATEST_ARTICLES_URL))
38
+ end
39
+
40
+ should "send GET request with query parameters and custom headers" do
41
+ stub_request(:get, LATEST_ARTICLES_URL_SORTED)
42
+ @client.get(LATEST_ARTICLES_URL_SORTED, CUSTOM_HEADERS)
43
+ assert_requested(:get, LATEST_ARTICLES_URL_SORTED, :headers => CUSTOM_HEADERS)
44
+ end
45
+
46
+ should "send POST request with payload and custom headers" do
47
+ stub_request(:post, NEW_ARTICLE_URL).with(:body => PAYLOAD, :headers => CUSTOM_HEADERS)
48
+ @client.post(NEW_ARTICLE_URL, PAYLOAD, CUSTOM_HEADERS)
49
+ assert_requested(:post, NEW_ARTICLE_URL, :body => PAYLOAD, :headers => CUSTOM_HEADERS)
50
+ end
51
+
52
+ should "store response code and message in HttpError when failed response" do
53
+ code, message = 501, 'Not Implemented'
54
+ stub_request(:get, LATEST_ARTICLES_URL).to_return(:status => [code, message])
55
+ begin
56
+ @client.send(:get, LATEST_ARTICLES_URL)
57
+ rescue HttpError => e
58
+ assert_equal(code, e.http_code)
59
+ assert_equal(message, e.http_message)
60
+ else
61
+ flunk 'Should have raised HttpError'
62
+ end
63
+ end
64
+
65
+ [:get, :post].each do |method|
66
+ should "raise HttpError when failed response to #{method} request" do
67
+ stub_request(method, LATEST_ARTICLES_URL).to_return(:status => [500, "Internal Server Error"])
68
+ assert_raise(HttpError) { @client.send(method, LATEST_ARTICLES_URL) }
69
+ end
70
+
71
+ should "retry connection upon connection timeout to #{method} request" do
72
+ stub_request(method, LATEST_ARTICLES_URL).to_timeout.then.to_return(:body => RESPONSE_BODY)
73
+ @ui.expects(:warn).with("Could not connect -- retrying in 4 seconds")
74
+ @client.send(method, LATEST_ARTICLES_URL)
75
+ assert_equal(RESPONSE_BODY, @client.send(method, LATEST_ARTICLES_URL))
76
+ end
77
+
78
+ should "retry connection a maximum of certain number of times upon connection timeout to #{method} request" do
79
+ stub_request(method, LATEST_ARTICLES_URL).to_timeout
80
+ io_calls = sequence("IO")
81
+ Http::Client::MAX_RETRIES.times { @ui.expects(:warn).in_sequence(io_calls) }
82
+ assert_raise(TimeoutError) { @client.send(method, LATEST_ARTICLES_URL) }
83
+ end
84
+
85
+ [
86
+ [Errno::ECONNABORTED, 'abort'],
87
+ [Errno::ECONNRESET, 'reset']
88
+ ].each do |error, desc|
89
+ should "retry connection upon connection #{desc} to #{method} request" do
90
+ stub_request(method, LATEST_ARTICLES_URL).to_raise(error).then.to_return(:body => RESPONSE_BODY)
91
+ @ui.expects(:warn).with("Could not connect -- retrying in 4 seconds")
92
+ @client.send(method, LATEST_ARTICLES_URL)
93
+ assert_equal(RESPONSE_BODY, @client.send(method, LATEST_ARTICLES_URL))
94
+ end
95
+
96
+ should "retry connection a maximum of certain number of times upon connection #{desc} to #{method} request" do
97
+ stub_request(method, LATEST_ARTICLES_URL).to_raise(error)
98
+ io_calls = sequence("IO")
99
+ Http::Client::MAX_RETRIES.times { @ui.expects(:warn).in_sequence(io_calls) }
100
+ assert_raise(ConnectionError) { @client.send(method, LATEST_ARTICLES_URL) }
101
+ end
102
+ end
103
+
104
+ should "allow access to the #{method} request object just before sending it" do
105
+ stub_request(method, LATEST_ARTICLES_URL)
106
+ @client.send(method, LATEST_ARTICLES_URL) do |_, request|
107
+ request['X-Quote'] = 'You monster.'
108
+ end
109
+ assert_requested(method, LATEST_ARTICLES_URL, :headers => {'X-Quote' => 'You monster.'})
110
+ end
111
+ end
112
+
113
+ context "for proxy support" do
114
+ [
115
+ ['proxy.net', 'proxy.net', 8080],
116
+ ['http://proxy.net', 'proxy.net', 8080],
117
+ ['https://proxy.net', 'proxy.net', 8080],
118
+ ['http://proxy.net:8080', 'proxy.net', 8080],
119
+ ['http://proxy.net:8182', 'proxy.net', 8182]
120
+ ].each do |proxy_url, expected_host, expected_port|
121
+ should "support proxy, when its URL is #{proxy_url}" do
122
+ proxy = Http::Client.new(:http_proxy => proxy_url)
123
+ net_http = proxy.instance_variable_get(:@http)
124
+ assert(net_http.proxy_class?)
125
+ assert_equal(expected_host, net_http.instance_variable_get(:@proxy_address))
126
+ assert_equal(expected_port, net_http.instance_variable_get(:@proxy_port))
127
+ end
128
+ end
129
+
130
+ should "raise CommandLineError if given invalid port" do
131
+ assert_raise(CommandLineError) do
132
+ Http::Client.new(:http_proxy => 'http://proxy.net:asdf')
133
+ end
134
+ end
135
+ end
136
+
137
+ context "when using client as resource" do
138
+ setup do
139
+ @resource = @client.as_resource(SITE_URL)
140
+ end
141
+
142
+ should "send GET request with custom headers to the base URL" do
143
+ stub_request(:get, SITE_URL)
144
+ @resource.get(CUSTOM_HEADERS)
145
+ assert_requested(:get, SITE_URL, :headers => CUSTOM_HEADERS)
146
+ end
147
+
148
+ should "send POST request with payload and custom headers to the base URL" do
149
+ stub_request(:post, SITE_URL).with(:body => PAYLOAD, :headers => CUSTOM_HEADERS)
150
+ @resource.post(PAYLOAD, CUSTOM_HEADERS)
151
+ assert_requested(:post, SITE_URL, :body => PAYLOAD, :headers => CUSTOM_HEADERS)
152
+ end
153
+
154
+ context "when constructing new resource from the original as a sub-URL" do
155
+ setup do
156
+ @news_resource = @resource['news']
157
+ @expected_news_url = "#{SITE_URL}/news"
158
+ end
159
+
160
+ should "send GET request with custom headers to the sub-URL" do
161
+ stub_request(:get, @expected_news_url)
162
+ @news_resource.get(CUSTOM_HEADERS)
163
+ assert_requested(:get, @expected_news_url, :headers => CUSTOM_HEADERS)
164
+ end
165
+
166
+ should "send POST request with payload and custom headers to the sub-URL" do
167
+ stub_request(:post, @expected_news_url).with(:body => PAYLOAD, :headers => CUSTOM_HEADERS)
168
+ @news_resource.post(PAYLOAD, CUSTOM_HEADERS)
169
+ assert_requested(:post, @expected_news_url, :body => PAYLOAD, :headers => CUSTOM_HEADERS)
170
+ end
171
+
172
+ should "further construct a new resource from the original" do
173
+ news_resource = @news_resource['top']
174
+ expected_url = "#{SITE_URL}/news/top"
175
+ stub_request(:get, expected_url)
176
+ news_resource.get(CUSTOM_HEADERS)
177
+ assert_requested(:get, expected_url, :headers => CUSTOM_HEADERS)
178
+ end
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ def stub_sleep
185
+ Kernel.class_eval do
186
+ alias_method :__original_sleep, :sleep
187
+ define_method(:sleep) { |*args| }
188
+ end
189
+ end
190
+
191
+ def restore_sleep
192
+ Kernel.class_eval do
193
+ remove_method :sleep
194
+ alias_method :sleep, :__original_sleep
195
+ end
196
+ end
197
+ end
198
+
199
+ end
@@ -0,0 +1,77 @@
1
+ # coding: utf-8
2
+
3
+ require "test_helper"
4
+ require "fixture/oauth"
5
+ require "net/http"
6
+
7
+ module Tweetwine::Test
8
+
9
+ class OAuthTest < UnitTestCase
10
+ include Fixture::OAuth
11
+
12
+ setup do
13
+ mock_http
14
+ mock_ui
15
+ @oauth = OAuth.new
16
+ end
17
+
18
+ should "authorize with PIN code so that request can be signed" do
19
+ expect_complete_oauth_dance
20
+ @oauth.authorize
21
+ connection, request = *fake_http_connection_and_request
22
+ @oauth.request_signer.call(connection, request)
23
+ assert_match(/^OAuth /, request['Authorization'])
24
+ assert_match(/oauth_token="#{ACCESS_TOKEN_KEY}"/, request['Authorization'])
25
+ end
26
+
27
+ should "raise AuthorizationError if OAuth dance fails due to HTTP 4xx response" do
28
+ @http.expects(:post).
29
+ with(REQUEST_TOKEN_URL).
30
+ raises(HttpError.new(401, 'Unauthorized'))
31
+ assert_raise(AuthorizationError) { @oauth.authorize }
32
+ end
33
+
34
+ should "pass other exceptions than due to HTTP 4xx responses through" do
35
+ @http.expects(:post).
36
+ with(REQUEST_TOKEN_URL).
37
+ raises(HttpError.new(503, 'Service Unavailable'))
38
+ assert_raise(HttpError) { @oauth.authorize }
39
+ end
40
+
41
+ context "when access token is known" do
42
+ setup do
43
+ @oauth = OAuth.new(Obfuscate.write("#{ACCESS_TOKEN_KEY}:2"))
44
+ end
45
+
46
+ should "sign request with it" do
47
+ connection, request = *fake_http_connection_and_request
48
+ @oauth.request_signer.call(connection, request)
49
+ assert_match(/^OAuth /, request['Authorization'])
50
+ assert_match(/oauth_token="#{ACCESS_TOKEN_KEY}"/, request['Authorization'])
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def expect_complete_oauth_dance
57
+ @http.expects(:post).
58
+ with(REQUEST_TOKEN_URL).
59
+ returns(REQUEST_TOKEN_RESPONSE)
60
+ @ui.expects(:info).
61
+ with("Please authorize: #{AUTHORIZE_URL}")
62
+ @ui.expects(:prompt).
63
+ with('Enter PIN').
64
+ returns(PIN)
65
+ @http.expects(:post).
66
+ with(ACCESS_TOKEN_URL).
67
+ returns(ACCESS_TOKEN_RESPONSE)
68
+ end
69
+
70
+ def fake_http_connection_and_request
71
+ connection = stub(:address => 'api.twitter.com', :port => 443)
72
+ request = Net::HTTP::Post.new('1/statuses/home_timeline.json')
73
+ [connection, request]
74
+ end
75
+ end
76
+
77
+ end
@@ -0,0 +1,16 @@
1
+ # coding: utf-8
2
+
3
+ require "test_helper"
4
+
5
+ module Tweetwine::Test
6
+
7
+ class ObfuscateTest < UnitTestCase
8
+ include Obfuscate
9
+
10
+ should "obfuscate symmetrically" do
11
+ str = 'hey, jack'
12
+ assert_equal(str, obfuscate(obfuscate(str)))
13
+ end
14
+ end
15
+
16
+ end