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/test.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module Hurley
|
2
|
+
class Test
|
3
|
+
def initialize
|
4
|
+
@handlers = []
|
5
|
+
yield self if block_given?
|
6
|
+
end
|
7
|
+
|
8
|
+
def head(url)
|
9
|
+
handle(:head, url, &Proc.new)
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(url)
|
13
|
+
handle(:get, url, &Proc.new)
|
14
|
+
end
|
15
|
+
|
16
|
+
def put(url)
|
17
|
+
handle(:put, url, &Proc.new)
|
18
|
+
end
|
19
|
+
|
20
|
+
def post(url)
|
21
|
+
handle(:post, url, &Proc.new)
|
22
|
+
end
|
23
|
+
|
24
|
+
def patch(url)
|
25
|
+
handle(:patch, url, &Proc.new)
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(url)
|
29
|
+
handle(:delete, url, &Proc.new)
|
30
|
+
end
|
31
|
+
|
32
|
+
def options(url)
|
33
|
+
handle(:options, url, &Proc.new)
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle(verb, url)
|
37
|
+
@handlers << Handler.new(Request.new(verb, Url.parse(url)), Proc.new)
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(request)
|
41
|
+
handler = @handlers.detect { |h| h.matches?(request) } ||
|
42
|
+
Handler.method(:not_found)
|
43
|
+
# Create a new url with fresh state from the url string
|
44
|
+
request.url = Url.parse(request.url.to_s)
|
45
|
+
handler.call(request)
|
46
|
+
end
|
47
|
+
|
48
|
+
def all_run?
|
49
|
+
@handlers.all?(&:run?)
|
50
|
+
end
|
51
|
+
|
52
|
+
class Handler
|
53
|
+
attr_reader :request
|
54
|
+
attr_reader :callback
|
55
|
+
|
56
|
+
def self.not_found(request)
|
57
|
+
Response.new(request, 404, Header.new) do |res|
|
58
|
+
res.receive_body("no test handler")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize(request, callback)
|
63
|
+
@request = request
|
64
|
+
@callback = callback
|
65
|
+
@path_regex = %r{\A#{@request.url.path}(/|\z)}
|
66
|
+
end
|
67
|
+
|
68
|
+
def call(request)
|
69
|
+
@run = true
|
70
|
+
status, header, body = @callback.call(request)
|
71
|
+
Response.new(request, status, Header.new(header)) do |res|
|
72
|
+
Array(body).each do |chunk|
|
73
|
+
res.receive_body(chunk)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def matches?(request)
|
79
|
+
return false unless @request.verb == request.verb
|
80
|
+
|
81
|
+
handler_url = @request.url
|
82
|
+
request_url = request.url
|
83
|
+
|
84
|
+
URL_ATTRS.each do |attr|
|
85
|
+
value = handler_url.send(attr)
|
86
|
+
return false if value && value != request_url.send(attr)
|
87
|
+
end
|
88
|
+
|
89
|
+
handler_url.query.subset_of?(request_url.query) &&
|
90
|
+
(handler_url.path =~ EMPTY_OR_SLASH || request_url.path =~ @path_regex)
|
91
|
+
end
|
92
|
+
|
93
|
+
def run?
|
94
|
+
!!@run
|
95
|
+
end
|
96
|
+
|
97
|
+
EMPTY_OR_SLASH = %r{\A/?\z}
|
98
|
+
URL_ATTRS = [:scheme, :host, :port]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
module Hurley
|
2
|
+
class Test
|
3
|
+
module Integration
|
4
|
+
class << self
|
5
|
+
attr_writer :live_endpoint
|
6
|
+
attr_writer :ssl_file
|
7
|
+
|
8
|
+
def live_endpoint
|
9
|
+
@live_endpoint ||= ENV["HURLEY_LIVE"].to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def ssl_file
|
13
|
+
@ssl_file ||= ENV["HURLEY_SSL_FILE"].to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def ssl?
|
17
|
+
live_endpoint.start_with?(Hurley::HTTPS)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.apply(base, *extra_features)
|
22
|
+
features = [:Common, *extra_features]
|
23
|
+
features << :SSL if Integration.ssl?
|
24
|
+
features.each do |name|
|
25
|
+
base.send(:include, const_get(name))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Common
|
30
|
+
def test_GET_retrieves_the_response_body
|
31
|
+
assert_equal "get", client.get("echo").body
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_GET_send_url_encoded_params
|
35
|
+
assert_equal %(get ?{"name"=>"zack"}), client.get("echo", :name => :zack).body
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_GET_retrieves_the_response_headers
|
39
|
+
response = client.get("echo")
|
40
|
+
assert_match(/text\/plain/, response.header["Content-Type"])
|
41
|
+
assert_match(/text\/plain/, response.header["content-type"])
|
42
|
+
assert_match(/text\/plain/, response.header[:content_type])
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_GET_sends_user_agent
|
46
|
+
assert_equal Hurley::USER_AGENT, client.get("echo_header", :name => :user_agent).body
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_GET_ssl
|
50
|
+
expected = Integration.ssl?.to_s
|
51
|
+
assert_equal expected, client.get("ssl").body
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_POST_send_url_encoded_params
|
55
|
+
res = client.post "echo" do |req|
|
56
|
+
req.body = "name=zack"
|
57
|
+
req.header[:content_type] = "application/x-www-form-urlencoded"
|
58
|
+
end
|
59
|
+
assert_equal %(post {"name"=>"zack"}), res.body
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_POST_send_url_encoded_nested_params
|
63
|
+
res = client.post "echo" do |req|
|
64
|
+
req.body = "name[first]=zack"
|
65
|
+
req.header[:content_type] = "application/x-www-form-urlencoded"
|
66
|
+
end
|
67
|
+
assert_equal %(post {"name"=>{"first"=>"zack"}}), res.body
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_POST_retrieves_the_response_headers
|
71
|
+
assert_match(/text\/plain/, client.post("echo").header[:content_type])
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_POST_sends_files_as_multipart
|
75
|
+
resp = client.post("file") do |req|
|
76
|
+
ctype, body = Query::Flat.new(
|
77
|
+
:uploaded_file => UploadIO.new(__FILE__, "text/x-ruby"),
|
78
|
+
).to_form
|
79
|
+
req.header[:content_type] = ctype
|
80
|
+
req.body = body
|
81
|
+
end
|
82
|
+
assert_equal "file integration.rb text/x-ruby #{File.size(__FILE__)}", resp.body
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_PUT_sends_files
|
86
|
+
resp = client.put("raw") do |req|
|
87
|
+
req.header[:content_length] = File.size(__FILE__)
|
88
|
+
req.body = File.open(__FILE__)
|
89
|
+
end
|
90
|
+
assert_equal "raw application/octet-stream #{File.size(__FILE__)} #{File.size(__FILE__)}", resp.body
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_PUT_sends_io
|
94
|
+
resp = client.put("raw") do |req|
|
95
|
+
req.body = GenericIO.new("READ")
|
96
|
+
req.header[:content_length] = 4
|
97
|
+
end
|
98
|
+
assert_equal "raw application/octet-stream 4 4", resp.body
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_PUT_sends_io_with_chunked_encoding
|
102
|
+
resp = client.put("raw") do |req|
|
103
|
+
req.body = GenericIO.new("READ")
|
104
|
+
end
|
105
|
+
assert_equal "raw application/octet-stream -1 4", resp.body
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_PUT_send_url_encoded_params
|
109
|
+
res = client.put "echo" do |req|
|
110
|
+
req.body = "name=zack"
|
111
|
+
req.header[:content_type] = "application/x-www-form-urlencoded"
|
112
|
+
end
|
113
|
+
assert_equal %(put {"name"=>"zack"}), res.body
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_PUT_send_url_encoded_nested_params
|
117
|
+
res = client.put "echo" do |req|
|
118
|
+
req.body = "name[first]=zack"
|
119
|
+
req.header[:content_type] = "application/x-www-form-urlencoded"
|
120
|
+
end
|
121
|
+
assert_equal %(put {"name"=>{"first"=>"zack"}}), res.body
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_PUT_retrieves_the_response_headers
|
125
|
+
assert_match(/text\/plain/, client.put("echo").header[:content_type])
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_PATCH_send_url_encoded_params
|
129
|
+
res = client.patch "echo" do |req|
|
130
|
+
req.header[:content_type] = "application/x-www-form-urlencoded"
|
131
|
+
req.body = "name=zack"
|
132
|
+
end
|
133
|
+
assert_equal %(patch {"name"=>"zack"}), res.body
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_OPTIONS
|
137
|
+
assert_equal "options", client.options("echo").body
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_HEAD_retrieves_no_response_body
|
141
|
+
assert_nil client.head("echo").body
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_HEAD_retrieves_the_response_headers
|
145
|
+
assert_match(/text\/plain/, client.head("echo").header[:content_type])
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_DELETE_retrieves_the_response_headers
|
149
|
+
assert_match(/text\/plain/, client.delete("echo").header[:content_type])
|
150
|
+
end
|
151
|
+
|
152
|
+
def test_DELETE_retrieves_the_body
|
153
|
+
assert_equal %(delete), client.delete("echo").body
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_timeout
|
157
|
+
client.request_options.timeout = 0.5
|
158
|
+
assert_raises Hurley::Timeout do
|
159
|
+
client.get "/slow"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_connection_error
|
164
|
+
assert_raises Hurley::ConnectionFailed do
|
165
|
+
client.get "http://localhost:4"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_empty_body_response_represented_as_nil
|
170
|
+
res = client.get("204")
|
171
|
+
assert_equal 204, res.status_code
|
172
|
+
assert_nil res.body
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_proxy
|
176
|
+
return unless client.request_options.proxy = proxy_url
|
177
|
+
|
178
|
+
res = client.get("/echo")
|
179
|
+
assert_equal "get", res.body
|
180
|
+
|
181
|
+
unless Integration.ssl?
|
182
|
+
# proxy can't append "Via" header for HTTPS responses
|
183
|
+
assert_match(/:#{proxy_url.port}$/, res.header[:Via])
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def test_proxy_auth_fail
|
188
|
+
return unless client.request_options.proxy = proxy_url
|
189
|
+
client.request_options.proxy.password = "WRONG"
|
190
|
+
assert_equal 407, client.put("/echo").status_code
|
191
|
+
rescue Hurley::ConnectionFailed
|
192
|
+
# this exception is allowed too
|
193
|
+
end
|
194
|
+
|
195
|
+
def client
|
196
|
+
@client ||= Client.new(Integration.live_endpoint) do |cli|
|
197
|
+
cli.header["X-Hurley-Connection"] = connection.class.name
|
198
|
+
cli.connection = connection
|
199
|
+
|
200
|
+
if Integration.ssl?
|
201
|
+
cli.ssl_options.ca_file = Integration.ssl_file
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def proxy_url
|
207
|
+
@proxy_url ||= if raw_url = ENV["HURLEY_PROXY"]
|
208
|
+
Url.parse(raw_url)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
module Compression
|
214
|
+
def test_GET_handles_compression
|
215
|
+
res = client.get("echo_header", :name => :accept_encoding)
|
216
|
+
assert_match(/gzip;.+\bdeflate\b/, res.body)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
module SSL
|
221
|
+
def test_GET_ssl_fails_with_bad_cert
|
222
|
+
client.ssl_options.ca_file = "tmp/hurley-different-ca-cert.crt"
|
223
|
+
|
224
|
+
err = assert_raises Hurley::SSLError do
|
225
|
+
client.get("/ssl")
|
226
|
+
end
|
227
|
+
|
228
|
+
assert_includes err.message, "certificate"
|
229
|
+
end
|
230
|
+
|
231
|
+
def test_GET_ssl_skips_verification
|
232
|
+
client.ssl_options.ca_file = "tmp/hurley-different-ca-cert.crt"
|
233
|
+
client.ssl_options.skip_verification = true
|
234
|
+
assert_equal "true", client.get("/ssl").body
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
class GenericIO
|
239
|
+
def initialize(str)
|
240
|
+
@io = StringIO.new(str)
|
241
|
+
end
|
242
|
+
|
243
|
+
def read(*args)
|
244
|
+
@io.read(*args)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
|
3
|
+
module Hurley
|
4
|
+
module Live
|
5
|
+
class Server < Sinatra::Base
|
6
|
+
set :environment, :test
|
7
|
+
disable :logging
|
8
|
+
disable :protection
|
9
|
+
|
10
|
+
[:get, :post, :put, :patch, :delete, :options].each do |method|
|
11
|
+
send(method, "/echo") do
|
12
|
+
out = [request.request_method.downcase]
|
13
|
+
|
14
|
+
if request.GET.any?
|
15
|
+
out << "?#{request.GET.inspect}"
|
16
|
+
end
|
17
|
+
|
18
|
+
if request.POST.any?
|
19
|
+
out << request.POST.inspect
|
20
|
+
end
|
21
|
+
|
22
|
+
content_type "text/plain"
|
23
|
+
out.join(" ")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
get "/echo_header" do
|
28
|
+
header = "HTTP_#{params[:name].tr("-", "_").upcase}"
|
29
|
+
request.env.fetch(header) { "NONE" }
|
30
|
+
end
|
31
|
+
|
32
|
+
post "/file" do
|
33
|
+
if params[:uploaded_file].respond_to? :each_key
|
34
|
+
"file %s %s %d" % [
|
35
|
+
params[:uploaded_file][:filename],
|
36
|
+
params[:uploaded_file][:type],
|
37
|
+
params[:uploaded_file][:tempfile].size
|
38
|
+
]
|
39
|
+
else
|
40
|
+
status 400
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
put "/raw" do
|
45
|
+
body = env["rack.input"]
|
46
|
+
"raw #{request.env.fetch("CONTENT_TYPE") { "NONE" }} #{request.env.fetch("CONTENT_LENGTH") { -1 }} #{body.size}"
|
47
|
+
end
|
48
|
+
|
49
|
+
get "/ssl" do
|
50
|
+
request.secure?.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
get "/slow" do
|
54
|
+
sleep 1
|
55
|
+
[200, {}, "ok"]
|
56
|
+
end
|
57
|
+
|
58
|
+
get "/204" do
|
59
|
+
status 204 # no content
|
60
|
+
end
|
61
|
+
|
62
|
+
error do |e|
|
63
|
+
"#{e.class}\n#{e.to_s}\n#{e.backtrace.join("\n")}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.start_server(options = nil)
|
68
|
+
require "webrick"
|
69
|
+
|
70
|
+
options ||= {}
|
71
|
+
port = options[:port] || 4000
|
72
|
+
|
73
|
+
log_io = $stdout
|
74
|
+
log_io.sync = true
|
75
|
+
|
76
|
+
webrick_opts = {
|
77
|
+
:Port => port, :Logger => WEBrick::Log::new(log_io),
|
78
|
+
:AccessLog => [[log_io, "[%{X-Hurley-Connection}i] %m %U -> %s %b"]],
|
79
|
+
}
|
80
|
+
|
81
|
+
if options[:ssl_key]
|
82
|
+
require "openssl"
|
83
|
+
require "webrick/https"
|
84
|
+
webrick_opts.update(
|
85
|
+
:SSLEnable => true,
|
86
|
+
:SSLPrivateKey => OpenSSL::PKey::RSA.new(File.read(options[:ssl_key])),
|
87
|
+
:SSLCertificate => OpenSSL::X509::Certificate.new(File.read(options[:ssl_file])),
|
88
|
+
:SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE,
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
Rack::Handler::WEBrick.run(Server, webrick_opts) do |server|
|
93
|
+
trap(:INT) { server.stop }
|
94
|
+
trap(:TERM) { server.stop }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
if $0 == __FILE__
|
101
|
+
Hurley::Server.run!
|
102
|
+
end
|