require "spec" require "http/server" describe HTTP::WebSocketHandler do it "returns not found if the request is not an websocket upgrade" do io = IO::Memory.new request = HTTP::Request.new("GET", "/") response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) invoked = false handler = HTTP::WebSocketHandler.new { invoked = true } handler.next = HTTP::Handler::HandlerProc.new &.response.print("Hello") handler.call context response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello") end it "returns not found if the request Upgrade is invalid" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "WS", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version" => "13", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) invoked = false handler = HTTP::WebSocketHandler.new { invoked = true } handler.next = HTTP::Handler::HandlerProc.new &.response.print("Hello") handler.call context response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello") end {% for connection in ["Upgrade", "keep-alive, Upgrade"] %} it "gives upgrade response for websocket upgrade request with '{{connection.id}}' request" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => {{connection}}, "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version" => "13", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new { } handler.next = HTTP::Handler::HandlerProc.new &.response.print("Hello") begin handler.call context rescue IO::Error # Raises because the IO::Memory is empty end response.close io.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") end {% end %} it "gives upgrade response for case-insensitive 'WebSocket' upgrade request" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "WebSocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version" => "13", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new { } handler.next = HTTP::Handler::HandlerProc.new &.response.print("Hello") begin handler.call context rescue IO::Error # Raises because the IO::Memory is empty end response.close io.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") end it "returns bad request if Sec-WebSocket-Key is missing" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", "Sec-WebSocket-Version" => "13", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new { } handler.call context response.close io.to_s.should eq("HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nContent-Length: 16\r\n\r\n400 Bad Request\n") end it "returns upgrade required if Sec-WebSocket-Version is missing" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new { } handler.call context response.close io.to_s.should eq("HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\nContent-Length: 0\r\n\r\n") end it "returns upgrade required if Sec-WebSocket-Version is invalid" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version" => "12", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new { } handler.call context response.close io.to_s.should eq("HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\nContent-Length: 0\r\n\r\n") end it "includes Sec-WebSocket-Protocol in upgrade response if it is valid" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "WebSocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version" => "13", "Sec-WebSocket-Protocol" => "chat", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new(["chat"]) { } handler.next = HTTP::Handler::HandlerProc.new &.response.print("Hello") begin handler.call context rescue IO::Error # Raises because the IO::Memory is empty end response.close io.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\nSec-WebSocket-Protocol: chat\r\n\r\n") end it "excludes Sec-WebSocket-Protocol in upgrade response if it is not valid" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "WebSocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version" => "13", "Sec-WebSocket-Protocol" => "chat", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new(["video"]) { } handler.next = HTTP::Handler::HandlerProc.new &.response.print("Hello") begin handler.call context rescue IO::Error # Raises because the IO::Memory is empty end response.close io.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") end [ { client_protocols: "chat", expected: "chat", server_protocols: ["chat"], }, { client_protocols: "chat,video", expected: "video", server_protocols: ["video"], }, { client_protocols: "chat,video", expected: "chat", server_protocols: ["video", "chat"], }, { client_protocols: "chat,video", expected: "chat", server_protocols: ["audio", "chat"], }, ].each do |test_case| it "uses the first matched Sec-WebSocket-Protocol in upgrade response for '#{test_case[:client_protocols]}'" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "WebSocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version" => "13", "Sec-WebSocket-Protocol" => test_case[:client_protocols], } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new(test_case[:server_protocols]) { } handler.next = HTTP::Handler::HandlerProc.new &.response.print("Hello") begin handler.call context rescue IO::Error # Raises because the IO::Memory is empty end response.close io.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\nSec-WebSocket-Protocol: #{test_case[:expected]}\r\n\r\n") end end it "excludes Sec-WebSocket-Protocol in upgrade response if no protocol is provided to handler" do io = IO::Memory.new headers = HTTP::Headers{ "Upgrade" => "WebSocket", "Connection" => "Upgrade", "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version" => "13", "Sec-WebSocket-Protocol" => "chat", } request = HTTP::Request.new("GET", "/", headers: headers) response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) handler = HTTP::WebSocketHandler.new() { } handler.next = HTTP::Handler::HandlerProc.new &.response.print("Hello") begin handler.call context rescue IO::Error # Raises because the IO::Memory is empty end response.close io.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") end end