1 # Copyright (c) 2009 Eric Wong
2 require 'test/test_helper'
6 class UploadTest < Test::Unit::TestCase
9 @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
11 @hdr = {'Content-Type' => 'text/plain', 'Content-Length' => '0'}
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 = { :pos => input.pos, :size => input.size, :class => input.class }
24 loop { @sha1.update(input.sysread(@bs)) }
27 resp[:sha1] = @sha1.hexdigest
28 [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
33 redirect_test_io { @server.stop(true) } if @server
38 start_server(@sha1_app)
39 sock = TCPSocket.new(@addr, @port)
40 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
42 buf = @random.sysread(@bs)
46 read = sock.read.split(/\r\n/)
47 assert_equal "HTTP/1.1 200 OK", read[0]
48 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
49 assert_equal length, resp[:size]
50 assert_equal 0, resp[:pos]
51 assert_equal @sha1.hexdigest, resp[:sha1]
54 def test_tempfile_unlinked
55 spew_path = lambda do |env|
56 if orig = env['HTTP_X_OLD_PATH']
57 assert orig != env['rack.input'].path
59 assert_equal length, env['rack.input'].size
60 [ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ]
62 start_server(spew_path)
63 sock = TCPSocket.new(@addr, @port)
64 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
65 @count.times { sock.syswrite(' ' * @bs) }
66 path = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
69 # send another request to ensure we hit the next request
70 sock = TCPSocket.new(@addr, @port)
71 sock.syswrite("PUT / HTTP/1.0\r\nX-Old-Path: #{path}\r\n" \
72 "Content-Length: #{length}\r\n\r\n")
73 @count.times { sock.syswrite(' ' * @bs) }
74 path2 = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
78 # make sure the next request comes in so the unlink got processed
79 sock = TCPSocket.new(@addr, @port)
80 sock.syswrite("GET ?lasdf\r\n\r\n\r\n\r\n")
81 sock.sysread(4096) rescue nil
84 assert ! File.exist?(path)
87 def test_put_keepalive_truncates_small_overwrite
88 start_server(@sha1_app)
89 sock = TCPSocket.new(@addr, @port)
90 to_upload = length + 1
91 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{to_upload}\r\n\r\n")
93 buf = @random.sysread(@bs)
97 sock.syswrite('12345') # write 4 bytes more than we expected
100 read = sock.read.split(/\r\n/)
101 assert_equal "HTTP/1.1 200 OK", read[0]
102 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
103 assert_equal to_upload, resp[:size]
104 assert_equal 0, resp[:pos]
105 assert_equal @sha1.hexdigest, resp[:sha1]
108 def test_put_excessive_overwrite_closed
109 start_server(lambda { |env| [ 200, @hdr, [] ] })
110 sock = TCPSocket.new(@addr, @port)
112 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
113 @count.times { sock.syswrite(buf) }
114 assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
115 ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
119 def test_put_handler_closed_file
121 start_server(lambda { |env|
122 env['rack.input'].close
123 resp = { :nr => nr.succ! }
124 [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
126 sock = TCPSocket.new(@addr, @port)
128 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
129 @count.times { sock.syswrite(buf) }
130 read = sock.read.split(/\r\n/)
131 assert_equal "HTTP/1.1 200 OK", read[0]
132 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
133 assert_equal '1', resp[:nr]
135 # server still alive?
136 sock = TCPSocket.new(@addr, @port)
137 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
138 read = sock.read.split(/\r\n/)
139 assert_equal "HTTP/1.1 200 OK", read[0]
140 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
141 assert_equal '2', resp[:nr]
144 def test_renamed_file_not_closed
145 start_server(lambda { |env|
146 new_tmp = Tempfile.new('unicorn_test')
147 input = env['rack.input']
148 File.rename(input.path, new_tmp.path)
150 :inode => input.stat.ino,
151 :size => input.stat.size,
152 :new_tmp => new_tmp.path,
153 :old_tmp => input.path,
155 [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
157 sock = TCPSocket.new(@addr, @port)
159 sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
160 @count.times { sock.syswrite(buf) }
161 read = sock.read.split(/\r\n/)
162 assert_equal "HTTP/1.1 200 OK", read[0]
163 resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
164 new_tmp = File.open(resp[:new_tmp])
165 assert_equal resp[:inode], new_tmp.stat.ino
166 assert_equal length, resp[:size]
167 assert ! File.exist?(resp[:old_tmp])
168 assert_equal resp[:size], new_tmp.stat.size
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(/Tempfile/, 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(/StringIO/, resp)
221 def start_server(app)
223 @server = HttpServer.new(app, :listeners => [ "#{@addr}:#{@port}" ] )