kronk 1.8.7 → 1.9.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.
@@ -10,6 +10,7 @@ class Kronk
10
10
 
11
11
 
12
12
  ENCODING_MATCHER = /(^|;\s?)charset=(.*?)\s*(;|$)/
13
+ DEFAULT_ENCODING = "ASCII-8BIT"
13
14
 
14
15
  ##
15
16
  # Read http response from a file and return a Kronk::Response instance.
@@ -25,7 +26,7 @@ class Kronk
25
26
 
26
27
 
27
28
  attr_reader :code, :io, :cookies, :headers
28
- attr_accessor :read, :request, :stringify_opts, :time
29
+ attr_accessor :read, :request, :stringify_opts, :time, :conn_time
29
30
 
30
31
  ##
31
32
  # Create a new Response object from a String or IO.
@@ -129,6 +130,7 @@ class Kronk
129
130
 
130
131
  begin
131
132
  read_body do |chunk|
133
+ chunk = chunk.dup
132
134
  chunk = unzip chunk if gzip?
133
135
 
134
136
  try_force_encoding chunk
@@ -169,10 +171,10 @@ class Kronk
169
171
 
170
172
 
171
173
  ##
172
- # Size of the raw body in bytes.
174
+ # Current size of the http body in bytes.
173
175
 
174
176
  def bytes
175
- (headers["content-length"] || self.raw_body.bytes.count).to_i
177
+ self.raw_body.bytes.count
176
178
  end
177
179
 
178
180
 
@@ -232,6 +234,29 @@ class Kronk
232
234
  end
233
235
 
234
236
 
237
+ ##
238
+ # The extension or file type corresponding to the body,
239
+ # based on the Content-Type. Defaults to 'txt' if none can be determined
240
+ # or Content-Type is text/plain.
241
+ # application/json => 'json'
242
+ # text/html => 'html'
243
+ # application/foo+xml => 'xml'
244
+
245
+ def ext
246
+ file_ext =
247
+ if headers['content-type']
248
+ types = MIME::Types[headers['content-type'].sub(%r{/\w+\+}, '/')]
249
+ types[0].extensions[0] unless types.empty?
250
+
251
+ elsif uri
252
+ File.extname(uri.path)[1..-1]
253
+ end
254
+
255
+ file_ext = "txt" if !file_ext || file_ext.strip.empty?
256
+ file_ext
257
+ end
258
+
259
+
235
260
  ##
236
261
  # Cookie header accessor.
237
262
 
@@ -264,10 +289,9 @@ class Kronk
264
289
 
265
290
  def encoding
266
291
  return @encoding if @encoding
267
- @encoding = "utf-8" unless headers["content-type"]
268
- c_type = headers["content-type"] =~ ENCODING_MATCHER
292
+ c_type = headers["content-type"].to_s =~ ENCODING_MATCHER
269
293
  @encoding = $2 if c_type
270
- @encoding ||= "ASCII-8BIT"
294
+ @encoding ||= DEFAULT_ENCODING
271
295
  @encoding = Encoding.find(@encoding) if defined?(Encoding)
272
296
  @encoding
273
297
  end
@@ -309,8 +333,9 @@ class Kronk
309
333
  # Ruby inspect.
310
334
 
311
335
  def inspect
312
- content_type = headers['content-type'] || "text/html"
313
- "#<#{self.class}:#{@code} #{content_type} #{total_bytes}bytes>"
336
+ content_type = headers['content-type'] || "text/plain"
337
+ "#<#{self.class}:#{@code} #{content_type} \
338
+ #{total_bytes}/#{expected_bytes}bytes>"
314
339
  end
315
340
 
316
341
 
@@ -329,8 +354,8 @@ class Kronk
329
354
  # Check if connection should stay alive.
330
355
 
331
356
  def keep_alive?
332
- @headers['connection'].to_s.include?('keep-alive') ||
333
- @headers['proxy-connection'].to_s.include?('keep-alive')
357
+ @headers['connection'].to_s.include?('Keep-Alive') ||
358
+ @headers['proxy-connection'].to_s.include?('Keep-Alive')
334
359
  end
335
360
 
336
361
  alias connection_keep_alive? keep_alive?
@@ -406,7 +431,7 @@ class Kronk
406
431
  # The parser to use on the body.
407
432
 
408
433
  def parser
409
- @parser ||= Kronk.parser_for headers["content-type"]
434
+ @parser ||= Kronk.parser_for self.ext
410
435
  end
411
436
 
412
437
 
@@ -434,7 +459,7 @@ class Kronk
434
459
  # Returns the body portion of the raw http response.
435
460
 
436
461
  def raw_body
437
- headless? ? raw : raw.split("\r\n\r\n", 2)[1]
462
+ headless? ? @raw.to_s : @body.to_s
438
463
  end
439
464
 
440
465
 
@@ -554,12 +579,13 @@ class Kronk
554
579
  # :only_data:: String/Array - Extracts the data from given data paths
555
580
  #
556
581
  # Example:
557
- # response.data :transform => [:delete, ["foo/0", "bar/1"]]
582
+ # response.data :transform => [[:delete, ["foo/0", "bar/1"]]]
558
583
  # response.data do |trans|
559
584
  # trans.delete "foo/0", "bar/1"
560
585
  # end
561
586
  #
562
- # See Path::Transaction for supported transform actions.
587
+ # See Path::Transaction for supported transform actions in the
588
+ # {ruby-path gem}[http://github.com/yaksnrainbows/ruby-path].
563
589
 
564
590
  def data opts={}
565
591
  data = nil
@@ -672,6 +698,16 @@ class Kronk
672
698
  # Number of bytes of the response including the header.
673
699
 
674
700
  def total_bytes
701
+ return raw.bytes.count if @read
702
+ return raw_header.bytes.count unless body_permitted?
703
+ raw_header.to_s.bytes.count + bytes + 2
704
+ end
705
+
706
+
707
+ ##
708
+ # Expected number of bytes to read from the server, including the header.
709
+
710
+ def expected_bytes
675
711
  return raw.bytes.count if @read
676
712
  return raw_header.bytes.count unless body_permitted?
677
713
  raw_header.to_s.bytes.count + (content_length || range_length).to_i + 2
@@ -738,13 +774,14 @@ class Kronk
738
774
 
739
775
  buff_io.rewind
740
776
 
741
- ext = File === buff_io.io ?
742
- File.extname(buff_io.io.path)[1..-1] : "html"
777
+ ctype = ["text/plain"]
778
+ ctype = MIME::Types.of(buff_io.io.path).concat ctype if
779
+ File === buff_io.io
743
780
 
744
781
  encoding = buff_io.io.respond_to?(:external_encoding) ?
745
- buff_io.io.external_encoding : "UTF-8"
782
+ buff_io.io.external_encoding : DEFAULT_ENCODING
746
783
  @headers = {
747
- 'content-type' => "text/#{ext}; charset=#{encoding}",
784
+ 'content-type' => "#{ctype[0]}; charset=#{encoding}",
748
785
  }
749
786
 
750
787
  @headless = true
@@ -1,7 +1,7 @@
1
1
  HTTP/1.1 200 OK
2
2
  Server: nginx/0.6.39
3
3
  Date: Fri, 03 Dec 2010 21:49:00 GMT
4
- Content-Type: application/x-plist; charset=utf-8
4
+ Content-Type: application/foobar+x-plist; charset=utf-8
5
5
  Connection: keep-alive
6
6
  Keep-Alive: timeout=20
7
7
  Status: 200 OK
@@ -321,11 +321,11 @@ class TestCmd < Test::Unit::TestCase
321
321
  opts = Kronk::Cmd.parse_args %w{uri -p mock_file}
322
322
  assert_equal mock_file, opts[:player].input.io
323
323
 
324
- opts = Kronk::Cmd.parse_args %w{uri --benchmark mock_file}
324
+ opts = Kronk::Cmd.parse_args %w{uri --benchmark -p mock_file}
325
325
  assert_equal mock_file, opts[:player].input.io
326
326
  assert_equal Kronk::Player::Benchmark, opts[:player].class
327
327
 
328
- opts = Kronk::Cmd.parse_args %w{uri --stream mock_file}
328
+ opts = Kronk::Cmd.parse_args %w{uri --stream -p mock_file}
329
329
  assert_equal mock_file, opts[:player].input.io
330
330
  assert_equal Kronk::Player::Stream, opts[:player].class
331
331
  end
@@ -366,7 +366,8 @@ class TestCmd < Test::Unit::TestCase
366
366
 
367
367
  def test_parse_args_http_options
368
368
  opts = Kronk::Cmd.parse_args %w{uri -A foo -L --no-cookies -? bar
369
- --suff /tail -X PUT -x example.com:2000 --form foo=bar}
369
+ --suff /tail -X PUT -x example.com:2000 --form foo=bar
370
+ --form-upload file=test/mocks/200_response.json}
370
371
 
371
372
  assert_equal "foo", opts[:user_agent]
372
373
  assert_equal true, opts[:follow_redirects]
@@ -375,6 +376,7 @@ class TestCmd < Test::Unit::TestCase
375
376
  assert_equal "/tail", opts[:uri_suffix]
376
377
  assert_equal "PUT", opts[:http_method]
377
378
  assert_equal "foo=bar", opts[:form]
379
+ assert_equal "file=test/mocks/200_response.json", opts[:form_upload]
378
380
  assert_equal({:host => "example.com", :port => "2000"}, opts[:proxy])
379
381
 
380
382
  opts = Kronk::Cmd.parse_args %w{uri -L 3}
@@ -160,33 +160,43 @@ ensure
160
160
  end
161
161
 
162
162
 
163
- def expect_request req_method, url, options={}
163
+ def expect_request req_method, url, opts={}
164
164
  uri = URI.parse url
165
165
 
166
- resp = Kronk::Response.new(options[:returns] || mock_200_response)
167
- resp.stubs(:code).returns(options[:status] || '200')
166
+ resp = Kronk::Response.new(opts[:returns] || mock_200_response)
167
+ resp.stubs(:code).returns(opts[:status] || '200')
168
168
  resp.stubs(:to_hash).returns Hash.new
169
169
 
170
170
  http = mock 'http'
171
171
  req = mock 'req'
172
172
 
173
- data = options[:data]
173
+ data = opts[:data]
174
174
  data &&= Hash === data ? Kronk::Request.build_query(data) : data.to_s
175
175
 
176
- headers = options[:headers] || Hash.new
176
+ req.stubs(:body).returns(data)
177
+ req.stubs(:body_stream).returns(StringIO.new(data.to_s))
178
+
179
+ req.stubs(:[]).with('Content-Length').returns(data)
180
+ req.stubs(:[]=)
181
+
182
+ headers = opts[:headers] || Hash.new
177
183
  headers['User-Agent'] ||= Kronk::DEFAULT_USER_AGENT
184
+ headers['Connection'] ||= 'Keep-Alive'
178
185
 
179
- req.expects(:start).yields(http).returns resp
186
+ http.expects(:started?).returns false
187
+ http.expects(:start)
180
188
  req.expects(:body=).with(data)
181
189
 
182
190
  Kronk::Request::VanillaRequest.expects(:new).
183
191
  with(req_method.to_s.upcase, uri.request_uri, headers).returns req
184
192
 
185
- Kronk::HTTP.expects(:new).with(uri.host, uri.port).returns req
193
+ proxy = opts[:proxy] || {}
194
+
195
+ Kronk::HTTP.expects(:new).
196
+ with(uri.host, uri.port, {:proxy => proxy, :ssl => !!opts[:ssl]}).
197
+ returns http
186
198
 
187
- http.expects(:request).
188
- with(req, nil, has_entry(:request => instance_of(Kronk::Request))).
189
- returns resp
199
+ http.expects(:request).with(req, nil, {}).returns resp
190
200
 
191
201
  yield http, req, resp if block_given?
192
202
 
@@ -0,0 +1,144 @@
1
+ require 'test/test_helper'
2
+
3
+ class TestMultipart < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @multi = Kronk::Multipart.new "foobar"
7
+ end
8
+
9
+
10
+ def test_initialize
11
+ assert_equal "foobar", @multi.boundary
12
+ end
13
+
14
+
15
+ def test_add
16
+ @multi.add "foo", "bar"
17
+ expected = [{'content-disposition' => 'form-data; name="foo"'}, "bar"]
18
+ assert_equal expected, @multi.parts.last
19
+ end
20
+
21
+
22
+ def test_add_headers
23
+ @multi.add "foo", "bar", "X-Header" => "blah"
24
+ expected = [{'content-disposition' => 'form-data; name="foo"',
25
+ "X-Header" => "blah"}, "bar"]
26
+ assert_equal expected, @multi.parts.last
27
+ end
28
+
29
+
30
+ def test_add_file
31
+ file = File.open("test/mocks/200_response.json", "rb")
32
+ @multi.add "foo", file
33
+
34
+ expected = [{
35
+ "content-disposition" =>
36
+ 'form-data; name="foo"; filename="200_response.json"',
37
+ "Content-Type" => "application/json",
38
+ "Content-Transfer-Encoding" => "binary"
39
+ }, file]
40
+
41
+ assert_equal expected, @multi.parts.last
42
+ end
43
+
44
+
45
+ def test_add_io
46
+ prd, pwr = IO.pipe
47
+ @multi.add "foo", prd
48
+
49
+ expected = [{
50
+ "content-disposition" => 'form-data; name="foo"',
51
+ "Content-Type" => "application/octet-stream",
52
+ "Content-Transfer-Encoding" => "binary"
53
+ }, prd]
54
+
55
+ assert_equal expected, @multi.parts.last
56
+ end
57
+
58
+
59
+ def test_add_escaped_name
60
+ @multi.add "foo:10", "bar"
61
+ expected = [{'content-disposition' => 'form-data; name="foo:10"'}, "bar"]
62
+ assert_equal expected, @multi.parts.last
63
+ end
64
+
65
+
66
+ def test_to_io
67
+ @multi.add "key1", "bar"
68
+ @multi.add "key2", "some value"
69
+ @multi.add "key3", "other thing"
70
+
71
+ io = @multi.to_io
72
+ assert_equal Kronk::MultipartIO, io.class
73
+ assert_equal 1, io.parts.length
74
+ assert_equal StringIO, io.parts.first.class
75
+
76
+ expected = <<-STR
77
+ --foobar\r
78
+ content-disposition: form-data; name="key1"\r
79
+ \r
80
+ bar\r
81
+ --foobar\r
82
+ content-disposition: form-data; name="key2"\r
83
+ \r
84
+ some value\r
85
+ --foobar\r
86
+ content-disposition: form-data; name="key3"\r
87
+ \r
88
+ other thing\r
89
+ --foobar--
90
+ STR
91
+ assert_equal expected.strip, io.read
92
+ end
93
+
94
+
95
+ def test_to_io_with_file
96
+ file = File.open("test/mocks/200_response.json", "rb")
97
+ @multi.add "key1", "bar"
98
+ @multi.add "key2", "some value"
99
+ @multi.add "my_file", file
100
+
101
+ io = @multi.to_io
102
+ assert_equal 3, io.parts.length
103
+ assert_equal StringIO, io.parts.first.class
104
+ assert_equal file, io.parts[1]
105
+ assert_equal StringIO, io.parts.last.class
106
+
107
+ file_content = File.read("test/mocks/200_response.json")
108
+
109
+ head = <<-STR
110
+ --foobar\r
111
+ content-disposition: form-data; name="key1"\r
112
+ \r
113
+ bar\r
114
+ --foobar\r
115
+ content-disposition: form-data; name="key2"\r
116
+ \r
117
+ some value\r
118
+ --foobar\r
119
+ content-disposition: form-data; name="my_file"; filename="200_response.json"\r
120
+ STR
121
+
122
+ ctype_line = "Content-Type: application/json\r\n"
123
+ ctran_line = "Content-Transfer-Encoding: binary\r\n"
124
+ tail = "\r\n#{file_content}\r\n--foobar--"
125
+
126
+ output = io.read head.bytes.count
127
+ assert_equal head, output
128
+
129
+ output = ""
130
+ output << io.read(1) while output[-1..-1] != "\n"
131
+
132
+ comp1, comp2 = output.length == ctype_line.length ?
133
+ [ctype_line, ctran_line] : [ctran_line, ctype_line]
134
+
135
+ assert_equal comp1, output
136
+
137
+ output = ""
138
+ output << io.read(1) while output[-1..-1] != "\n"
139
+
140
+ assert_equal comp2, output
141
+
142
+ assert_equal tail, io.read
143
+ end
144
+ end
@@ -0,0 +1,92 @@
1
+ require 'test/test_helper'
2
+
3
+ class TestMultipartIo < Test::Unit::TestCase
4
+
5
+ def setup
6
+ strio = StringIO.new "thing"
7
+ @io = Kronk::MultipartIO.new "foo", "bar", strio
8
+
9
+ @pipe_rd, @pipe_wr = IO.pipe
10
+ @pipe_wr.write "thing"
11
+ @pipe_wr.close
12
+ @pio = Kronk::MultipartIO.new "foo", @pipe_rd, "bar"
13
+
14
+ @file = File.open 'test/mocks/200_response.json', "r"
15
+ @fio = Kronk::MultipartIO.new "foo", @file, "bar"
16
+ end
17
+
18
+
19
+ def teardown
20
+ @file.close
21
+ end
22
+
23
+
24
+ def test_initialize
25
+ assert_equal StringIO, @io.parts[0].class
26
+ assert_equal "foo", @io.parts[0].read
27
+ assert_equal StringIO, @io.parts[1].class
28
+ assert_equal "bar", @io.parts[1].read
29
+ assert_equal StringIO, @io.parts[2].class
30
+ assert_equal "thing", @io.parts[2].read
31
+ end
32
+
33
+
34
+ def test_size
35
+ assert_equal 11, @io.size
36
+ assert_nil @pio.size
37
+ assert_equal((@file.size + 6), @fio.size)
38
+ end
39
+
40
+
41
+ def test_read_all_strings
42
+ assert_equal "foobarthing", @io.read_all
43
+ assert_nil @io.read 1
44
+ assert_equal "", @io.read
45
+ end
46
+
47
+
48
+ def test_read_all_file
49
+ assert_equal "foo#{File.read "test/mocks/200_response.json"}bar",
50
+ @fio.read_all
51
+ assert_nil @fio.read 1
52
+ assert_equal "", @fio.read
53
+ end
54
+
55
+
56
+ def test_read_all_io
57
+ assert_equal "foothingbar",
58
+ @pio.read_all
59
+ assert_nil @pio.read 1
60
+ assert_equal "", @pio.read
61
+ end
62
+
63
+
64
+ def test_read_all_after_read_partial
65
+ assert_equal "foo", @pio.read(3)
66
+ assert_equal "thingbar", @pio.read
67
+ end
68
+
69
+
70
+ def test_eof
71
+ assert !@pio.eof?, "EOF should NOT be reached"
72
+ @pio.read 5
73
+
74
+ assert !@pio.eof?, "EOF should NOT be reached"
75
+ @pio.read_all
76
+
77
+ assert @pio.eof?, "EOF should be reached"
78
+ end
79
+
80
+
81
+ def test_close
82
+ @pio.parts.each do |io|
83
+ assert !io.closed?, "#{io.inspect} should NOT be closed"
84
+ end
85
+
86
+ @pio.close
87
+
88
+ @pio.parts.each do |io|
89
+ assert io.closed?, "#{io.inspect} should be closed"
90
+ end
91
+ end
92
+ end