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.
@@ -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
- before do
6
- @dry = Class.new do
7
- attr_accessor :status
8
- def call env
9
- yield(env.merge(RC::RESPONSE_STATUS => status,
10
- RC::RESPONSE_HEADERS => {'LOCATION' => 'location'}))
11
- end
12
- end.new
13
- @app = RC::FollowRedirect.new(dry, 1)
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
@@ -3,9 +3,7 @@ require 'rest-core/test'
3
3
 
4
4
  describe RC::JsonResponse do
5
5
  describe 'app' do
6
- def app
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
- def client
27
- @client ||= RC::Builder.client do
28
- use RC::JsonResponse, true
29
- run Class.new{
30
- def call env
31
- yield(env.merge(RC::RESPONSE_BODY => '{}'))
32
- end
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
@@ -2,92 +2,92 @@
2
2
  require 'rest-core/test'
3
3
 
4
4
  describe RC::Oauth1Header do
5
- before do
6
- @env = {RC::REQUEST_METHOD => :post,
7
- RC::REQUEST_PATH =>
8
- 'https://api.twitter.com/oauth/request_token'}
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
- callback =
11
+ callback =
11
12
  'http://localhost:3005/the_dance/process_callback?service_provider_id=11'
12
13
 
13
- @oauth_params =
14
- {'oauth_callback' => callback ,
15
- 'oauth_consumer_key' => 'GDdmIQH6jhtmLUypg82g' ,
16
- 'oauth_nonce' => 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk',
17
- 'oauth_timestamp' => '1272323042' ,
18
- 'oauth_version' => '1.0' ,
19
- 'oauth_signature_method' => 'HMAC-SHA1'}
20
-
21
- @auth = RC::Oauth1Header.new(RC::Dry.new,
22
- nil, nil, nil,
23
- 'GDdmIQH6jhtmLUypg82g',
24
- 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98')
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
- @auth.signature(@env, @oauth_params).should.eq(
28
+ auth.signature(env, oauth_params).should.eq(
29
29
  '8wUi7m5HFQy76nowoCThusfgB+Q=')
30
30
  end
31
31
 
32
32
  describe 'base_string' do
33
- before do
34
- @base_string =
35
- 'POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Frequest_token&' \
36
- 'oauth_callback%3Dhttp%253A%252F%252Flocalhost%253A3005%252F' \
37
- 'the_dance%252Fprocess_callback%253Fservice_provider_id%253D' \
38
- '11%26oauth_consumer_key%3DGDdmIQH6jhtmLUypg82g%26oauth_nonc' \
39
- 'e%3DQP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk%26oauth_sig' \
40
- 'nature_method%3DHMAC-SHA1%26oauth_timestamp%3D1272323042%26' \
41
- 'oauth_version%3D1.0'
42
- end
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
- @env.merge!(RC::REQUEST_PAYLOAD => {'file' => File.open(__FILE__)})
54
- check
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
- @env.merge!(RC::REQUEST_PAYLOAD => File.open(__FILE__))
59
- check
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
- @env.merge!(RC::REQUEST_PAYLOAD => [File.open(__FILE__)])
64
- check
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
- @env.merge!(RC::REQUEST_PAYLOAD => {'pay' => 'load'},
69
- RC::REQUEST_HEADERS => {'Content-Type' => 'text/plain'})
70
- check
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
- @base_string << '%26pay%3Dload'
75
- @env.merge!(RC::REQUEST_PAYLOAD => {'pay' => 'load'},
76
- RC::REQUEST_HEADERS =>
77
- {'Content-Type' => 'application/x-www-form-urlencoded'})
78
- check
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
- @base_string << '%26pay%3Dload'
83
- @env.merge!(RC::REQUEST_PAYLOAD => {'pay' => 'load'})
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
- @base_string << '%26tilde%3D~'
89
- @env.merge!(RC::REQUEST_PAYLOAD => {'tilde' => '~'})
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::DefaultPayload do
5
- app = RC::DefaultPayload.new(RC::Dry.new, {})
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
- should 'do nothing' do
8
- app.call({}){ |r| r[RC::REQUEST_PAYLOAD].should.eq({}) }
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
- should 'merge payload' do
12
- app.instance_eval{@payload = {'pay' => 'load'}}
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
- app.call({}){ |r| r.should.eq({RC::REQUEST_PAYLOAD =>
15
- {'pay' => 'load'}}) }
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
- format = {'format' => 'json'}
18
- env = {RC::REQUEST_PAYLOAD => format}
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
- app.call(env){ |r| r.should.eq({RC::REQUEST_PAYLOAD =>
21
- {'pay' => 'load'}.merge(format)})}
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
- should 'accept non-hash payload' do
25
- u = RC::Universal.new(:log_method => false)
26
- env = {RC::REQUEST_PAYLOAD => 'payload'}
27
- u.request_full(env, u.dry)[RC::REQUEST_PAYLOAD].should.eq('payload')
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
- u.payload = 'default'
30
- u.request_full(env, u.dry)[RC::REQUEST_PAYLOAD].should.eq('payload')
31
- u.request_full({} , u.dry)[RC::REQUEST_PAYLOAD].should.eq('default')
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
- u = RC::Builder.client{use RC::DefaultPayload, 'maylord'}.new
34
- u.request_full({} , u.dry)[RC::REQUEST_PAYLOAD].should.eq('maylord')
35
- u.request_full(env, u.dry)[RC::REQUEST_PAYLOAD].should.eq('payload')
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