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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +28 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +28 -0
- data/LICENSE.md +20 -0
- data/README.md +317 -0
- data/Rakefile +1 -0
- data/contributors.yaml +8 -0
- data/hurley.gemspec +29 -0
- data/lib/hurley.rb +104 -0
- data/lib/hurley/addressable.rb +9 -0
- data/lib/hurley/client.rb +349 -0
- data/lib/hurley/connection.rb +123 -0
- data/lib/hurley/header.rb +144 -0
- data/lib/hurley/multipart.rb +235 -0
- data/lib/hurley/options.rb +142 -0
- data/lib/hurley/query.rb +252 -0
- data/lib/hurley/tasks.rb +111 -0
- data/lib/hurley/test.rb +101 -0
- data/lib/hurley/test/integration.rb +249 -0
- data/lib/hurley/test/server.rb +102 -0
- data/lib/hurley/url.rb +197 -0
- data/script/bootstrap +2 -0
- data/script/package +7 -0
- data/script/test +168 -0
- data/test/client_test.rb +585 -0
- data/test/header_test.rb +108 -0
- data/test/helper.rb +14 -0
- data/test/live/net_http_test.rb +16 -0
- data/test/multipart_test.rb +306 -0
- data/test/query_test.rb +189 -0
- data/test/test_test.rb +38 -0
- data/test/url_test.rb +443 -0
- metadata +181 -0
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
data/script/package
ADDED
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
|
data/test/client_test.rb
ADDED
@@ -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
|