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.
@@ -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