rest-core 2.0.4 → 2.1.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.
@@ -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