test: cover test_a_hub_subscriber_cap_rejects_the_connection_that_exc...
This commit is contained in:
1 parent
dbed6261e6
commit
43c5214475
1 file changed
+201
@@ -157,3 +157,204 @@ class WebSocketIntegrationTest < Minitest::Test
|
|||||||
socket = connect_and_upgrade('/cable')
|
socket = connect_and_upgrade('/cable')
|
||||||
Frame.decode(socket) # welcome
|
Frame.decode(socket) # welcome
|
||||||
|
|
||||||
|
socket.write(Frame.encode(Frame::TEXT, 'this payload is well over sixteen bytes', mask: true))
|
||||||
|
assert_equal 'echo: this payload is well over sixteen bytes', Frame.decode(socket).payload
|
||||||
|
ensure
|
||||||
|
socket&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
# Application.websocket + Hub.new(max_subscribers_per_stream:) end-to-end:
|
||||||
|
# the second real connection to a one-slot room is rejected with a real
|
||||||
|
# 1008 close frame instead of ever hearing "welcome".
|
||||||
|
def test_a_hub_subscriber_cap_rejects_the_connection_that_exceeds_it_over_the_real_wire
|
||||||
|
first = connect_and_upgrade('/capped-room')
|
||||||
|
assert_equal 'welcome', Frame.decode(first).payload
|
||||||
|
|
||||||
|
second = connect_and_upgrade('/capped-room')
|
||||||
|
reply = Frame.decode(second)
|
||||||
|
assert_equal Frame::CLOSE, reply.opcode
|
||||||
|
assert_equal 1008, reply.payload.byteslice(0, 2).unpack1('n')
|
||||||
|
ensure
|
||||||
|
first&.close
|
||||||
|
second&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
# The rejected connection above must not disturb the one already holding
|
||||||
|
# the slot - the cap rejects new subscribers, it does not evict or
|
||||||
|
# otherwise touch existing ones.
|
||||||
|
def test_a_rejected_connection_does_not_disturb_the_one_already_subscribed
|
||||||
|
first = connect_and_upgrade('/capped-room')
|
||||||
|
Frame.decode(first) # welcome
|
||||||
|
|
||||||
|
second = connect_and_upgrade('/capped-room')
|
||||||
|
Frame.decode(second) # 1008 close, discarded
|
||||||
|
|
||||||
|
first.write(Frame.encode(Frame::TEXT, 'still here', mask: true))
|
||||||
|
assert_equal 'echo: still here', Frame.decode(first).payload
|
||||||
|
ensure
|
||||||
|
first&.close
|
||||||
|
second&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
# Closing the connection holding the slot frees it (Connection#on_close ->
|
||||||
|
# Hub#unsubscribe_all, the same path every other channel already relies
|
||||||
|
# on) - a subsequent connection is not permanently locked out.
|
||||||
|
def test_closing_the_holder_frees_the_slot_for_the_next_connection
|
||||||
|
first = connect_and_upgrade('/capped-room')
|
||||||
|
Frame.decode(first) # welcome
|
||||||
|
first.write(Frame.encode(Frame::CLOSE, '', mask: true))
|
||||||
|
Frame.decode(first) # echoed CLOSE
|
||||||
|
first.close
|
||||||
|
|
||||||
|
second = connect_and_upgrade('/capped-room')
|
||||||
|
assert_equal 'welcome', Frame.decode(second).payload
|
||||||
|
ensure
|
||||||
|
first&.close
|
||||||
|
second&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def upgrade_request(path, key: 'dGhlIHNhbXBsZSBub25jZQ==')
|
||||||
|
"GET #{path} HTTP/1.1\r\n" \
|
||||||
|
"Host: 127.0.0.1\r\n" \
|
||||||
|
"Upgrade: websocket\r\n" \
|
||||||
|
"Connection: Upgrade\r\n" \
|
||||||
|
"Sec-WebSocket-Key: #{key}\r\n" \
|
||||||
|
"Sec-WebSocket-Version: 13\r\n" \
|
||||||
|
"\r\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_headers(socket)
|
||||||
|
headers = +''
|
||||||
|
until headers.end_with?("\r\n\r\n")
|
||||||
|
line = socket.gets("\r\n")
|
||||||
|
break if line.nil?
|
||||||
|
headers << line
|
||||||
|
end
|
||||||
|
headers
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect_and_upgrade(path)
|
||||||
|
socket = TCPSocket.new('127.0.0.1', @port)
|
||||||
|
key = 'dGhlIHNhbXBsZSBub25jZQ=='
|
||||||
|
socket.write(upgrade_request(path, key: key))
|
||||||
|
response = read_headers(socket)
|
||||||
|
assert_includes response, '101'
|
||||||
|
assert_includes response, Handshake.accept_key(key)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prism::Server's own max_websocket_connections:, distinct from
|
||||||
|
# WebSocket::Hub#max_subscribers_per_stream (test_websocket_hub.rb /
|
||||||
|
# test_websocket_integration.rb above): a process-wide cap on live
|
||||||
|
# WebSocket sockets regardless of how many different streams they are
|
||||||
|
# spread across, proven over the real wire like every other Server option.
|
||||||
|
class WebSocketConnectionCapTest < Minitest::Test
|
||||||
|
Frame = Prism::WebSocket::Frame
|
||||||
|
Handshake = Prism::WebSocket::Handshake
|
||||||
|
|
||||||
|
class EchoChannel
|
||||||
|
def on_open(connection)
|
||||||
|
connection.write_text('welcome')
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_message(connection, message)
|
||||||
|
connection.write_text("echo: #{message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_close(connection); end
|
||||||
|
end
|
||||||
|
|
||||||
|
class WebSocketApp < Prism::Application
|
||||||
|
websocket '/cable-a', EchoChannel
|
||||||
|
websocket '/cable-b', EchoChannel
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup
|
||||||
|
@server = Prism::Server.new(WebSocketApp, port: 0, max_websocket_connections: 2).start
|
||||||
|
@port = @server.port
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
@server.stop
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_connections_within_the_cap_are_accepted_normally
|
||||||
|
a = connect_and_upgrade('/cable-a')
|
||||||
|
b = connect_and_upgrade('/cable-b')
|
||||||
|
assert_equal 'welcome', Frame.decode(a).payload
|
||||||
|
assert_equal 'welcome', Frame.decode(b).payload
|
||||||
|
ensure
|
||||||
|
a&.close
|
||||||
|
b&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
# The cap is global across endpoints/streams, not per-endpoint like
|
||||||
|
# Hub#max_subscribers_per_stream - two connections to two DIFFERENT
|
||||||
|
# paths already exhaust it.
|
||||||
|
def test_a_third_connection_across_different_endpoints_is_rejected_with_503
|
||||||
|
a = connect_and_upgrade('/cable-a')
|
||||||
|
b = connect_and_upgrade('/cable-b')
|
||||||
|
|
||||||
|
socket = TCPSocket.new('127.0.0.1', @port)
|
||||||
|
socket.write(upgrade_request('/cable-a'))
|
||||||
|
response = read_headers(socket)
|
||||||
|
socket.close
|
||||||
|
|
||||||
|
assert_includes response, '503'
|
||||||
|
refute_includes response, '101'
|
||||||
|
ensure
|
||||||
|
a&.close
|
||||||
|
b&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_closing_a_connection_frees_its_slot_for_a_new_one
|
||||||
|
a = connect_and_upgrade('/cable-a')
|
||||||
|
b = connect_and_upgrade('/cable-b')
|
||||||
|
a.write(Frame.encode(Frame::CLOSE, '', mask: true))
|
||||||
|
Frame.decode(a) # echoed CLOSE
|
||||||
|
a.close
|
||||||
|
|
||||||
|
Timeout.timeout(2) { sleep 0.01 while @server.websocket_connection_count >= 2 }
|
||||||
|
|
||||||
|
c = connect_and_upgrade('/cable-a')
|
||||||
|
assert_equal 'welcome', Frame.decode(c).payload
|
||||||
|
ensure
|
||||||
|
b&.close
|
||||||
|
c&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def upgrade_request(path, key: 'dGhlIHNhbXBsZSBub25jZQ==')
|
||||||
|
"GET #{path} HTTP/1.1\r\n" \
|
||||||
|
"Host: 127.0.0.1\r\n" \
|
||||||
|
"Upgrade: websocket\r\n" \
|
||||||
|
"Connection: Upgrade\r\n" \
|
||||||
|
"Sec-WebSocket-Key: #{key}\r\n" \
|
||||||
|
"Sec-WebSocket-Version: 13\r\n" \
|
||||||
|
"\r\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_headers(socket)
|
||||||
|
headers = +''
|
||||||
|
until headers.end_with?("\r\n\r\n")
|
||||||
|
line = socket.gets("\r\n")
|
||||||
|
break if line.nil?
|
||||||
|
headers << line
|
||||||
|
end
|
||||||
|
headers
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect_and_upgrade(path)
|
||||||
|
socket = TCPSocket.new('127.0.0.1', @port)
|
||||||
|
key = 'dGhlIHNhbXBsZSBub25jZQ=='
|
||||||
|
socket.write(upgrade_request(path, key: key))
|
||||||
|
response = read_headers(socket)
|
||||||
|
assert_includes response, '101'
|
||||||
|
assert_includes response, Handshake.accept_key(key)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user