Stop extending core classes
[unicorn.git] / test / unit / test_upload.rb
blobc4d6f6f05fc124f5efa62a91494dd1528ab027c6
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.size, :class => input.class }
22       @sha1.reset
23       begin
24         loop { @sha1.update(input.sysread(@bs)) }
25       rescue EOFError
26       end
27       resp[:sha1] = @sha1.hexdigest
28       [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
29     end
30   end
32   def teardown
33     redirect_test_io { @server.stop(true) } if @server
34     @random.close
35   end
37   def test_put
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")
41     @count.times do
42       buf = @random.sysread(@bs)
43       @sha1.update(buf)
44       sock.syswrite(buf)
45     end
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]
52   end
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
58       end
59       assert_equal length, env['rack.input'].size
60       [ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ]
61     end
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]
67     sock.close
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]
75     sock.close
76     assert path != path2
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
82     sock.close
84     assert ! File.exist?(path)
85   end
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")
92     @count.times do
93       buf = @random.sysread(@bs)
94       @sha1.update(buf)
95       sock.syswrite(buf)
96     end
97     sock.syswrite('12345') # write 4 bytes more than we expected
98     @sha1.update('1')
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]
106   end
108   def test_put_excessive_overwrite_closed
109     start_server(lambda { |env| [ 200, @hdr, [] ] })
110     sock = TCPSocket.new(@addr, @port)
111     buf = ' ' * @bs
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) }
116     end
117   end
119   def test_put_handler_closed_file
120     nr = '0'
121     start_server(lambda { |env|
122       env['rack.input'].close
123       resp = { :nr => nr.succ! }
124       [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
125     })
126     sock = TCPSocket.new(@addr, @port)
127     buf = ' ' * @bs
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]
142   end
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)
149       resp = {
150         :inode => input.stat.ino,
151         :size => input.stat.size,
152         :new_tmp => new_tmp.path,
153         :old_tmp => input.path,
154       }
155       [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
156     })
157     sock = TCPSocket.new(@addr, @port)
158     buf = ' ' * @bs
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
169   end
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
175   # byte-level.
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)
213   end
215   private
217   def length
218     @bs * @count
219   end
221   def start_server(app)
222     redirect_test_io do
223       @server = HttpServer.new(app, :listeners => [ "#{@addr}:#{@port}" ] )
224       @server.start
225     end
226   end