test_request: tests esoteric/rare REQUEST_URIs
[unicorn.git] / test / unit / test_upload.rb
blob58058f1288a6e2961c248f01366ed0a39fbb14a5
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 = { :pos => input.pos, :size => input.stat.size }
22       begin
23         loop { @sha1.update(input.sysread(@bs)) }
24       rescue EOFError
25       end
26       resp[:sha1] = @sha1.hexdigest
27       [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
28     end
29   end
31   def teardown
32     redirect_test_io { @server.stop(true) } if @server
33     @random.close
34   end
36   def test_put
37     start_server(@sha1_app)
38     sock = TCPSocket.new(@addr, @port)
39     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
40     @count.times do
41       buf = @random.sysread(@bs)
42       @sha1.update(buf)
43       sock.syswrite(buf)
44     end
45     read = sock.read.split(/\r\n/)
46     assert_equal "HTTP/1.1 200 OK", read[0]
47     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
48     assert_equal length, resp[:size]
49     assert_equal 0, resp[:pos]
50     assert_equal @sha1.hexdigest, resp[:sha1]
51   end
53   def test_tempfile_unlinked
54     spew_path = lambda do |env|
55       if orig = env['HTTP_X_OLD_PATH']
56         assert orig != env['rack.input'].path
57       end
58       assert_equal length, env['rack.input'].size
59       [ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ]
60     end
61     start_server(spew_path)
62     sock = TCPSocket.new(@addr, @port)
63     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
64     @count.times { sock.syswrite(' ' * @bs) }
65     path = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
66     sock.close
68     # send another request to ensure we hit the next request
69     sock = TCPSocket.new(@addr, @port)
70     sock.syswrite("PUT / HTTP/1.0\r\nX-Old-Path: #{path}\r\n" \
71                   "Content-Length: #{length}\r\n\r\n")
72     @count.times { sock.syswrite(' ' * @bs) }
73     path2 = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
74     sock.close
75     assert path != path2
77     # make sure the next request comes in so the unlink got processed
78     sock = TCPSocket.new(@addr, @port)
79     sock.syswrite("GET ?lasdf\r\n\r\n\r\n\r\n")
80     sock.sysread(4096) rescue nil
81     sock.close
83     assert ! File.exist?(path)
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 0, resp[:pos]
104     assert_equal @sha1.hexdigest, resp[:sha1]
105   end
107   def test_put_excessive_overwrite_closed
108     start_server(lambda { |env| [ 200, @hdr, [] ] })
109     sock = TCPSocket.new(@addr, @port)
110     buf = ' ' * @bs
111     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
112     @count.times { sock.syswrite(buf) }
113     assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
114       ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
115     end
116   end
118   def test_put_handler_closed_file
119     nr = '0'
120     start_server(lambda { |env|
121       env['rack.input'].close
122       resp = { :nr => nr.succ! }
123       [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
124     })
125     sock = TCPSocket.new(@addr, @port)
126     buf = ' ' * @bs
127     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
128     @count.times { sock.syswrite(buf) }
129     read = sock.read.split(/\r\n/)
130     assert_equal "HTTP/1.1 200 OK", read[0]
131     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
132     assert_equal '1', resp[:nr]
134     # server still alive?
135     sock = TCPSocket.new(@addr, @port)
136     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
137     read = sock.read.split(/\r\n/)
138     assert_equal "HTTP/1.1 200 OK", read[0]
139     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
140     assert_equal '2', resp[:nr]
141   end
143   def test_renamed_file_not_closed
144     start_server(lambda { |env|
145       new_tmp = Tempfile.new('unicorn_test')
146       input = env['rack.input']
147       File.rename(input.path, new_tmp.path)
148       resp = {
149         :inode => input.stat.ino,
150         :size => input.stat.size,
151         :new_tmp => new_tmp.path,
152         :old_tmp => input.path,
153       }
154       [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
155     })
156     sock = TCPSocket.new(@addr, @port)
157     buf = ' ' * @bs
158     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
159     @count.times { sock.syswrite(buf) }
160     read = sock.read.split(/\r\n/)
161     assert_equal "HTTP/1.1 200 OK", read[0]
162     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
163     new_tmp = File.open(resp[:new_tmp])
164     assert_equal resp[:inode], new_tmp.stat.ino
165     assert_equal length, resp[:size]
166     assert ! File.exist?(resp[:old_tmp])
167     assert_equal resp[:size], new_tmp.stat.size
168   end
170   # Despite reading numerous articles and inspecting the 1.9.1-p0 C
171   # source, Eric Wong will never trust that we're always handling
172   # encoding-aware IO objects correctly.  Thus this test uses shell
173   # utilities that should always operate on files/sockets on a
174   # byte-level.
175   def test_uncomfortable_with_onenine_encodings
176     # POSIX doesn't require all of these to be present on a system
177     which('curl') or return
178     which('sha1sum') or return
179     which('dd') or return
181     start_server(@sha1_app)
183     tmp = Tempfile.new('dd_dest')
184     assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
185                         "bs=#{@bs}", "count=#{@count}"),
186            "dd #@random to #{tmp}")
187     sha1_re = %r!\b([a-f0-9]{40})\b!
188     sha1_out = `sha1sum #{tmp.path}`
189     assert $?.success?, 'sha1sum ran OK'
191     assert_match(sha1_re, sha1_out)
192     sha1 = sha1_re.match(sha1_out)[1]
193     resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
194     assert $?.success?, 'curl ran OK'
195     assert_match(%r!\b#{sha1}\b!, resp)
196   end
198   private
200   def length
201     @bs * @count
202   end
204   def start_server(app)
205     redirect_test_io do
206       @server = HttpServer.new(app, :listeners => [ "#{@addr}:#{@port}" ] )
207       @server.start
208     end
209   end