rest-core 2.0.4 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +33 -0
- data/Gemfile +1 -1
- data/README.md +1 -1
- data/Rakefile +13 -0
- data/lib/rest-core.rb +2 -0
- data/lib/rest-core/client.rb +22 -13
- data/lib/rest-core/engine/em-http-request.rb +37 -8
- data/lib/rest-core/engine/rest-client.rb +6 -4
- data/lib/rest-core/middleware.rb +24 -3
- data/lib/rest-core/middleware/auth_basic.rb +1 -1
- data/lib/rest-core/middleware/cache.rb +2 -2
- data/lib/rest-core/middleware/default_headers.rb +1 -1
- data/lib/rest-core/middleware/default_payload.rb +1 -1
- data/lib/rest-core/middleware/default_query.rb +1 -1
- data/lib/rest-core/middleware/oauth1_header.rb +3 -13
- data/lib/rest-core/middleware/oauth2_header.rb +1 -1
- data/lib/rest-core/middleware/oauth2_query.rb +1 -1
- data/lib/rest-core/test.rb +48 -0
- data/lib/rest-core/util/payload.rb +162 -0
- data/lib/rest-core/version.rb +1 -1
- data/rest-core.gemspec +14 -7
- data/task/gemgem.rb +7 -6
- data/test/test_auth_basic.rb +7 -5
- data/test/test_cache.rb +6 -0
- data/test/test_client_oauth1.rb +22 -22
- data/test/test_default_payload.rb +38 -0
- data/test/test_default_query.rb +10 -8
- data/test/{test_em_http_request.rb → test_em-http-request.rb} +40 -0
- data/test/test_follow_redirect.rb +9 -12
- data/test/test_json_response.rb +8 -12
- data/test/test_oauth1_header.rb +53 -53
- data/test/test_payload.rb +189 -22
- data/test/test_rest-client.rb +40 -0
- data/test/test_timeout.rb +8 -10
- metadata +25 -9
@@ -18,6 +18,46 @@ describe RC::EmHttpRequest do
|
|
18
18
|
}.resume}
|
19
19
|
end
|
20
20
|
|
21
|
+
describe 'POST Payload' do
|
22
|
+
after do
|
23
|
+
WebMock.reset!
|
24
|
+
end
|
25
|
+
|
26
|
+
client = RC::Builder.client
|
27
|
+
client.builder.run(RC::EmHttpRequest)
|
28
|
+
path = 'http://example.com'
|
29
|
+
ok = 'OK'
|
30
|
+
c = client.new
|
31
|
+
|
32
|
+
post = lambda do |payload, body|
|
33
|
+
stub_request(:post, path).with(:body => body).to_return(:body => ok)
|
34
|
+
EM.error_handler{ |e| e.should.kind_of?(NilClass); EM.stop }
|
35
|
+
EM.run{ EM.defer{
|
36
|
+
c.post(path, payload).should.eq ok
|
37
|
+
EM.next_tick{ EM.stop }
|
38
|
+
}}
|
39
|
+
end
|
40
|
+
|
41
|
+
should 'post with string' do
|
42
|
+
post['string', 'string']
|
43
|
+
end
|
44
|
+
|
45
|
+
should 'post with file' do
|
46
|
+
File.open(__FILE__) do |f|
|
47
|
+
b = f.read
|
48
|
+
f.rewind
|
49
|
+
post[f, b]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'post with socket' do
|
54
|
+
rd, wr = IO.pipe
|
55
|
+
wr.write('socket')
|
56
|
+
wr.close
|
57
|
+
post[rd, 'socket']
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
21
61
|
# ----------------------------------------------------------------------
|
22
62
|
|
23
63
|
describe RC::Simple do
|
@@ -2,21 +2,18 @@
|
|
2
2
|
require 'rest-core/test'
|
3
3
|
|
4
4
|
describe RC::FollowRedirect do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
5
|
+
dry = Class.new do
|
6
|
+
attr_accessor :status
|
7
|
+
def call env
|
8
|
+
yield(env.merge(RC::RESPONSE_STATUS => status,
|
9
|
+
RC::RESPONSE_HEADERS => {'LOCATION' => 'location'}))
|
10
|
+
end
|
11
|
+
end.new
|
12
|
+
app = RC::FollowRedirect.new(dry, 1)
|
13
|
+
|
15
14
|
after do
|
16
15
|
RR.verify
|
17
16
|
end
|
18
|
-
def dry; @dry; end
|
19
|
-
def app; @app; end
|
20
17
|
|
21
18
|
[301, 302, 303, 307].each do |status|
|
22
19
|
should "not follow redirect if reached max_redirects: #{status}" do
|
data/test/test_json_response.rb
CHANGED
@@ -3,9 +3,7 @@ require 'rest-core/test'
|
|
3
3
|
|
4
4
|
describe RC::JsonResponse do
|
5
5
|
describe 'app' do
|
6
|
-
|
7
|
-
@app ||= RC::JsonResponse.new(RC::Dry.new, true)
|
8
|
-
end
|
6
|
+
app = RC::JsonResponse.new(RC::Dry.new, true)
|
9
7
|
|
10
8
|
should 'do nothing' do
|
11
9
|
expected = {RC::RESPONSE_BODY => nil,
|
@@ -23,15 +21,13 @@ describe RC::JsonResponse do
|
|
23
21
|
end
|
24
22
|
|
25
23
|
describe 'client' do
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
}
|
34
|
-
end
|
24
|
+
client = RC::Builder.client do
|
25
|
+
use RC::JsonResponse, true
|
26
|
+
run Class.new{
|
27
|
+
def call env
|
28
|
+
yield(env.merge(RC::RESPONSE_BODY => '{}'))
|
29
|
+
end
|
30
|
+
}
|
35
31
|
end
|
36
32
|
|
37
33
|
should 'do nothing' do
|
data/test/test_oauth1_header.rb
CHANGED
@@ -2,92 +2,92 @@
|
|
2
2
|
require 'rest-core/test'
|
3
3
|
|
4
4
|
describe RC::Oauth1Header do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
env = {RC::REQUEST_METHOD => :post,
|
6
|
+
RC::REQUEST_PATH =>
|
7
|
+
'https://api.twitter.com/oauth/request_token',
|
8
|
+
RC::REQUEST_QUERY => {},
|
9
|
+
RC::REQUEST_PAYLOAD => {}}
|
9
10
|
|
10
|
-
|
11
|
+
callback =
|
11
12
|
'http://localhost:3005/the_dance/process_callback?service_provider_id=11'
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
14
|
+
oauth_params =
|
15
|
+
{'oauth_callback' => callback ,
|
16
|
+
'oauth_consumer_key' => 'GDdmIQH6jhtmLUypg82g' ,
|
17
|
+
'oauth_nonce' => 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk',
|
18
|
+
'oauth_timestamp' => '1272323042' ,
|
19
|
+
'oauth_version' => '1.0' ,
|
20
|
+
'oauth_signature_method' => 'HMAC-SHA1'}
|
21
|
+
|
22
|
+
auth = RC::Oauth1Header.new(RC::Dry.new,
|
23
|
+
nil, nil, nil,
|
24
|
+
'GDdmIQH6jhtmLUypg82g',
|
25
|
+
'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98')
|
26
26
|
|
27
27
|
should 'have correct signature' do
|
28
|
-
|
28
|
+
auth.signature(env, oauth_params).should.eq(
|
29
29
|
'8wUi7m5HFQy76nowoCThusfgB+Q=')
|
30
30
|
end
|
31
31
|
|
32
32
|
describe 'base_string' do
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
def check
|
45
|
-
@auth.base_string(@env, @oauth_params).should.eq @base_string
|
33
|
+
base_string =
|
34
|
+
'POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Frequest_token&' \
|
35
|
+
'oauth_callback%3Dhttp%253A%252F%252Flocalhost%253A3005%252F' \
|
36
|
+
'the_dance%252Fprocess_callback%253Fservice_provider_id%253D' \
|
37
|
+
'11%26oauth_consumer_key%3DGDdmIQH6jhtmLUypg82g%26oauth_nonc' \
|
38
|
+
'e%3DQP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk%26oauth_sig' \
|
39
|
+
'nature_method%3DHMAC-SHA1%26oauth_timestamp%3D1272323042%26' \
|
40
|
+
'oauth_version%3D1.0'
|
41
|
+
|
42
|
+
check = lambda do |e, b|
|
43
|
+
auth.base_string(e, oauth_params).should.eq b
|
46
44
|
end
|
47
45
|
|
48
46
|
should 'have correct base_string' do
|
49
|
-
check
|
47
|
+
check[env, base_string]
|
50
48
|
end
|
51
49
|
|
52
50
|
should 'not use payload in multipart request for base_string' do
|
53
|
-
|
54
|
-
|
51
|
+
File.open(__FILE__) do |f|
|
52
|
+
check[env.merge(RC::REQUEST_PAYLOAD => {'file' => f}), base_string]
|
53
|
+
end
|
55
54
|
end
|
56
55
|
|
57
56
|
should 'not use payload if it contains binary' do
|
58
|
-
|
59
|
-
|
57
|
+
File.open(__FILE__) do |f|
|
58
|
+
check[env.merge(RC::REQUEST_PAYLOAD => f), base_string]
|
59
|
+
end
|
60
60
|
end
|
61
61
|
|
62
62
|
should 'not use payload if it contains [binary]' do
|
63
|
-
|
64
|
-
|
63
|
+
File.open(__FILE__) do |f|
|
64
|
+
check[env.merge(RC::REQUEST_PAYLOAD => [f]), base_string]
|
65
|
+
end
|
65
66
|
end
|
66
67
|
|
67
68
|
should 'not use payload if Content-Type is not x-www-form-urlencoded' do
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
check[
|
70
|
+
env.merge(RC::REQUEST_PAYLOAD => {'pay' => 'load'},
|
71
|
+
RC::REQUEST_HEADERS => {'Content-Type' => 'text/plain'}),
|
72
|
+
base_string]
|
71
73
|
end
|
72
74
|
|
73
75
|
should 'use payload if Content-Type is x-www-form-urlencoded' do
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
76
|
+
check[env.merge(
|
77
|
+
RC::REQUEST_PAYLOAD => {'pay' => 'load'},
|
78
|
+
RC::REQUEST_HEADERS =>
|
79
|
+
{'Content-Type' => 'application/x-www-form-urlencoded'}),
|
80
|
+
base_string + '%26pay%3Dload']
|
79
81
|
end
|
80
82
|
|
81
83
|
should 'use payload if there is no binary data' do
|
82
|
-
|
83
|
-
|
84
|
-
check
|
84
|
+
check[env.merge(RC::REQUEST_PAYLOAD => {'pay' => 'load'}),
|
85
|
+
base_string + '%26pay%3Dload']
|
85
86
|
end
|
86
87
|
|
87
88
|
should 'not escape ~' do
|
88
|
-
|
89
|
-
|
90
|
-
check
|
89
|
+
check[env.merge(RC::REQUEST_PAYLOAD => {'tilde' => '~'}),
|
90
|
+
base_string + '%26tilde%3D~']
|
91
91
|
end
|
92
92
|
end
|
93
93
|
end
|
data/test/test_payload.rb
CHANGED
@@ -1,37 +1,204 @@
|
|
1
1
|
|
2
2
|
require 'rest-core/test'
|
3
3
|
|
4
|
-
describe RC::
|
5
|
-
|
4
|
+
describe RC::Payload do
|
5
|
+
describe 'A regular Payload' do
|
6
|
+
should 'use standard enctype as default content-type' do
|
7
|
+
RC::Payload::UrlEncoded.new({}).headers['Content-Type'].
|
8
|
+
should.eq 'application/x-www-form-urlencoded'
|
9
|
+
end
|
6
10
|
|
7
|
-
|
8
|
-
|
11
|
+
should 'form properly encoded params' do
|
12
|
+
RC::Payload::UrlEncoded.new(:foo => 'bar').read.
|
13
|
+
should.eq 'foo=bar'
|
14
|
+
RC::Payload::UrlEncoded.new(:foo => 'bar', :baz => 'qux').read.
|
15
|
+
should.eq 'baz=qux&foo=bar'
|
16
|
+
end
|
17
|
+
|
18
|
+
should 'escape parameters' do
|
19
|
+
RC::Payload::UrlEncoded.new('foo ' => 'bar').read.
|
20
|
+
should.eq 'foo%20=bar'
|
21
|
+
end
|
22
|
+
|
23
|
+
should 'properly handle arrays as repeated parameters' do
|
24
|
+
RC::Payload::UrlEncoded.new(:foo => ['bar']).read.
|
25
|
+
should.eq 'foo=bar'
|
26
|
+
RC::Payload::UrlEncoded.new(:foo => ['bar', 'baz']).read.
|
27
|
+
should.eq 'foo=bar&foo=baz'
|
28
|
+
end
|
29
|
+
|
30
|
+
should 'not close if stream already closed' do
|
31
|
+
p = RC::Payload::UrlEncoded.new('foo ' => 'bar')
|
32
|
+
p.close
|
33
|
+
2.times{ p.close.should.eq nil }
|
34
|
+
end
|
9
35
|
end
|
10
36
|
|
11
|
-
|
12
|
-
|
37
|
+
describe 'A multipart Payload' do
|
38
|
+
should 'use standard enctype as default content-type' do
|
39
|
+
p = RC::Payload::Multipart.new({})
|
40
|
+
stub(p).boundary{123}
|
41
|
+
p.headers['Content-Type'].should.eq 'multipart/form-data; boundary=123'
|
42
|
+
end
|
43
|
+
|
44
|
+
should 'not error on close if stream already closed' do
|
45
|
+
p = RC::Payload::Multipart.new(:file => File.open(__FILE__))
|
46
|
+
p.close
|
47
|
+
2.times{ p.close.should.eq nil }
|
48
|
+
end
|
13
49
|
|
14
|
-
|
15
|
-
|
50
|
+
should 'form properly separated multipart data' do
|
51
|
+
p = RC::Payload::Multipart.new(:bar => 'baz', :foo => 'bar')
|
52
|
+
p.read.should.eq <<-EOS
|
53
|
+
--#{p.boundary}\r
|
54
|
+
Content-Disposition: form-data; name="bar"\r
|
55
|
+
\r
|
56
|
+
baz\r
|
57
|
+
--#{p.boundary}\r
|
58
|
+
Content-Disposition: form-data; name="foo"\r
|
59
|
+
\r
|
60
|
+
bar\r
|
61
|
+
--#{p.boundary}--\r
|
62
|
+
EOS
|
63
|
+
end
|
16
64
|
|
17
|
-
|
18
|
-
|
65
|
+
should 'form multiple files with the same name' do
|
66
|
+
with_img do |f, n|
|
67
|
+
with_img do |ff, nn|
|
68
|
+
p = RC::Payload::Multipart.new(:foo => [f, ff])
|
69
|
+
p.read.should.eq <<-EOS
|
70
|
+
--#{p.boundary}\r
|
71
|
+
Content-Disposition: form-data; name="foo"; filename="#{n}"\r
|
72
|
+
Content-Type: image/jpeg\r
|
73
|
+
\r
|
74
|
+
#{'a'*10}\r
|
75
|
+
--#{p.boundary}\r
|
76
|
+
Content-Disposition: form-data; name="foo"; filename="#{nn}"\r
|
77
|
+
Content-Type: image/jpeg\r
|
78
|
+
\r
|
79
|
+
#{'a'*10}\r
|
80
|
+
--#{p.boundary}--\r
|
81
|
+
EOS
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
19
85
|
|
20
|
-
|
21
|
-
|
86
|
+
should 'not escape parameters names' do
|
87
|
+
p = RC::Payload::Multipart.new('bar ' => 'baz')
|
88
|
+
p.read.should.eq <<-EOS
|
89
|
+
--#{p.boundary}\r
|
90
|
+
Content-Disposition: form-data; name="bar "\r
|
91
|
+
\r
|
92
|
+
baz\r
|
93
|
+
--#{p.boundary}--\r
|
94
|
+
EOS
|
95
|
+
end
|
96
|
+
|
97
|
+
should 'form properly separated multipart data' do
|
98
|
+
with_img do |f, n|
|
99
|
+
p = RC::Payload::Multipart.new(:foo => f)
|
100
|
+
p.read.should.eq <<-EOS
|
101
|
+
--#{p.boundary}\r
|
102
|
+
Content-Disposition: form-data; name="foo"; filename="#{n}"\r
|
103
|
+
Content-Type: image/jpeg\r
|
104
|
+
\r
|
105
|
+
#{File.read(f.path)}\r
|
106
|
+
--#{p.boundary}--\r
|
107
|
+
EOS
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
should "ignore the name attribute when it's not set" do
|
112
|
+
with_img do |f, n|
|
113
|
+
p = RC::Payload::Multipart.new(nil => f)
|
114
|
+
p.read.should.eq <<-EOS
|
115
|
+
--#{p.boundary}\r
|
116
|
+
Content-Disposition: form-data; filename="#{n}"\r
|
117
|
+
Content-Type: image/jpeg\r
|
118
|
+
\r
|
119
|
+
#{File.read(f.path)}\r
|
120
|
+
--#{p.boundary}--\r
|
121
|
+
EOS
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
should 'detect optional (original) content type and filename' do
|
126
|
+
File.open(__FILE__) do |f|
|
127
|
+
def f.content_type ; 'image/jpeg'; end
|
128
|
+
def f.original_filename; 'foo.txt' ; end
|
129
|
+
p = RC::Payload::Multipart.new(:foo => f)
|
130
|
+
p.read.should.eq <<-EOS
|
131
|
+
--#{p.boundary}\r
|
132
|
+
Content-Disposition: form-data; name="foo"; filename="foo.txt"\r
|
133
|
+
Content-Type: image/jpeg\r
|
134
|
+
\r
|
135
|
+
#{File.read(f.path)}\r
|
136
|
+
--#{p.boundary}--\r
|
137
|
+
EOS
|
138
|
+
end
|
139
|
+
end
|
22
140
|
end
|
23
141
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
142
|
+
describe 'streamed payloads' do
|
143
|
+
should 'properly determine the size of file payloads' do
|
144
|
+
File.open(__FILE__) do |f|
|
145
|
+
p = RC::Payload.generate(f)
|
146
|
+
p.size.should.eq f.stat.size
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
should 'properly determine the size of other kinds of payloads' do
|
151
|
+
s = StringIO.new('foo')
|
152
|
+
p = RC::Payload.generate(s)
|
153
|
+
p.size.should.eq 3
|
154
|
+
|
155
|
+
begin
|
156
|
+
f = Tempfile.new('rest-core')
|
157
|
+
f.write('foo bar')
|
158
|
+
f.rewind
|
159
|
+
|
160
|
+
p = RC::Payload.generate(f)
|
161
|
+
p.size.should.eq 7
|
162
|
+
ensure
|
163
|
+
f.close!
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe 'Payload generation' do
|
169
|
+
should 'recognize standard urlencoded params' do
|
170
|
+
RC::Payload.generate('foo' => 'bar').should.
|
171
|
+
kind_of?(RC::Payload::UrlEncoded)
|
172
|
+
end
|
173
|
+
|
174
|
+
should 'recognize multipart params' do
|
175
|
+
File.open(__FILE__) do |f|
|
176
|
+
RC::Payload.generate('foo' => f).should.
|
177
|
+
kind_of?(RC::Payload::Multipart)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
should 'return data if none of the above' do
|
182
|
+
RC::Payload.generate('data').should.
|
183
|
+
kind_of?(RC::Payload::StreamedString)
|
184
|
+
end
|
185
|
+
|
186
|
+
should 'recognize nested multipart payloads in arrays' do
|
187
|
+
File.open(__FILE__) do |f|
|
188
|
+
RC::Payload.generate('foo' => [f]).should.
|
189
|
+
kind_of?(RC::Payload::Multipart)
|
190
|
+
end
|
191
|
+
end
|
28
192
|
|
29
|
-
|
30
|
-
|
31
|
-
|
193
|
+
should 'recognize file payloads that can be streamed' do
|
194
|
+
File.open(__FILE__) do |f|
|
195
|
+
RC::Payload.generate(f).should.kind_of?(RC::Payload::Streamed)
|
196
|
+
end
|
197
|
+
end
|
32
198
|
|
33
|
-
|
34
|
-
|
35
|
-
|
199
|
+
should 'recognize other payloads that can be streamed' do
|
200
|
+
RC::Payload.generate(StringIO.new('foo')).should.
|
201
|
+
kind_of?(RC::Payload::Streamed)
|
202
|
+
end
|
36
203
|
end
|
37
204
|
end
|