test: cover ServerRemoteAddrApp, ServerTest

This commit is contained in:
dualfroz committed 2026-09-13 21:39:03 +02:00
1 parent a2cf52256a
commit d62920fa24
2 files changed
+201

No files matched your search

+183
View File
@@ -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
+18
View File
@@ -30,4 +30,22 @@ class SessionTest < Minitest::Test
assert_equal({}, Prism::Session.decode(nil, 's'))
assert_equal({}, Prism::Session.decode('', 's'))
end
def test_previous_secrets_still_decode_a_cookie_signed_under_a_retired_secret
cookie = Prism::Session.encode({ 'user' => 'foksiu' }, 'old-secret')
decoded = Prism::Session.decode(cookie, 'new-secret', previous_secrets: ['old-secret'])
assert_equal 'foksiu', decoded['user']
end
def test_previous_secrets_do_not_accept_a_cookie_signed_under_neither_secret
cookie = Prism::Session.encode({ 'user' => 'foksiu' }, 'unrelated-secret')
decoded = Prism::Session.decode(cookie, 'new-secret', previous_secrets: ['old-secret'])
assert_equal({}, decoded)
end
def test_encode_always_signs_under_the_current_secret_not_a_previous_one
cookie = Prism::Session.encode({ 'user' => 'foksiu' }, 'new-secret')
assert_equal({}, Prism::Session.decode(cookie, 'old-secret'))
assert_equal 'foksiu', Prism::Session.decode(cookie, 'new-secret')['user']
end
end