1 # -*- encoding: binary -*-
3 # Copyright (c) 2009 Eric Wong
4 require 'test/test_helper'
9 class UploadTest < Test::Unit::TestCase
12 @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
14 @hdr = {'Content-Type' => 'text/plain', 'Content-Length' => '0'}
19 # we want random binary data to test 1.9 encoding-aware IO craziness
20 @random = File.open('/dev/urandom','rb')
21 @sha1 = Digest::SHA1.new
22 @sha1_app = lambda do |env|
23 input = env['rack.input']
27 while buf = input.read(@bs)
30 resp[:sha1] = @sha1.hexdigest
32 # rewind and read again
35 while buf = input.read(@bs)
39 if resp[:sha1] == @sha1.hexdigest
40 resp[:sysread_read_byte_match] = true
43 if expect_size = env['HTTP_X_EXPECT_SIZE']
44 if expect_size.to_i == input.size
45 resp[:expect_size_match] = true
48 resp[:size] = input.size
49 resp[:content_md5] = env['HTTP_CONTENT_MD5']
51 [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
56 redirect_test_io { @server.stop(false) } if @server
62 start_server(@sha1_app)
63 sock = TCPSocket.new(@addr, @port)
64 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
66 buf = @random.sysread(@bs)
70 read = sock.read.split(/\r\n/)
71 assert_equal "HTTP/1.1 200 OK", read[0]
72 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
73 assert_equal length, resp[:size]
74 assert_equal @sha1.hexdigest, resp[:sha1]
77 def test_put_content_md5
79 start_server(@sha1_app)
80 sock = TCPSocket.new(@addr, @port)
81 sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \
82 "Trailer: Content-MD5\r\n\r\n")
84 buf = @random.sysread(@bs)
87 sock.syswrite("#{'%x' % buf.size}\r\n")
88 sock.syswrite(buf << "\r\n")
90 sock.syswrite("0\r\n")
92 content_md5 = [ md5.digest! ].pack('m').strip.freeze
93 sock.syswrite("Content-MD5: #{content_md5}\r\n\r\n")
94 read = sock.read.split(/\r\n/)
95 assert_equal "HTTP/1.1 200 OK", read[0]
96 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
97 assert_equal length, resp[:size]
98 assert_equal @sha1.hexdigest, resp[:sha1]
99 assert_equal content_md5, resp[:content_md5]
102 def test_put_trickle_small
104 start_server(@sha1_app)
105 assert_equal 256, length
106 sock = TCPSocket.new(@addr, @port)
107 hdr = "PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n"
109 buf = @random.sysread(@bs)
116 read = sock.read.split(/\r\n/)
117 assert_equal "HTTP/1.1 200 OK", read[0]
118 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
119 assert_equal length, resp[:size]
120 assert_equal @sha1.hexdigest, resp[:sha1]
123 def test_put_keepalive_truncates_small_overwrite
124 start_server(@sha1_app)
125 sock = TCPSocket.new(@addr, @port)
126 to_upload = length + 1
127 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{to_upload}\r\n\r\n")
129 buf = @random.sysread(@bs)
133 sock.syswrite('12345') # write 4 bytes more than we expected
136 buf = sock.readpartial(4096)
137 while buf !~ /\r\n\r\n/
138 buf << sock.readpartial(4096)
140 read = buf.split(/\r\n/)
141 assert_equal "HTTP/1.1 200 OK", read[0]
142 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
143 assert_equal to_upload, resp[:size]
144 assert_equal @sha1.hexdigest, resp[:sha1]
147 def test_put_excessive_overwrite_closed
148 tmp = Tempfile.new('overwrite_check')
150 start_server(lambda { |env|
152 while buf = env['rack.input'].read(65536)
158 sock = TCPSocket.new(@addr, @port)
160 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
162 @count.times { sock.syswrite(buf) }
163 assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
164 ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
168 assert_equal length, tmp.read.to_i
171 # Despite reading numerous articles and inspecting the 1.9.1-p0 C
172 # source, Eric Wong will never trust that we're always handling
173 # encoding-aware IO objects correctly. Thus this test uses shell
174 # utilities that should always operate on files/sockets on a
176 def test_uncomfortable_with_onenine_encodings
177 # POSIX doesn't require all of these to be present on a system
178 which('curl') or return
179 which('sha1sum') or return
180 which('dd') or return
182 start_server(@sha1_app)
184 tmp = Tempfile.new('dd_dest')
185 assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
186 "bs=#{@bs}", "count=#{@count}"),
187 "dd #@random to #{tmp}")
188 sha1_re = %r!\b([a-f0-9]{40})\b!
189 sha1_out = `sha1sum #{tmp.path}`
190 assert $?.success?, 'sha1sum ran OK'
192 assert_match(sha1_re, sha1_out)
193 sha1 = sha1_re.match(sha1_out)[1]
194 resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
195 assert $?.success?, 'curl ran OK'
196 assert_match(%r!\b#{sha1}\b!, resp)
197 assert_match(/sysread_read_byte_match/, resp)
199 # small StringIO path
200 assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
201 "bs=1024", "count=1"),
202 "dd #@random to #{tmp}")
203 sha1_re = %r!\b([a-f0-9]{40})\b!
204 sha1_out = `sha1sum #{tmp.path}`
205 assert $?.success?, 'sha1sum ran OK'
207 assert_match(sha1_re, sha1_out)
208 sha1 = sha1_re.match(sha1_out)[1]
209 resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
210 assert $?.success?, 'curl ran OK'
211 assert_match(%r!\b#{sha1}\b!, resp)
212 assert_match(/sysread_read_byte_match/, resp)
215 def test_chunked_upload_via_curl
216 # POSIX doesn't require all of these to be present on a system
217 which('curl') or return
218 which('sha1sum') or return
219 which('dd') or return
221 start_server(@sha1_app)
223 tmp = Tempfile.new('dd_dest')
224 assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
225 "bs=#{@bs}", "count=#{@count}"),
226 "dd #@random to #{tmp}")
227 sha1_re = %r!\b([a-f0-9]{40})\b!
228 sha1_out = `sha1sum #{tmp.path}`
229 assert $?.success?, 'sha1sum ran OK'
231 assert_match(sha1_re, sha1_out)
232 sha1 = sha1_re.match(sha1_out)[1]
233 cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
234 -isSf --no-buffer -T- " \
235 "http://#@addr:#@port/"
236 resp = Tempfile.new('resp')
240 wr.sync = rd.sync = true
252 wr.write(tmp.read(@bs))
253 sleep(rand / 10) if 0 == i % 8
256 pid, status = Process.waitpid2(pid)
260 assert status.success?, 'curl ran OK'
261 assert_match(%r!\b#{sha1}\b!, resp)
262 assert_match(/sysread_read_byte_match/, resp)
263 assert_match(/expect_size_match/, resp)
266 def test_curl_chunked_small
267 # POSIX doesn't require all of these to be present on a system
268 which('curl') or return
269 which('sha1sum') or return
270 which('dd') or return
272 start_server(@sha1_app)
274 tmp = Tempfile.new('dd_dest')
275 # small StringIO path
276 assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
277 "bs=1024", "count=1"),
278 "dd #@random to #{tmp}")
279 sha1_re = %r!\b([a-f0-9]{40})\b!
280 sha1_out = `sha1sum #{tmp.path}`
281 assert $?.success?, 'sha1sum ran OK'
283 assert_match(sha1_re, sha1_out)
284 sha1 = sha1_re.match(sha1_out)[1]
285 resp = `curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
286 -isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}`
287 assert $?.success?, 'curl ran OK'
288 assert_match(%r!\b#{sha1}\b!, resp)
289 assert_match(/sysread_read_byte_match/, resp)
290 assert_match(/expect_size_match/, resp)
299 def start_server(app)
301 @server = HttpServer.new(app, :listeners => [ "#{@addr}:#{@port}" ] )