From d8d0139a0f425f2f21d644de93d81b0a5c08f640 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 14 Sep 2026 02:46:18 +0200 Subject: [PATCH] test: cover WebSocketFrameTest, decode and WebSocketHandshakeTest --- test/test_websocket_frame.rb | 104 +++++++++++++++++++++++++++++++ test/test_websocket_handshake.rb | 54 ++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 test/test_websocket_frame.rb create mode 100644 test/test_websocket_handshake.rb diff --git a/test/test_websocket_frame.rb b/test/test_websocket_frame.rb new file mode 100644 index 0000000..e4960b5 --- /dev/null +++ b/test/test_websocket_frame.rb @@ -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 diff --git a/test/test_websocket_handshake.rb b/test/test_websocket_handshake.rb new file mode 100644 index 0000000..d30f258 --- /dev/null +++ b/test/test_websocket_handshake.rb @@ -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