test: cover WebSocketFrameTest, decode and WebSocketHandshakeTest
This commit is contained in:
1 parent
14b93c93bf
commit
d8d0139a0f
2 files changed
+158
No files matched your search
@@ -0,0 +1,104 @@
|
|||||||
|
require 'minitest/autorun'
|
||||||
|
require 'prism'
|
||||||
|
require 'stringio'
|
||||||
|
|
||||||
|
class WebSocketFrameTest < Minitest::Test
|
||||||
|
Frame = Prism::WebSocket::Frame
|
||||||
|
|
||||||
|
def decode(bytes)
|
||||||
|
Frame.decode(StringIO.new(bytes))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_encodes_and_decodes_a_short_unmasked_text_frame
|
||||||
|
encoded = Frame.encode(Frame::TEXT, 'hi')
|
||||||
|
frame = decode(encoded)
|
||||||
|
assert frame.fin
|
||||||
|
assert_equal Frame::TEXT, frame.opcode
|
||||||
|
assert_equal 'hi', frame.payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_masked_frame_round_trips_the_same_payload_as_unmasked
|
||||||
|
encoded = Frame.encode(Frame::TEXT, 'client says hi', mask: true)
|
||||||
|
frame = decode(encoded)
|
||||||
|
assert_equal 'client says hi', frame.payload
|
||||||
|
# the mask key is 4 random bytes right after the length field - a
|
||||||
|
# masked frame for the same payload must not be byte-identical to an
|
||||||
|
# unmasked one (mask bit set, and the payload bytes themselves differ).
|
||||||
|
unmasked = Frame.encode(Frame::TEXT, 'client says hi')
|
||||||
|
refute_equal unmasked, encoded
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_two_masked_encodes_of_the_same_payload_differ_by_mask_key
|
||||||
|
first = Frame.encode(Frame::TEXT, 'same payload', mask: true)
|
||||||
|
second = Frame.encode(Frame::TEXT, 'same payload', mask: true)
|
||||||
|
refute_equal first, second
|
||||||
|
assert_equal decode(first).payload, decode(second).payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_medium_payload_uses_the_16_bit_length_field
|
||||||
|
payload = 'x' * 200
|
||||||
|
encoded = Frame.encode(Frame::BINARY, payload)
|
||||||
|
# byte 1 (0-indexed) low 7 bits must read 126 - the 16-bit-length marker
|
||||||
|
assert_equal 126, encoded.getbyte(1) & 0x7F
|
||||||
|
assert_equal payload, decode(encoded).payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_large_payload_uses_the_64_bit_length_field
|
||||||
|
payload = 'x' * 70_000
|
||||||
|
encoded = Frame.encode(Frame::BINARY, payload)
|
||||||
|
assert_equal 127, encoded.getbyte(1) & 0x7F
|
||||||
|
assert_equal 70_000, decode(encoded).payload.bytesize
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fin_bit_is_clear_on_a_fragment
|
||||||
|
encoded = Frame.encode(Frame::TEXT, 'part one', fin: false)
|
||||||
|
frame = decode(encoded)
|
||||||
|
refute frame.fin
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_close_ping_pong_opcodes_round_trip
|
||||||
|
[Frame::CLOSE, Frame::PING, Frame::PONG].each do |opcode|
|
||||||
|
frame = decode(Frame.encode(opcode, 'payload'))
|
||||||
|
assert_equal opcode, frame.opcode
|
||||||
|
assert_equal 'payload', frame.payload
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_decode_raises_protocol_error_when_stream_ends_mid_frame
|
||||||
|
encoded = Frame.encode(Frame::TEXT, 'truncated me')
|
||||||
|
truncated = encoded[0...-3]
|
||||||
|
assert_raises(Prism::WebSocket::ProtocolError) { decode(truncated) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_empty_payload_round_trips
|
||||||
|
frame = decode(Frame.encode(Frame::PING, ''))
|
||||||
|
assert_equal '', frame.payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_decode_reports_whether_the_frame_was_masked
|
||||||
|
refute decode(Frame.encode(Frame::TEXT, 'x')).masked
|
||||||
|
assert decode(Frame.encode(Frame::TEXT, 'x', mask: true)).masked
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_decode_accepts_a_frame_within_the_max_length
|
||||||
|
frame = Frame.decode(StringIO.new(Frame.encode(Frame::TEXT, 'ok')), max_length: 10)
|
||||||
|
assert_equal 'ok', frame.payload
|
||||||
|
end
|
||||||
|
|
||||||
|
# A truthful 64-bit length header is enough to reject the frame - the
|
||||||
|
# payload itself is never even attempted to be read, which is the whole
|
||||||
|
# point of checking before read_exact instead of after.
|
||||||
|
def test_decode_rejects_an_oversized_frame_before_reading_its_payload
|
||||||
|
header = ((0x80 | Frame::BINARY)).chr + 127.chr + [10_000_000].pack('Q>')
|
||||||
|
io = StringIO.new(header)
|
||||||
|
error = assert_raises(Prism::WebSocket::ProtocolError) { Frame.decode(io, max_length: 1024) }
|
||||||
|
assert_match(/exceeds the 1024 byte limit/, error.message)
|
||||||
|
# RFC 6455 7.4.1: 1009 ("message too big"), not the generic 1002.
|
||||||
|
assert_equal 1009, error.code
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_protocol_error_defaults_to_the_generic_1002_close_code
|
||||||
|
error = assert_raises(Prism::WebSocket::ProtocolError) { decode('') }
|
||||||
|
assert_equal 1002, error.code
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
require 'minitest/autorun'
|
||||||
|
require 'prism'
|
||||||
|
|
||||||
|
class WebSocketHandshakeTest < Minitest::Test
|
||||||
|
Handshake = Prism::WebSocket::Handshake
|
||||||
|
|
||||||
|
# RFC 6455 section 1.3's worked example - the canonical regression test
|
||||||
|
# for any Sec-WebSocket-Accept implementation.
|
||||||
|
def test_accept_key_matches_the_rfc_6455_worked_example
|
||||||
|
assert_equal 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=', Handshake.accept_key('dGhlIHNhbXBsZSBub25jZQ==')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_request_recognizes_a_valid_upgrade_request
|
||||||
|
env = {
|
||||||
|
'REQUEST_METHOD' => 'GET',
|
||||||
|
'HTTP_UPGRADE' => 'websocket',
|
||||||
|
'HTTP_CONNECTION' => 'Upgrade',
|
||||||
|
'HTTP_SEC_WEBSOCKET_KEY' => 'dGhlIHNhbXBsZSBub25jZQ=='
|
||||||
|
}
|
||||||
|
assert Handshake.request?(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_request_accepts_a_comma_separated_connection_header
|
||||||
|
env = {
|
||||||
|
'REQUEST_METHOD' => 'GET',
|
||||||
|
'HTTP_UPGRADE' => 'websocket',
|
||||||
|
'HTTP_CONNECTION' => 'keep-alive, Upgrade',
|
||||||
|
'HTTP_SEC_WEBSOCKET_KEY' => 'dGhlIHNhbXBsZSBub25jZQ=='
|
||||||
|
}
|
||||||
|
assert Handshake.request?(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_request_rejects_a_plain_get
|
||||||
|
env = { 'REQUEST_METHOD' => 'GET' }
|
||||||
|
refute Handshake.request?(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_request_rejects_missing_upgrade_header
|
||||||
|
env = { 'REQUEST_METHOD' => 'GET', 'HTTP_CONNECTION' => 'Upgrade', 'HTTP_SEC_WEBSOCKET_KEY' => 'x' }
|
||||||
|
refute Handshake.request?(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_request_rejects_missing_sec_websocket_key
|
||||||
|
env = { 'REQUEST_METHOD' => 'GET', 'HTTP_UPGRADE' => 'websocket', 'HTTP_CONNECTION' => 'Upgrade' }
|
||||||
|
refute Handshake.request?(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_response_headers_include_the_computed_accept_value
|
||||||
|
headers = Handshake.response_headers('HTTP_SEC_WEBSOCKET_KEY' => 'dGhlIHNhbXBsZSBub25jZQ==')
|
||||||
|
assert_equal 'websocket', headers['Upgrade']
|
||||||
|
assert_equal 'Upgrade', headers['Connection']
|
||||||
|
assert_equal 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=', headers['Sec-WebSocket-Accept']
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user