test: cover WebSocketConnectionTest and teardown
This commit is contained in:
1 parent
44b93174b4
commit
14b93c93bf
1 file changed
+167
@@ -0,0 +1,167 @@
|
||||
require 'minitest/autorun'
|
||||
require 'prism'
|
||||
require 'socket'
|
||||
|
||||
# Exercises Connection over a real connected socket pair (one end plays the
|
||||
# framework's Connection, the other plays a raw client sending hand-built
|
||||
# frames) rather than mocking the socket, since the interesting behavior
|
||||
# here - auto ping/pong, fragment reassembly, the close handshake - is all
|
||||
# about how bytes actually flow across the wire.
|
||||
class WebSocketConnectionTest < Minitest::Test
|
||||
Frame = Prism::WebSocket::Frame
|
||||
|
||||
def setup
|
||||
@server_socket, @client_socket = Socket.pair(:UNIX, :STREAM, 0)
|
||||
@connection = Prism::WebSocket::Connection.new(@server_socket)
|
||||
end
|
||||
|
||||
def teardown
|
||||
@server_socket.close unless @server_socket.closed?
|
||||
@client_socket.close unless @client_socket.closed?
|
||||
end
|
||||
|
||||
def test_reads_a_single_unfragmented_text_message
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, 'hello', mask: true))
|
||||
assert_equal 'hello', @connection.read_message
|
||||
end
|
||||
|
||||
def test_reassembles_a_fragmented_message_across_continuation_frames
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, 'foo', fin: false, mask: true))
|
||||
@client_socket.write(Frame.encode(Frame::CONTINUATION, 'bar', fin: false, mask: true))
|
||||
@client_socket.write(Frame.encode(Frame::CONTINUATION, 'baz', mask: true))
|
||||
assert_equal 'foobarbaz', @connection.read_message
|
||||
end
|
||||
|
||||
def test_automatically_replies_to_a_ping_with_a_pong_and_keeps_reading
|
||||
@client_socket.write(Frame.encode(Frame::PING, 'are you there', mask: true))
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, 'after the ping', mask: true))
|
||||
|
||||
assert_equal 'after the ping', @connection.read_message
|
||||
|
||||
reply = Frame.decode(@client_socket)
|
||||
assert_equal Frame::PONG, reply.opcode
|
||||
assert_equal 'are you there', reply.payload
|
||||
end
|
||||
|
||||
def test_close_frame_echoes_close_and_returns_nil
|
||||
@client_socket.write(Frame.encode(Frame::CLOSE, '', mask: true))
|
||||
assert_nil @connection.read_message
|
||||
assert @connection.closed?
|
||||
|
||||
reply = Frame.decode(@client_socket)
|
||||
assert_equal Frame::CLOSE, reply.opcode
|
||||
end
|
||||
|
||||
def test_write_text_is_visible_on_the_other_end_unmasked
|
||||
@connection.write_text('pushed from the server')
|
||||
frame = Frame.decode(@client_socket)
|
||||
assert_equal Frame::TEXT, frame.opcode
|
||||
assert_equal 'pushed from the server', frame.payload
|
||||
end
|
||||
|
||||
def test_continuation_without_a_preceding_data_frame_raises
|
||||
@client_socket.write(Frame.encode(Frame::CONTINUATION, 'orphan', mask: true))
|
||||
assert_raises(Prism::WebSocket::ProtocolError) { @connection.read_message }
|
||||
assert @connection.closed?
|
||||
end
|
||||
|
||||
def test_read_message_returns_nil_once_closed
|
||||
@connection.close
|
||||
assert_nil @connection.read_message
|
||||
end
|
||||
|
||||
# RFC 6455 5.1: the server MUST close the connection upon receiving a
|
||||
# frame that is not masked - a Connection is always the server's view
|
||||
# of one client, so this applies to every opcode, not just data frames.
|
||||
def test_raises_on_an_unmasked_data_frame_from_the_client
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, 'no mask', mask: false))
|
||||
assert_raises(Prism::WebSocket::ProtocolError) { @connection.read_message }
|
||||
assert @connection.closed?
|
||||
end
|
||||
|
||||
def test_raises_on_an_unmasked_control_frame_from_the_client
|
||||
@client_socket.write(Frame.encode(Frame::PING, 'no mask', mask: false))
|
||||
assert_raises(Prism::WebSocket::ProtocolError) { @connection.read_message }
|
||||
assert @connection.closed?
|
||||
end
|
||||
|
||||
def test_raises_when_a_single_frame_exceeds_the_configured_max_message_size
|
||||
capped = Prism::WebSocket::Connection.new(@server_socket, max_message_size: 10)
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, 'x' * 20, mask: true))
|
||||
assert_raises(Prism::WebSocket::ProtocolError) { capped.read_message }
|
||||
assert capped.closed?
|
||||
end
|
||||
|
||||
# Each individual fragment fits under the cap, but the reassembled
|
||||
# message does not - proves the check runs on the accumulated buffer,
|
||||
# not only on Frame.decode's per-frame length.
|
||||
def test_raises_when_fragments_reassemble_past_the_configured_max_message_size
|
||||
capped = Prism::WebSocket::Connection.new(@server_socket, max_message_size: 10)
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, 'x' * 8, fin: false, mask: true))
|
||||
@client_socket.write(Frame.encode(Frame::CONTINUATION, 'x' * 8, mask: true))
|
||||
assert_raises(Prism::WebSocket::ProtocolError) { capped.read_message }
|
||||
assert capped.closed?
|
||||
end
|
||||
|
||||
# RFC 6455 7.4.1: 1002 is the generic "protocol error" close status.
|
||||
def test_protocol_violation_sends_a_1002_close_frame_to_the_client_before_raising
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, 'no mask', mask: false))
|
||||
error = assert_raises(Prism::WebSocket::ProtocolError) { @connection.read_message }
|
||||
assert_equal 1002, error.code
|
||||
|
||||
close_frame = Frame.decode(@client_socket)
|
||||
assert_equal Frame::CLOSE, close_frame.opcode
|
||||
assert_equal 1002, close_frame.payload.byteslice(0, 2).unpack1('n')
|
||||
end
|
||||
|
||||
# RFC 6455 7.4.1: 1009 is the "message too big" close status - distinct
|
||||
# from the generic 1002 raised by the other ProtocolError cases above.
|
||||
def test_oversized_message_sends_a_1009_close_frame_to_the_client_before_raising
|
||||
capped = Prism::WebSocket::Connection.new(@server_socket, max_message_size: 10)
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, 'x' * 20, mask: true))
|
||||
error = assert_raises(Prism::WebSocket::ProtocolError) { capped.read_message }
|
||||
assert_equal 1009, error.code
|
||||
|
||||
close_frame = Frame.decode(@client_socket)
|
||||
assert_equal Frame::CLOSE, close_frame.opcode
|
||||
assert_equal 1009, close_frame.payload.byteslice(0, 2).unpack1('n')
|
||||
end
|
||||
|
||||
# RFC 6455 8.1: a TEXT frame payload that is not valid UTF-8 must fail
|
||||
# the connection - "\xC3\x28" is a textbook invalid 2-byte sequence
|
||||
# (0xC3 wants a continuation byte in 0x80..0xBF next, 0x28 is not one).
|
||||
def test_raises_1007_on_invalid_utf8_text_payload
|
||||
invalid = String.new("\xC3\x28", encoding: Encoding::BINARY)
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, invalid, mask: true))
|
||||
error = assert_raises(Prism::WebSocket::ProtocolError) { @connection.read_message }
|
||||
assert_equal 1007, error.code
|
||||
assert @connection.closed?
|
||||
end
|
||||
|
||||
# RFC 6455 7.4.1: 1007 is the "invalid payload data" close status -
|
||||
# distinct from 1002/1009 raised by the other ProtocolError cases above.
|
||||
def test_invalid_utf8_sends_a_1007_close_frame_to_the_client_before_raising
|
||||
invalid = String.new("\xC3\x28", encoding: Encoding::BINARY)
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, invalid, mask: true))
|
||||
assert_raises(Prism::WebSocket::ProtocolError) { @connection.read_message }
|
||||
|
||||
close_frame = Frame.decode(@client_socket)
|
||||
assert_equal Frame::CLOSE, close_frame.opcode
|
||||
assert_equal 1007, close_frame.payload.byteslice(0, 2).unpack1('n')
|
||||
end
|
||||
|
||||
# A legally split multi-byte character (the 2-byte UTF-8 encoding of
|
||||
# "e" with an acute accent, split after its first byte across a
|
||||
# CONTINUATION boundary) must NOT be rejected - proves validation runs
|
||||
# on the fully reassembled message, not per fragment.
|
||||
def test_valid_utf8_split_across_a_continuation_boundary_is_accepted
|
||||
first_byte = String.new("\xC3", encoding: Encoding::BINARY)
|
||||
second_byte = String.new("\xA9", encoding: Encoding::BINARY)
|
||||
@client_socket.write(Frame.encode(Frame::TEXT, first_byte, fin: false, mask: true))
|
||||
@client_socket.write(Frame.encode(Frame::CONTINUATION, second_byte, mask: true))
|
||||
|
||||
message = @connection.read_message
|
||||
assert_equal "\u00e9", message
|
||||
assert message.valid_encoding?
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user