tweetwine 0.2.12 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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