test: cover ServerRemoteAddrApp, ServerTest
This commit is contained in:
1 parent
a2cf52256a
commit
d62920fa24
2 files changed
+201
No files matched your search
@@ -0,0 +1,183 @@
|
||||
require 'minitest/autorun'
|
||||
require 'prism'
|
||||
require 'net/http'
|
||||
require 'socket'
|
||||
|
||||
class ServerRemoteAddrApp < Prism::Application
|
||||
get '/whoami' do
|
||||
json(remote_addr: request.env['REMOTE_ADDR'])
|
||||
end
|
||||
end
|
||||
|
||||
class ServerTest < Minitest::Test
|
||||
def setup
|
||||
@server = Prism::Server.new(ServerRemoteAddrApp, port: 0).start
|
||||
@port = @server.port
|
||||
end
|
||||
|
||||
def teardown
|
||||
@server.stop
|
||||
end
|
||||
|
||||
# Regression: the hand-rolled Rack env built by Server#read_request never
|
||||
# set REMOTE_ADDR, so anything keying off the caller's IP over a real
|
||||
# socket - Middleware::RateLimiting's IP-based throttles chief among
|
||||
# them - silently never matched, even though the middleware itself was
|
||||
# correctly implemented and covered by tests that inject REMOTE_ADDR by
|
||||
# hand.
|
||||
def test_remote_addr_is_populated_from_the_real_socket
|
||||
response = Net::HTTP.get_response(URI("http://127.0.0.1:#{@port}/whoami"))
|
||||
assert_equal '200', response.code
|
||||
assert_equal '127.0.0.1', JSON.parse(response.body)['remote_addr']
|
||||
end
|
||||
end
|
||||
|
||||
# A controller action returning a Prism::StreamingBody (see
|
||||
# Prism::Controller#stream) - real end-to-end proof that
|
||||
# Server#write_chunked_response speaks correct HTTP/1.1 chunked
|
||||
# Transfer-Encoding over an actual TCPSocket, not just against a directly
|
||||
# invoked middleware/App#call(env) tuple the way test_controller_streaming.rb
|
||||
# and test_response.rb both already cover.
|
||||
class ServerStreamingApp < Prism::Application
|
||||
get '/export' do
|
||||
headers('Content-Type' => 'text/csv')
|
||||
stream do |out|
|
||||
out.write("id,title\n")
|
||||
out.write("1,one\n")
|
||||
out.write("2,two\n")
|
||||
end
|
||||
end
|
||||
|
||||
get '/plain' do
|
||||
json(ok: true)
|
||||
end
|
||||
end
|
||||
|
||||
class ServerStreamingTest < Minitest::Test
|
||||
def setup
|
||||
@server = Prism::Server.new(ServerStreamingApp, port: 0).start
|
||||
@port = @server.port
|
||||
end
|
||||
|
||||
def teardown
|
||||
@server.stop
|
||||
end
|
||||
|
||||
def test_net_http_client_receives_the_full_reassembled_body
|
||||
response = Net::HTTP.get_response(URI("http://127.0.0.1:#{@port}/export"))
|
||||
assert_equal '200', response.code
|
||||
assert_equal "id,title\n1,one\n2,two\n", response.body
|
||||
end
|
||||
|
||||
def test_net_http_reports_chunked_transfer_encoding_not_content_length
|
||||
response = Net::HTTP.get_response(URI("http://127.0.0.1:#{@port}/export"))
|
||||
assert_equal 'chunked', response['Transfer-Encoding']
|
||||
assert_nil response['Content-Length']
|
||||
end
|
||||
|
||||
# Reads the raw bytes off the socket ourselves (bypassing Net::HTTP's own
|
||||
# chunked-decoding) to prove the wire format itself - not just that some
|
||||
# HTTP client happens to be able to reassemble it - matches RFC 7230
|
||||
# SS4.1: one `<hex-size>\r\n<data>\r\n` frame per chunk written by the
|
||||
# action, in the order it wrote them, followed by the `0\r\n\r\n`
|
||||
# terminating chunk.
|
||||
def test_raw_wire_bytes_use_correct_chunk_framing
|
||||
socket = TCPSocket.new('127.0.0.1', @port)
|
||||
socket.write("GET /export HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
|
||||
raw = socket.read
|
||||
socket.close
|
||||
|
||||
head, chunked = raw.split("\r\n\r\n", 2)
|
||||
assert_includes head, 'Transfer-Encoding: chunked'
|
||||
refute_includes head, 'Content-Length'
|
||||
|
||||
expected = "9\r\nid,title\n\r\n" \
|
||||
"6\r\n1,one\n\r\n" \
|
||||
"6\r\n2,two\n\r\n" \
|
||||
"0\r\n\r\n"
|
||||
assert_equal expected, chunked
|
||||
end
|
||||
|
||||
# A streamed response must not break the keep-alive connection for
|
||||
# whatever request comes after it on the same socket - the chunked body
|
||||
# is self-delimiting (the 0-length terminating chunk), so Server#serve's
|
||||
# normal loop can keep reading the next request right after it exactly
|
||||
# like it would after an ordinary Content-Length body.
|
||||
def test_keep_alive_connection_survives_a_streamed_response_followed_by_a_plain_one
|
||||
Net::HTTP.start('127.0.0.1', @port) do |http|
|
||||
first = http.request(Net::HTTP::Get.new('/export'))
|
||||
second = http.request(Net::HTTP::Get.new('/plain'))
|
||||
|
||||
assert_equal '200', first.code
|
||||
assert_equal "id,title\n1,one\n2,two\n", first.body
|
||||
assert_equal '200', second.code
|
||||
assert_equal({ 'ok' => true }, JSON.parse(second.body))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A streaming action whose producer block pauses (blocked on a Queue the
|
||||
# test controls) between two chunks, so a test can yank the client socket
|
||||
# out from under it mid-stream at a precise, non-racy point instead of
|
||||
# guessing with a sleep.
|
||||
class ServerAbandonedStreamApp < Prism::Application
|
||||
class << self
|
||||
attr_accessor :gate
|
||||
end
|
||||
|
||||
get '/slow-export' do
|
||||
stream do |out|
|
||||
out.write("first\n")
|
||||
self.class.gate.pop
|
||||
out.write("second\n")
|
||||
end
|
||||
end
|
||||
|
||||
get '/plain' do
|
||||
json(ok: true)
|
||||
end
|
||||
end
|
||||
|
||||
# Regression coverage for the "not a dedicated test" gap left open by the
|
||||
# streaming session (see prism-postep.md, sesja 91, "NIE zweryfikowane"):
|
||||
# write_chunked_response's `socket.write` runs against a socket the client
|
||||
# may have already closed, and Server#serve wraps the WHOLE request loop in
|
||||
# `rescue Errno::EPIPE, Errno::ECONNRESET, IOError` to survive that - but
|
||||
# nothing had ever actually exercised a real abandoned connection mid-chunk.
|
||||
class ServerAbandonedStreamTest < Minitest::Test
|
||||
def setup
|
||||
ServerAbandonedStreamApp.gate = Queue.new
|
||||
@server = Prism::Server.new(ServerAbandonedStreamApp, port: 0).start
|
||||
@port = @server.port
|
||||
end
|
||||
|
||||
def teardown
|
||||
@server.stop
|
||||
end
|
||||
|
||||
def test_client_disconnecting_mid_stream_does_not_crash_the_server_or_wedge_future_requests
|
||||
socket = TCPSocket.new('127.0.0.1', @port)
|
||||
socket.write("GET /slow-export HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
|
||||
|
||||
# Wait for the first chunk to actually land on the wire before pulling
|
||||
# the rug - otherwise we might close before write_chunked_response's
|
||||
# loop has even started, proving nothing about a write to a dead socket.
|
||||
ready = IO.select([socket], nil, nil, 2)
|
||||
refute_nil ready, 'server never sent the first chunk within 2s'
|
||||
socket.recv(4096)
|
||||
socket.close
|
||||
sleep 0.05 # let the kernel actually deliver the FIN/RST before we continue the producer
|
||||
|
||||
ServerAbandonedStreamApp.gate.push(:go)
|
||||
sleep 0.1 # give the (separate) serve() thread a moment to hit the dead write and rescue it
|
||||
|
||||
# The real assertion: whatever happened to that abandoned write, the
|
||||
# server process itself is still alive and still accepting brand-new
|
||||
# connections on a fresh socket - Server#serve's rescue clause did its
|
||||
# job instead of an unhandled exception silently killing the accept
|
||||
# loop or leaving the process in a broken state.
|
||||
response = Net::HTTP.get_response(URI("http://127.0.0.1:#{@port}/plain"))
|
||||
assert_equal '200', response.code
|
||||
assert_equal({ 'ok' => true }, JSON.parse(response.body))
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user