hurley 0.1

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/lib/hurley/url.rb ADDED
@@ -0,0 +1,197 @@
1
+ require "base64"
2
+ require "erb"
3
+ require "forwardable"
4
+ require "set"
5
+ require "uri"
6
+
7
+ module Hurley
8
+ class Url
9
+ def self.escape_path(path)
10
+ ERB::Util.url_encode(path.to_s)
11
+ end
12
+
13
+ def self.escape_paths(*paths)
14
+ paths.map do |path|
15
+ escape_path(path)
16
+ end.join(SLASH)
17
+ end
18
+
19
+ def self.parse(raw_url)
20
+ case raw_url
21
+ when Url then raw_url
22
+ when nil, EMPTY then Empty.new
23
+ else new(@@parser.call(raw_url.to_s))
24
+ end
25
+ end
26
+
27
+ def initialize(parsed)
28
+ @parsed = parsed
29
+ if u = @parsed.user
30
+ @user = CGI.unescape(u)
31
+ @parsed.user = nil
32
+ else
33
+ @user = nil
34
+ end
35
+
36
+ if pwd = @parsed.password
37
+ @password = CGI.unescape(pwd)
38
+ @parsed.password = nil
39
+ else
40
+ @password = nil
41
+ end
42
+
43
+ @parsed.userinfo = nil
44
+ end
45
+
46
+ def self.join(absolute, relative)
47
+ parse(absolute).join(parse(relative))
48
+ end
49
+
50
+ extend Forwardable
51
+ def_delegators(:@parsed,
52
+ :scheme, :scheme=,
53
+ :host, :host=,
54
+ :port=,
55
+ )
56
+
57
+ attr_accessor :user
58
+ attr_accessor :password
59
+
60
+ def port
61
+ @parsed.port || INFERRED_PORTS[@parsed.scheme]
62
+ end
63
+
64
+ def path
65
+ @parsed.path
66
+ end
67
+
68
+ def path=(new_path)
69
+ @parsed.path = new_path
70
+ end
71
+
72
+ def query
73
+ @query ||= query_class.parse(@parsed.query)
74
+ end
75
+
76
+ def join(relative)
77
+ has_host = false
78
+
79
+ query.each do |key, value|
80
+ relative.query[key] = value unless relative.query.key?(key)
81
+ end
82
+
83
+ if !path.empty? && !relative.path.start_with?(SLASH)
84
+ rel_path = relative.path
85
+ relative.path = path
86
+ if !rel_path.empty?
87
+ joiner = path.end_with?(SLASH) ? nil : SLASH
88
+ relative.path += "#{joiner}#{rel_path}"
89
+ end
90
+ end
91
+
92
+ if !relative.path.empty? && !relative.path.start_with?(SLASH)
93
+ relative.path = "/#{relative.path}"
94
+ end
95
+
96
+ if relative.host
97
+ has_host = true
98
+ else
99
+ relative.host = host
100
+ end
101
+
102
+ if has_host && relative.host != host
103
+ relative.user = relative.password = nil
104
+ else
105
+ relative.user ||= user
106
+ relative.password ||= password
107
+ end
108
+
109
+ if relative.scheme
110
+ has_host = true
111
+ else
112
+ relative.scheme = scheme
113
+ end
114
+
115
+ inferred_port = INFERRED_PORTS[relative.scheme]
116
+ if !has_host && relative.port == inferred_port
117
+ relative.port = port == inferred_port ? nil : port
118
+ end
119
+
120
+ relative
121
+ end
122
+
123
+ def request_uri
124
+ req_path = path
125
+ req_path = SLASH if req_path.empty?
126
+
127
+ if (q = query.to_query_string).empty?
128
+ req_path
129
+ else
130
+ "#{req_path}?#{q}"
131
+ end
132
+ end
133
+
134
+ def to_s
135
+ @parsed.query = raw_query
136
+ @parsed.to_s
137
+ end
138
+
139
+ def raw_query
140
+ if (q = query.to_query_string).empty?
141
+ nil
142
+ else
143
+ q
144
+ end
145
+ end
146
+
147
+ def raw_query=(new_query)
148
+ @query = nil
149
+ @parsed.query = new_query
150
+ end
151
+
152
+ def basic_auth
153
+ return unless @user
154
+ userinfo = @password ? "#{@user}:#{@password}" : @user
155
+ "Basic #{Base64.encode64(userinfo).rstrip}"
156
+ end
157
+
158
+ def query_class
159
+ @query_class ||= Query.default
160
+ end
161
+
162
+ def query_class=(new_query)
163
+ @query = query ? new_query.new(@query) : nil
164
+ @query_class = new_query
165
+ end
166
+
167
+ def inspect
168
+ "#<%s %s>" % [
169
+ self.class.name,
170
+ to_s,
171
+ ]
172
+ end
173
+
174
+ private
175
+
176
+ @@parser = URI.method(:parse)
177
+
178
+ EMPTY = "".freeze
179
+ SLASH = "/".freeze
180
+
181
+ INFERRED_PORTS = {
182
+ "https" => 443,
183
+ "http" => 80,
184
+ }.freeze
185
+
186
+ class Empty < self
187
+ def initialize
188
+ @parsed = @@parser.call(EMPTY)
189
+ @query = Query.parse(EMPTY)
190
+ end
191
+
192
+ def relation_with(url)
193
+ :diff
194
+ end
195
+ end
196
+ end
197
+ end
data/script/bootstrap ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ bundle install --quiet "$@"
data/script/package ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: script/package
3
+ # Updates the gemspec and builds a new gem in the pkg directory.
4
+
5
+ mkdir -p pkg
6
+ gem build *.gemspec
7
+ mv *.gem pkg
data/script/test ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: script/test [file] [adapter]... -- [test/unit options]
3
+ # Runs the test suite against a local server spawned automatically in a
4
+ # thread. After tests are done, the server is shut down.
5
+ #
6
+ # If filename arguments are given, only those files are run. If arguments given
7
+ # are not filenames, they are taken as words that filter the list of files to run.
8
+ #
9
+ # Examples:
10
+ #
11
+ # $ script/test
12
+ # $ script/test test/env_test.rb
13
+ # $ script/test excon typhoeus
14
+ #
15
+ # # Run only tests matching /ssl/ for the net_http adapter, with SSL enabled.
16
+ # $ HURLEY_SSL=1 script/test net_http -- -n /ssl/
17
+ #
18
+ # # Run against multiple rbenv versions
19
+ # $ RBENV_VERSIONS="1.9.3-p194 ree-1.8.7-2012.02" script/test
20
+ set -e
21
+
22
+ port=3999
23
+ proxy_port=3998
24
+ scheme=http
25
+
26
+ if [ "$HURLEY_SSL" = "1" ] || [ "$HURLEY_SSL" = "yes" ]; then
27
+ scheme=https
28
+ if [ -z "$HURLEY_SSL_KEY" ] || [ -z "$HURLEY_SSL_FILE" ]; then
29
+ eval "$(rake hurley:generate_certs IN_SHELL=1)"
30
+ fi
31
+ fi
32
+
33
+ find_test_files() {
34
+ find "$1" -name '*_test.rb'
35
+ }
36
+
37
+ filter_matching() {
38
+ pattern="$1"
39
+ shift
40
+ for line in "$@"; do
41
+ [[ $line == *"$pattern"* ]] && echo "$line"
42
+ done
43
+ }
44
+
45
+ start_server() {
46
+ mkdir -p log
47
+ rake hurley:start_server HURLEY_PORT=$port >log/test.log 2>&1 &
48
+ echo $!
49
+ }
50
+
51
+ start_proxy() {
52
+ mkdir -p log
53
+ rake hurley:start_proxy HURLEY_PORT=$proxy_port "HURLEY_PROXY_AUTH=hurley@test.local:there is cake" >log/proxy.log 2>&1 &
54
+ echo $!
55
+ }
56
+
57
+ server_started() {
58
+ lsof -i :${1?} >/dev/null
59
+ }
60
+
61
+ timestamp() {
62
+ date +%s
63
+ }
64
+
65
+ wait_for_server() {
66
+ timeout=$(( `timestamp` + $1 ))
67
+ while true; do
68
+ if server_started "$2"; then
69
+ break
70
+ elif [ `timestamp` -gt "$timeout" ]; then
71
+ echo "timed out after $1 seconds" >&2
72
+ return 1
73
+ fi
74
+ done
75
+ }
76
+
77
+ filtered=
78
+ IFS=$'\n' test_files=($(find_test_files "test"))
79
+ declare -a explicit_files
80
+
81
+ # Process filter arguments:
82
+ # - test filenames as taken as-is
83
+ # - other words are taken as pattern to match the list of known files against
84
+ # - arguments after "--" are forwarded to the ruby process
85
+ while [ $# -gt 0 ]; do
86
+ arg="$1"
87
+ shift
88
+ if [ "$arg" = "--" ]; then
89
+ break
90
+ elif [ -f "$arg" ]; then
91
+ filtered=true
92
+ explicit_files[${#explicit_files[@]}+1]="$arg"
93
+ else
94
+ filtered=true
95
+ IFS=$'\n' explicit_files=(
96
+ ${explicit_files[@]}
97
+ $(filter_matching "$arg" "${test_files[@]}" || true)
98
+ )
99
+ fi
100
+ done
101
+
102
+ # If there were filter args, replace test files list with the results
103
+ if [ -n "$filtered" ]; then
104
+ if [ ${#explicit_files[@]} -eq 0 ]; then
105
+ echo "Error: no test files match" >&2
106
+ exit 1
107
+ else
108
+ test_files=(${explicit_files[@]})
109
+ echo running "${test_files[@]}"
110
+ fi
111
+ fi
112
+
113
+ # If there are live tests, spin up the HTTP server
114
+ if [ -n "$(filter_matching "live" "${test_files[@]}")" ]; then
115
+ if server_started $port; then
116
+ echo "aborted: another instance of server running on $port" >&2
117
+ exit 1
118
+ fi
119
+ server_pid=$(start_server)
120
+ proxy_pid=$(start_proxy)
121
+ wait_for_server 30 $port || {
122
+ cat log/test.log
123
+ kill "$server_pid"
124
+ kill "$proxy_pid"
125
+ exit 1
126
+ }
127
+ wait_for_server 5 $proxy_port
128
+ cleanup() {
129
+ if [ $? -ne 0 ] && [ -n "$TRAVIS" ]; then
130
+ cat log/test.log
131
+ fi
132
+ kill "$server_pid"
133
+ kill "$proxy_pid"
134
+ }
135
+ trap cleanup INT EXIT
136
+ export HURLEY_LIVE="${scheme}://localhost:${port}"
137
+ export HURLEY_PROXY="http://hurley%40test.local:there%20is%20cake@localhost:${proxy_port}"
138
+ fi
139
+
140
+ warnings="${TMPDIR:-/tmp}/hurley-warnings.$$"
141
+
142
+ run_test_files() {
143
+ # Save warnings on stderr to a separate file
144
+ RUBYOPT="$RUBYOPT -w" ruby -e 'while f=ARGV.shift and f!="--"; load f; end' "${test_files[@]}" -- "$@" \
145
+ 2> >(tee >(grep 'warning:' >"$warnings") | grep -v 'warning:')
146
+ }
147
+
148
+ check_warnings() {
149
+ # Display Ruby warnings from this project's source files. Abort if any were found.
150
+ num="$(grep -F "$PWD" "$warnings" | grep -v "${PWD}/bundle" | sort | uniq -c | sort -rn | tee /dev/stderr | wc -l)"
151
+ rm -f "$warnings"
152
+ if [ "$num" -gt 0 ]; then
153
+ echo "FAILED: this test suite doesn't tolerate Ruby syntax warnings!" >&2
154
+ exit 1
155
+ fi
156
+ }
157
+
158
+ if [ -n "$RBENV_VERSIONS" ]; then
159
+ IFS=' ' versions=($RBENV_VERSIONS)
160
+ for version in "${versions[@]}"; do
161
+ echo "[${version}]"
162
+ RBENV_VERSION="$version" run_test_files "$@"
163
+ done
164
+ else
165
+ run_test_files "$@"
166
+ fi
167
+
168
+ check_warnings
@@ -0,0 +1,585 @@
1
+ require File.expand_path("../helper", __FILE__)
2
+
3
+ module Hurley
4
+ class ClientTest < TestCase
5
+ def test_integration_verbs
6
+ verbs = [:head, :get, :put, :post, :delete, :options]
7
+ client = Client.new "https://example.com"
8
+ client.connection = Test.new do |t|
9
+ verbs.each do |verb|
10
+ t.handle(verb, "/a") do |req|
11
+ [200, {}, verb.inspect]
12
+ end
13
+ end
14
+ end
15
+
16
+ errors = []
17
+
18
+ verbs.each do |verb|
19
+ res = client.send(verb, "/a")
20
+ if res.body != verb.inspect
21
+ errors << "#{verb} = #{res.status_code} / #{res.body}"
22
+ end
23
+ end
24
+
25
+ if errors.any?
26
+ fail "\n" + errors.join("\n")
27
+ end
28
+ end
29
+
30
+ def test_integration_before_callback
31
+ c = Client.new "https://example.com"
32
+ c.connection = Test.new do |test|
33
+ test.post "/a" do |req|
34
+ assert_equal "BOOYA", req.body
35
+ [200, {}, "meh"]
36
+ end
37
+ end
38
+
39
+ c.before_call do |req|
40
+ req.body = req.body.to_s.upcase
41
+ end
42
+
43
+ res = c.post "a" do |req|
44
+ req.body = "booya"
45
+ end
46
+
47
+ assert_equal 200, res.status_code
48
+ assert c.connection.all_run?
49
+ end
50
+
51
+ def test_integration_after_callback
52
+ c = Client.new "https://example.com"
53
+ c.connection = Test.new do |test|
54
+ test.get "/a" do |req|
55
+ [200, {}, "meh"]
56
+ end
57
+ end
58
+
59
+ c.after_call do |res|
60
+ res.body = res.body.to_s.upcase
61
+ end
62
+
63
+ res = c.get "a"
64
+
65
+ assert_equal 200, res.status_code
66
+ assert_equal "MEH", res.body
67
+ assert c.connection.all_run?
68
+ end
69
+
70
+ def test_integration_default_headers
71
+ headers = [:content_type, :content_length, :transfer_encoding]
72
+ c = Client.new "https://example.com"
73
+ c.connection = Test.new do |test|
74
+ [:get, :put, :post, :patch, :options, :delete].each do |verb|
75
+ test.send(verb, "/a") do |req|
76
+ body = if !req.body
77
+ :-
78
+ elsif req.body.respond_to?(:path)
79
+ req.body.path
80
+ else
81
+ req.body_io.read
82
+ end
83
+ [200, {}, headers.map { |h| req.header[h] || "NONE" }.join(",") + " #{body}"]
84
+ end
85
+ end
86
+ end
87
+
88
+ errors = []
89
+ tests = {}
90
+ file_size = File.size(__FILE__)
91
+
92
+ # IO-like object without #size/#length
93
+ fake_reader = Object.new
94
+ def fake_reader.read(*args)
95
+ "READ"
96
+ end
97
+
98
+ # test defaults with non-body requests
99
+ [:get, :patch, :options, :delete].each do |verb|
100
+ tests.update(
101
+ lambda {
102
+ c.send(verb, "a")
103
+ } => "NONE,NONE,NONE -",
104
+
105
+ lambda {
106
+ c.send(verb, "a") { |r| r.body ="ABC" }
107
+ } => "application/octet-stream,3,NONE ABC",
108
+
109
+ lambda {
110
+ c.send(verb, "a") do |r|
111
+ r.header[:content_type] = "text/plain"
112
+ r.body = "ABC"
113
+ end
114
+ } => "text/plain,3,NONE ABC",
115
+ )
116
+ end
117
+
118
+ # these http verbs need a body
119
+ [:post, :put].each do |verb|
120
+ tests.update(
121
+ # RAW BODY TESTS
122
+
123
+ lambda {
124
+ c.send(verb, "a")
125
+ } => "NONE,0,NONE -",
126
+
127
+ lambda {
128
+ c.send(verb, "a") do |r|
129
+ r.body = "abc"
130
+ end
131
+ } => "application/octet-stream,3,NONE abc",
132
+
133
+ lambda {
134
+ c.send(verb, "a") do |r|
135
+ r.header[:content_type] = "text/plain"
136
+ r.body = "abc"
137
+ end
138
+ } => "text/plain,3,NONE abc",
139
+
140
+ # FILE TESTS
141
+
142
+ lambda {
143
+ c.send(verb, "a") do |r|
144
+ r.body = File.new(__FILE__)
145
+ end
146
+ } => "application/octet-stream,#{file_size},NONE #{__FILE__}",
147
+
148
+ lambda {
149
+ c.send(verb, "a") do |r|
150
+ r.header[:content_type] = "text/plain"
151
+ r.body = File.new(__FILE__)
152
+ end
153
+ } => "text/plain,#{file_size},NONE #{__FILE__}",
154
+
155
+ # GENERIC IO TESTS
156
+
157
+ lambda {
158
+ c.send(verb, "a") do |r|
159
+ r.body = fake_reader
160
+ end
161
+ } => "application/octet-stream,NONE,chunked READ",
162
+
163
+ lambda {
164
+ c.send(verb, "a") do |r|
165
+ r.header[:content_type] = "text/plain"
166
+ r.body = fake_reader
167
+ end
168
+ } => "text/plain,NONE,chunked READ",
169
+
170
+ lambda {
171
+ c.send(verb, "a") do |r|
172
+ r.header[:content_length] = 4
173
+ r.body = fake_reader
174
+ end
175
+ } => "application/octet-stream,4,NONE READ",
176
+
177
+ lambda {
178
+ c.send(verb, "a") do |r|
179
+ r.header[:content_length] = 4
180
+ r.header[:content_type] = "text/plain"
181
+ r.body = fake_reader
182
+ end
183
+ } => "text/plain,4,NONE READ",
184
+ )
185
+ end
186
+
187
+ tests.each do |req_block, expected|
188
+ res = req_block.call
189
+ req = res.request
190
+ if expected != res.body
191
+ errors << "#{req.inspect} Expected #{expected.inspect}; Got #{res.body.inspect}"
192
+ end
193
+ end
194
+
195
+ if errors.any?
196
+ fail "\n" + errors.join("\n")
197
+ end
198
+ end
199
+
200
+ def test_integration_with_query
201
+ c = Client.new "https://example.com"
202
+ c.connection = Test.new do |test|
203
+ [:get, :options, :delete].each do |verb|
204
+ test.send(verb, "/a") do |req|
205
+ [200, {}, req.url.to_s]
206
+ end
207
+ end
208
+ end
209
+
210
+ errors = []
211
+ prefix = "https://example.com/a"
212
+
213
+ {
214
+ nil => prefix,
215
+ {:foo => :bar} => "#{prefix}?foo=bar",
216
+ }.each do |input, expected|
217
+ [:get, :options, :delete].each do |verb|
218
+ res = c.send(verb, "a", input)
219
+ if res.body != expected
220
+ errors << "#{res.request.url.inspect} => #{expected.inspect} != #{res.body.inspect}"
221
+ end
222
+ end
223
+ end
224
+
225
+ if errors.any?
226
+ fail "\n" + errors.join("\n")
227
+ end
228
+ end
229
+
230
+ def test_integration_with_body
231
+ c = Client.new "https://example.com"
232
+ c.connection = Test.new do |test|
233
+ [:post, :put, :patch].each do |verb|
234
+ test.send(verb, "/form") do |req|
235
+ [200, {}, "#{req.header[:content_type]}:#{req.body_io.read}"]
236
+ end
237
+ end
238
+
239
+ [:post, :put, :patch].each do |verb|
240
+ test.send(verb, "/multipart") do |req|
241
+ m = Rack::Multipart.parse_multipart(
242
+ "CONTENT_TYPE" => req.header[:content_type],
243
+ "CONTENT_LENGTH" => req.header[:content_length],
244
+ "rack.input" => req.body_io,
245
+ )
246
+ [200, {}, "#{req.header[:content_type]}:#{Array(m["a"]).join(",")}:#{m["h"].inspect}:#{m["file"][:tempfile].read}"]
247
+ end
248
+ end
249
+ end
250
+
251
+ errors = []
252
+
253
+ flat_query = Query::Flat.new :a => [1,2]
254
+ nested_query = Query::Nested.new :a => [1,2]
255
+
256
+ {
257
+ ["abc"] => "application/octet-stream:abc",
258
+ ["abc", "text/plain"] => "text/plain:abc",
259
+ [{:a => 1}] => "application/x-www-form-urlencoded:a=1",
260
+ [flat_query] => "application/x-www-form-urlencoded:a=1&a=2",
261
+ [nested_query] => "application/x-www-form-urlencoded:a%5B%5D=1&a%5B%5D=2",
262
+ [flat_query, :form] => "form:a=1&a=2",
263
+ [nested_query, :form] => "form:a%5B%5D=1&a%5B%5D=2",
264
+ }.each do |args, expected|
265
+ [:post, :put, :patch].each do |verb|
266
+ res = c.send(verb, "form", *args)
267
+ if res.body != expected
268
+ errors << "#{verb} => #{expected.inspect} != #{res.body.inspect}"
269
+ end
270
+ end
271
+ end
272
+
273
+ multipart_tests = {}
274
+ [:post, :put, :patch].each do |verb|
275
+ nested_query = Query::Nested.new(:file => UploadIO.new(StringIO.new("ABC"), "text/plain"), :a => [1,2], :h => {:a => 1})
276
+ flat_query = Query::Flat.new(:file => UploadIO.new(StringIO.new("ABC"), "text/plain"), :a => [3,4], :h => 0)
277
+ hash_query = {:file => UploadIO.new(StringIO.new("ABC"), "text/plain"), :a => [5,6], :h => {:a => 1}}
278
+ multipart_tests.update(
279
+ c.send(verb, "multipart", nested_query) => %(:1,2:{"a"=>"1"}:ABC),
280
+ c.send(verb, "multipart", flat_query) => %(:4:"0":ABC),
281
+ c.send(verb, "multipart", hash_query) => %(:5,6:{"a"=>"1"}:ABC),
282
+ )
283
+ end
284
+
285
+ multipart_tests.each do |res, expected|
286
+ if res.body !~ %r{\Amultipart/form-data; boundary=Hurley-(\w+):}
287
+ errors << "#{res.request.verb} multipart (#{expected[1..-1]}) bad type: #{res.body}"
288
+ end
289
+
290
+ if !res.body.end_with?(expected)
291
+ errors << "#{res.request.verb} multipart (#{expected[1..-1]}) bad body: #{res.body}"
292
+ end
293
+ end
294
+
295
+ if errors.any?
296
+ fail "\n" + errors.join("\n")
297
+ end
298
+ end
299
+
300
+ def test_integration_follow_get_redirect
301
+ statuses = [301, 302, 303]
302
+
303
+ c = Client.new "http://example.com?o=1"
304
+ c.request_options.redirection_limit = 0
305
+ c.connection = Test.new do |t|
306
+ statuses.each do |st|
307
+ t.get "/#{st}/host/2" do |req|
308
+ [st, {"Location" => "http://example.com/#{st}/host/1"}, nil]
309
+ end
310
+
311
+ t.get "/#{st}/host/1" do |req|
312
+ [st, {"Location" => "http://example.com/#{st}/host/0"}, nil]
313
+ end
314
+
315
+ t.get "/#{st}/host/0" do |req|
316
+ [200, {}, "ok"]
317
+ end
318
+
319
+ t.post "/#{st}/host" do |req|
320
+ [st, {"Location" => "http://example.com/#{st}/host/2?o=2"}, nil]
321
+ end
322
+
323
+ t.get "/#{st}/path/2" do |req|
324
+ [st, {"Location" => "/#{st}/path/1"}, nil]
325
+ end
326
+
327
+ t.get "/#{st}/path/1" do |req|
328
+ [st, {"Location" => "/#{st}/path/0"}, nil]
329
+ end
330
+
331
+ t.get "/#{st}/path/0" do |req|
332
+ [200, {}, "ok"]
333
+ end
334
+
335
+ t.post "/#{st}/path" do |req|
336
+ [st, {"Location" => "2?o=2"}, nil]
337
+ end
338
+ end
339
+ end
340
+
341
+ statuses.each do |st|
342
+ {
343
+ "/#{st}/host" => "http://example.com/#{st}/host/",
344
+ "/#{st}/path" => "http://example.com/#{st}/path/",
345
+ }.each do |input, prefix|
346
+ res = c.post(input)
347
+ assert_equal st, res.status_code
348
+ assert_equal prefix + "2?o=2", res.location.url.to_s
349
+
350
+ res = c.call(res.location)
351
+ assert_equal st, res.status_code
352
+ assert_equal prefix + "1?o=2", res.location.url.to_s
353
+
354
+ res = c.call(res.location)
355
+ assert_equal st, res.status_code
356
+ assert_equal prefix + "0?o=2", res.location.url.to_s
357
+
358
+ res = c.call(res.location)
359
+ assert_equal 200, res.status_code
360
+ end
361
+ end
362
+ end
363
+
364
+ def test_integration_follow_post_redirect
365
+ statuses = [307, 308]
366
+
367
+ c = Client.new "http://example.com?o=1"
368
+ c.request_options.redirection_limit = 0
369
+ c.connection = Test.new do |t|
370
+ statuses.each do |st|
371
+ t.post "/#{st}/host/2" do |req|
372
+ [st, {"Location" => "http://example.com/#{st}/host/1"}, nil]
373
+ end
374
+
375
+ t.post "/#{st}/host/1" do |req|
376
+ [st, {"Location" => "http://example.com/#{st}/host/0"}, nil]
377
+ end
378
+
379
+ t.post "/#{st}/host/0" do |req|
380
+ [200, {}, "ok"]
381
+ end
382
+
383
+ t.post "/#{st}/host" do |req|
384
+ [st, {"Location" => "http://example.com/#{st}/host/2?o=2"}, nil]
385
+ end
386
+
387
+ t.post "/#{st}/path/2" do |req|
388
+ [st, {"Location" => "/#{st}/path/1"}, nil]
389
+ end
390
+
391
+ t.post "/#{st}/path/1" do |req|
392
+ [st, {"Location" => "/#{st}/path/0"}, nil]
393
+ end
394
+
395
+ t.post "/#{st}/path/0" do |req|
396
+ [200, {}, "ok"]
397
+ end
398
+
399
+ t.post "/#{st}/path" do |req|
400
+ [st, {"Location" => "2?o=2"}, nil]
401
+ end
402
+ end
403
+ end
404
+
405
+ statuses.each do |st|
406
+ {
407
+ "/#{st}/host" => "http://example.com/#{st}/host/",
408
+ "/#{st}/path" => "http://example.com/#{st}/path/",
409
+ }.each do |input, prefix|
410
+ res = c.post(input)
411
+ assert_equal st, res.status_code
412
+ assert_equal prefix + "2?o=2", res.location.url.to_s
413
+
414
+ res = c.call(res.location)
415
+ assert_equal st, res.status_code
416
+ assert_equal prefix + "1?o=2", res.location.url.to_s
417
+
418
+ res = c.call(res.location)
419
+ assert_equal st, res.status_code
420
+ assert_equal prefix + "0?o=2", res.location.url.to_s
421
+
422
+ res = c.call(res.location)
423
+ assert_equal 200, res.status_code
424
+ end
425
+ end
426
+ end
427
+
428
+ def test_integration_automatic_redirection
429
+ c = Client.new "https://example.com"
430
+ c.connection = Test.new do |t|
431
+ 1.upto(5) do |i|
432
+ t.get "/#{i}" do |req|
433
+ [301, {"Location" => "/#{i - 1}"}, i.to_s]
434
+ end
435
+ end
436
+
437
+ t.get "/0" do |req|
438
+ [200, {}, "ok"]
439
+ end
440
+ end
441
+
442
+ res = c.get("/5") { |r| r.options.redirection_limit = 10 }
443
+ assert_equal "ok", res.body
444
+ assert_equal [
445
+ "https://example.com/5",
446
+ "https://example.com/4",
447
+ "https://example.com/3",
448
+ "https://example.com/2",
449
+ "https://example.com/1",
450
+ ], res.via.map { |r| r.url.to_s }
451
+ assert_equal "https://example.com/0", res.request.url.to_s
452
+
453
+ res = c.get("/5") { |r| r.options.redirection_limit = 3 }
454
+ assert_equal "2", res.body
455
+ assert_equal [
456
+ "https://example.com/5",
457
+ "https://example.com/4",
458
+ "https://example.com/3",
459
+ ], res.via.map { |r| r.url.to_s }
460
+ assert_equal "https://example.com/2", res.request.url.to_s
461
+ end
462
+
463
+ def test_parses_endpoint
464
+ c = Client.new "https://example.com/a?a=1"
465
+ assert_equal "https", c.scheme
466
+ assert_equal "example.com", c.host
467
+ assert_equal "/a", c.url.path
468
+ end
469
+
470
+ def test_builds_request
471
+ c = Client.new "https://example.com/a?a=1"
472
+ c.header["Accept"] = "*"
473
+ c.request_options.bind = "bind:123"
474
+ c.ssl_options.openssl_client_cert = "abc"
475
+
476
+ req = c.request :get, "b"
477
+ assert_equal "bind", req.options.bind.host
478
+ assert_equal 123, req.options.bind.port
479
+ assert_equal "abc", req.ssl_options.openssl_client_cert
480
+ req.ssl_options.openssl_client_cert = "def"
481
+ req.options.bind = "updated"
482
+
483
+ assert_equal "*", req.header["Accept"]
484
+ assert_equal "def", req.ssl_options.openssl_client_cert
485
+ assert_equal "updated", req.options.bind.host
486
+ assert_nil req.options.bind.port
487
+
488
+ url = req.url
489
+ assert_equal "https://example.com/a/b?a=1", url.to_s
490
+
491
+ assert_equal "abc", c.ssl_options.openssl_client_cert
492
+ assert_equal "bind", c.request_options.bind.host
493
+ assert_equal 123, c.request_options.bind.port
494
+ end
495
+
496
+ def test_sets_before_callbacks
497
+ c = Client.new nil
498
+ c.before_call(:first) { |r| 1 }
499
+ c.before_call { |r| 2 }
500
+ c.before_call NamedCallback.new(:third, lambda { |r| 3 })
501
+
502
+ callbacks = c.before_callbacks
503
+ assert_equal 3, callbacks.size
504
+ assert_equal :first, callbacks[0]
505
+ assert callbacks[1].start_with?("#<Proc:")
506
+ assert_equal :third, callbacks[2]
507
+ end
508
+
509
+ def test_sets_after_callbacks
510
+ c = Client.new nil
511
+ c.after_call(:first) { |r| 1 }
512
+ c.after_call { |r| 2 }
513
+ c.after_call NamedCallback.new(:third, lambda { |r| 3 })
514
+
515
+ callbacks = c.after_callbacks
516
+ assert_equal 3, callbacks.size
517
+ assert_equal :first, callbacks[0]
518
+ assert callbacks[1].start_with?("#<Proc:")
519
+ assert_equal :third, callbacks[2]
520
+ end
521
+
522
+ SUCCESSFUL_RESPONSES = [200, 201, 202, 204, 205, 206]
523
+ REDIRECTION_RESPONSES = [301, 302, 303, 307, 308]
524
+ CLIENT_ERROR_RESPONSES = [400, 404, 405, 406, 409, 410, 422]
525
+ SERVER_ERROR_RESPONSES = [500, 502, 503, 504]
526
+ ALL_RESPONSES = SUCCESSFUL_RESPONSES + REDIRECTION_RESPONSES + CLIENT_ERROR_RESPONSES + SERVER_ERROR_RESPONSES + [100, 304]
527
+
528
+ def test_knows_successful_responses
529
+ bad = SUCCESSFUL_RESPONSES.reject do |st|
530
+ res = Response.new(nil, st)
531
+ res.success? && res.status_type == :success
532
+ end
533
+ assert_empty bad
534
+
535
+ bad = (ALL_RESPONSES - SUCCESSFUL_RESPONSES).reject do |st|
536
+ res = Response.new(nil, st)
537
+
538
+ !Response.new(nil, st).success?
539
+ end
540
+ assert_empty bad
541
+ end
542
+
543
+ def test_knows_redirection_responses
544
+ bad = REDIRECTION_RESPONSES.reject do |st|
545
+ res = Response.new(nil, st)
546
+ res.redirection? && res.status_type == :redirection
547
+ end
548
+ assert_empty bad
549
+
550
+ bad = (ALL_RESPONSES - REDIRECTION_RESPONSES).reject do |st|
551
+ res = Response.new(nil, st)
552
+ !res.redirection? && res.status_type != :redirection
553
+ end
554
+ assert_empty bad
555
+ end
556
+
557
+ def test_knows_client_error_responses
558
+ bad = CLIENT_ERROR_RESPONSES.reject do |st|
559
+ res = Response.new(nil, st)
560
+ res.client_error? && res.status_type == :client_error
561
+ end
562
+ assert_empty bad
563
+
564
+ bad = (ALL_RESPONSES - CLIENT_ERROR_RESPONSES).reject do |st|
565
+ res = Response.new(nil, st)
566
+ !res.client_error? && res.status_type != :client_error
567
+ end
568
+ assert_empty bad
569
+ end
570
+
571
+ def test_knows_server_error_responses
572
+ bad = SERVER_ERROR_RESPONSES.reject do |st|
573
+ res = Response.new(nil, st)
574
+ res.server_error? && res.status_type == :server_error
575
+ end
576
+ assert_empty bad
577
+
578
+ bad = (ALL_RESPONSES - SERVER_ERROR_RESPONSES).reject do |st|
579
+ res = Response.new(nil, st)
580
+ !res.server_error? && res.status_type != :server_error
581
+ end
582
+ assert_empty bad
583
+ end
584
+ end
585
+ end