Transfer-Encoding: chunked streaming input support
[unicorn.git] / test / unit / test_upload.rb
blobadc036d68e0978ff3b33858a0b9e5646da6cad7f
1 # Copyright (c) 2009 Eric Wong
2 require 'test/test_helper'
4 include Unicorn
6 class UploadTest < Test::Unit::TestCase
8   def setup
9     @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
10     @port = unused_port
11     @hdr = {'Content-Type' => 'text/plain', 'Content-Length' => '0'}
12     @bs = 4096
13     @count = 256
14     @server = nil
16     # we want random binary data to test 1.9 encoding-aware IO craziness
17     @random = File.open('/dev/urandom','rb')
18     @sha1 = Digest::SHA1.new
19     @sha1_app = lambda do |env|
20       input = env['rack.input']
21       resp = { :size => input.size }
23       @sha1.reset
24       while buf = input.read(@bs)
25         @sha1.update(buf)
26       end
27       resp[:sha1] = @sha1.hexdigest
29       # rewind and read again
30       input.rewind
31       @sha1.reset
32       while buf = input.read(@bs)
33         @sha1.update(buf)
34       end
36       if resp[:sha1] == @sha1.hexdigest
37         resp[:sysread_read_byte_match] = true
38       end
40       [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
41     end
42   end
44   def teardown
45     redirect_test_io { @server.stop(true) } if @server
46     @random.close
47   end
49   def test_put
50     start_server(@sha1_app)
51     sock = TCPSocket.new(@addr, @port)
52     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
53     @count.times do |i|
54       buf = @random.sysread(@bs)
55       @sha1.update(buf)
56       sock.syswrite(buf)
57     end
58     read = sock.read.split(/\r\n/)
59     assert_equal "HTTP/1.1 200 OK", read[0]
60     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
61     assert_equal length, resp[:size]
62     assert_equal @sha1.hexdigest, resp[:sha1]
63   end
65   def test_put_trickle_small
66     @count, @bs = 2, 128
67     start_server(@sha1_app)
68     assert_equal 256, length
69     sock = TCPSocket.new(@addr, @port)
70     hdr = "PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n"
71     @count.times do
72       buf = @random.sysread(@bs)
73       @sha1.update(buf)
74       hdr << buf
75       sock.syswrite(hdr)
76       hdr = ''
77       sleep 0.6
78     end
79     read = sock.read.split(/\r\n/)
80     assert_equal "HTTP/1.1 200 OK", read[0]
81     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
82     assert_equal length, resp[:size]
83     assert_equal @sha1.hexdigest, resp[:sha1]
84   end
86   def test_put_keepalive_truncates_small_overwrite
87     start_server(@sha1_app)
88     sock = TCPSocket.new(@addr, @port)
89     to_upload = length + 1
90     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{to_upload}\r\n\r\n")
91     @count.times do
92       buf = @random.sysread(@bs)
93       @sha1.update(buf)
94       sock.syswrite(buf)
95     end
96     sock.syswrite('12345') # write 4 bytes more than we expected
97     @sha1.update('1')
99     read = sock.read.split(/\r\n/)
100     assert_equal "HTTP/1.1 200 OK", read[0]
101     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
102     assert_equal to_upload, resp[:size]
103     assert_equal @sha1.hexdigest, resp[:sha1]
104   end
106   def test_put_excessive_overwrite_closed
107     start_server(lambda { |env| [ 200, @hdr, [] ] })
108     sock = TCPSocket.new(@addr, @port)
109     buf = ' ' * @bs
110     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
111     @count.times { sock.syswrite(buf) }
112     assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
113       ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
114     end
115   end
117   # Despite reading numerous articles and inspecting the 1.9.1-p0 C
118   # source, Eric Wong will never trust that we're always handling
119   # encoding-aware IO objects correctly.  Thus this test uses shell
120   # utilities that should always operate on files/sockets on a
121   # byte-level.
122   def test_uncomfortable_with_onenine_encodings
123     # POSIX doesn't require all of these to be present on a system
124     which('curl') or return
125     which('sha1sum') or return
126     which('dd') or return
128     start_server(@sha1_app)
130     tmp = Tempfile.new('dd_dest')
131     assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
132                         "bs=#{@bs}", "count=#{@count}"),
133            "dd #@random to #{tmp}")
134     sha1_re = %r!\b([a-f0-9]{40})\b!
135     sha1_out = `sha1sum #{tmp.path}`
136     assert $?.success?, 'sha1sum ran OK'
138     assert_match(sha1_re, sha1_out)
139     sha1 = sha1_re.match(sha1_out)[1]
140     resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
141     assert $?.success?, 'curl ran OK'
142     assert_match(%r!\b#{sha1}\b!, resp)
143     assert_match(/sysread_read_byte_match/, resp)
145     # small StringIO path
146     assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
147                         "bs=1024", "count=1"),
148            "dd #@random to #{tmp}")
149     sha1_re = %r!\b([a-f0-9]{40})\b!
150     sha1_out = `sha1sum #{tmp.path}`
151     assert $?.success?, 'sha1sum ran OK'
153     assert_match(sha1_re, sha1_out)
154     sha1 = sha1_re.match(sha1_out)[1]
155     resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
156     assert $?.success?, 'curl ran OK'
157     assert_match(%r!\b#{sha1}\b!, resp)
158     assert_match(/sysread_read_byte_match/, resp)
159   end
161   private
163   def length
164     @bs * @count
165   end
167   def start_server(app)
168     redirect_test_io do
169       @server = HttpServer.new(app, :listeners => [ "#{@addr}:#{@port}" ] )
170       @server.start
171     end
172   end