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.
- 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
|