tweetwine 0.2.12 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +7 -0
- data/Gemfile +17 -0
- data/README.md +57 -47
- data/Rakefile +17 -26
- data/bin/tweetwine +11 -12
- data/contrib/tweetwine-completion.bash +2 -3
- data/example/application_behavior_example.rb +173 -0
- data/example/example_helper.rb +44 -28
- data/example/fixture/config.yaml +8 -0
- data/example/fixture/shorten_rubygems.html +5 -0
- data/example/fixture/shorten_rubylang.html +5 -0
- data/example/fixture/update_utf8.json +1 -0
- data/example/fixture/update_with_urls.json +1 -0
- data/example/fixture/{update.json → update_without_urls.json} +0 -0
- data/example/search_statuses_example.rb +49 -16
- data/example/show_followers_example.rb +7 -8
- data/example/show_friends_example.rb +7 -8
- data/example/show_home_example.rb +19 -16
- data/example/show_mentions_example.rb +8 -9
- data/example/show_user_example.rb +16 -13
- data/example/update_status_example.rb +143 -26
- data/example/use_http_proxy_example.rb +40 -20
- data/lib/tweetwine/basic_object.rb +19 -0
- data/lib/tweetwine/character_encoding.rb +59 -0
- data/lib/tweetwine/cli.rb +354 -230
- data/lib/tweetwine/config.rb +65 -0
- data/lib/tweetwine/http.rb +120 -0
- data/lib/tweetwine/oauth.rb +104 -0
- data/lib/tweetwine/obfuscate.rb +21 -0
- data/lib/tweetwine/option_parser.rb +31 -0
- data/lib/tweetwine/promise.rb +39 -0
- data/lib/tweetwine/twitter.rb +211 -0
- data/lib/tweetwine/{io.rb → ui.rb} +30 -21
- data/lib/tweetwine/url_shortener.rb +15 -9
- data/lib/tweetwine/util.rb +30 -15
- data/lib/tweetwine.rb +72 -12
- data/man/tweetwine.7 +43 -69
- data/man/tweetwine.7.ronn +57 -47
- data/test/character_encoding_test.rb +87 -0
- data/test/cli_test.rb +19 -6
- data/test/config_test.rb +244 -0
- data/test/fixture/oauth.rb +21 -0
- data/test/fixture/test_config.yaml +4 -4
- data/test/http_test.rb +199 -0
- data/test/oauth_test.rb +77 -0
- data/test/obfuscate_test.rb +16 -0
- data/test/option_parser_test.rb +60 -0
- data/test/promise_test.rb +56 -0
- data/test/test_helper.rb +76 -8
- data/test/twitter_test.rb +625 -0
- data/test/{io_test.rb → ui_test.rb} +92 -74
- data/test/url_shortener_test.rb +115 -135
- data/test/util_test.rb +136 -85
- data/tweetwine.gemspec +53 -0
- metadata +112 -56
- data/example/show_metadata_example.rb +0 -86
- data/lib/tweetwine/client.rb +0 -187
- data/lib/tweetwine/meta.rb +0 -5
- data/lib/tweetwine/options.rb +0 -24
- data/lib/tweetwine/retrying_http.rb +0 -99
- data/lib/tweetwine/startup_config.rb +0 -50
- data/man/tweetwine.1 +0 -109
- data/man/tweetwine.1.ronn +0 -69
- data/test/client_test.rb +0 -544
- data/test/options_test.rb +0 -45
- data/test/retrying_http_test.rb +0 -147
- 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
|
-
|
8
|
-
|
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
|
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
|
-
|
15
|
-
|
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
|
data/test/config_test.rb
ADDED
@@ -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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
defopt:
|
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
|
data/test/oauth_test.rb
ADDED
@@ -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
|