Skip to content

Commit

Permalink
test that websockets work through the master proxy to a phoenix endpo…
Browse files Browse the repository at this point in the history
…int.
  • Loading branch information
jesseshieh committed Dec 11, 2018
1 parent 5f5f3f5 commit 55ac62d
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 1 deletion.
3 changes: 3 additions & 0 deletions config/test.exs
Expand Up @@ -10,3 +10,6 @@ config :master_proxy, MasterProxy.Test.Endpoint,
server: false

config :phoenix, :json_library, Jason

config :master_proxy,
http: [:inet6, port: 5907]
5 changes: 4 additions & 1 deletion mix.exs
Expand Up @@ -30,8 +30,11 @@ defmodule MasterProxy.MixProject do
[
{:plug_cowboy, "~> 2.0.0"},
{:phoenix, "~> 1.4.0"},

# test
{:stream_data, "~> 0.4.2", only: :test},
{:jason, "~> 1.0", only: :test}
{:jason, "~> 1.0", only: :test},
{:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}
]
end
end
1 change: 1 addition & 0 deletions mix.lock
Expand Up @@ -10,4 +10,5 @@
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"stream_data": {:hex, :stream_data, "0.4.2", "fa86b78c88ec4eaa482c0891350fcc23f19a79059a687760ddcf8680aac2799b", [:mix], [], "hexpm"},
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
}
104 changes: 104 additions & 0 deletions test/support/websocket_client.exs
@@ -0,0 +1,104 @@
# Copied from https://raw.githubusercontent.com/phoenixframework/phoenix/v1.4/test/support/websocket_client.exs
defmodule MasterProxy.Integration.WebsocketClient do
alias Phoenix.Socket.Message
require Logger

@doc """
Starts the WebSocket server for given ws URL. Received Socket.Message's
are forwarded to the sender pid
"""
def start_link(sender, url, serializer, headers \\ []) do
:crypto.start()
:ssl.start()

Logger.debug inspect url
:websocket_client.start_link(
String.to_charlist(url),
__MODULE__,
[sender, serializer],
extra_headers: headers
)
end

@doc """
Closes the socket
"""
def close(socket) do
send(socket, :close)
end

@doc """
Sends an event to the WebSocket server per the message protocol.
"""
def send_event(server_pid, topic, event, msg) do
send(server_pid, {:send, %Message{topic: topic, event: event, payload: msg}})
end

@doc """
Sends a low-level text message to the client.
"""
def send_message(server_pid, msg) do
send(server_pid, {:send, msg})
end

@doc """
Sends a heartbeat event
"""
def send_heartbeat(server_pid) do
send_event(server_pid, "phoenix", "heartbeat", %{})
end

@doc """
Sends join event to the WebSocket server per the Message protocol
"""
def join(server_pid, topic, msg) do
send_event(server_pid, topic, "phx_join", msg)
end

@doc """
Sends leave event to the WebSocket server per the Message protocol
"""
def leave(server_pid, topic, msg) do
send_event(server_pid, topic, "phx_leave", msg)
end

@doc false
def init([sender, serializer], _conn_state) do
{:ok, %{sender: sender, join_ref: 1, ref: 0, serializer: serializer}}
end

@doc false
def websocket_handle({:text, msg}, _conn_state, %{serializer: :noop} = state) do
send(state.sender, {:text, msg})
{:ok, state}
end

def websocket_handle({:text, msg}, _conn_state, state) do
send(state.sender, state.serializer.decode!(msg, opcode: :text))
{:ok, state}
end

@doc false
def websocket_info({:send, msg}, _conn_state, %{serializer: :noop} = state) do
{:reply, {:text, msg}, state}
end

def websocket_info({:send, %Message{} = msg}, _conn_state, state) do
msg = Map.merge(msg, %{ref: to_string(state.ref + 1), join_ref: to_string(state.join_ref)})
{:reply, {:text, encode!(msg, state)}, put_in(state, [:ref], state.ref + 1)}
end

def websocket_info(:close, _conn_state, _state) do
{:close, <<>>, "done"}
end

@doc false
def websocket_terminate(_reason, _conn_state, _state) do
:ok
end

defp encode!(map, state) do
{:socket_push, :text, chardata} = state.serializer.encode!(map)
IO.chardata_to_string(chardata)
end
end
106 changes: 106 additions & 0 deletions test/websocket_socket_test.exs
@@ -0,0 +1,106 @@
# Copied from https://raw.githubusercontent.com/phoenixframework/phoenix/v1.4/test/phoenix/integration/websocket_socket_test.exs
Code.require_file("./support/websocket_client.exs", __DIR__)

defmodule MasterProxy.Integration.WebSocketTest do
use ExUnit.Case, async: true
import ExUnit.CaptureLog
require Logger

alias MasterProxy.Integration.WebsocketClient
alias __MODULE__.Endpoint

@moduletag :capture_log
@port 5907
@path "ws://127.0.0.1:#{@port}/ws/websocket"

# TODO: how does this work? when I try to configure
# :master_proxy here, it is too late. maybe ExUnit is
# starting my "main" app automatically before we get here?
Application.put_env(
:phoenix,
Endpoint,
https: false,
http: [port: @port + 1],
debug_errors: false,
server: true
)

defmodule UserSocket do
@behaviour Phoenix.Socket.Transport

def child_spec(opts) do
:value = Keyword.fetch!(opts, :custom)
Supervisor.Spec.worker(Task, [fn -> :ok end], restart: :transient)
end

def connect(map) do
%{endpoint: Endpoint, params: params, transport: :websocket} = map
{:ok, {:params, params}}
end

def init({:params, _} = state) do
{:ok, state}
end

def handle_in({"params", opts}, {:params, params} = state) do
:text = Keyword.fetch!(opts, :opcode)
{:reply, :ok, {:text, inspect(params)}, state}
end

def handle_in({"ping", opts}, state) do
:text = Keyword.fetch!(opts, :opcode)
send(self(), :ping)
{:ok, state}
end

def handle_info(:ping, state) do
{:push, {:text, "pong"}, state}
end

def terminate(_reason, {:params, _}) do
:ok
end
end

defmodule Endpoint do
use Phoenix.Endpoint, otp_app: :phoenix

socket "/ws", UserSocket,
websocket: [check_origin: ["//example.com"], timeout: 200],
custom: :value
end

setup_all do
# capture_log(fn -> MasterProxy.Application.start(nil, nil) end)
# MasterProxy.Application.start(nil, nil)
# matches everything and proxies over to the Endpoint here
backends = [%{phoenix_endpoint: Endpoint}]
Application.put_env(:master_proxy, :backends, backends)
# This needs to start so Phoenix.Config is initialized
# among other things
capture_log(fn -> Endpoint.start_link() end)
:ok
end

test "refuses unallowed origins" do
capture_log(fn ->
headers = [{"origin", "https://example.com"}]
assert {:ok, _} = WebsocketClient.start_link(self(), @path, :noop, headers)

headers = [{"origin", "http://notallowed.com"}]
assert {:error, {403, _}} = WebsocketClient.start_link(self(), @path, :noop, headers)
end)
end

test "returns params with sync request" do
assert {:ok, client} = WebsocketClient.start_link(self(), "#{@path}?key=value", :noop)
WebsocketClient.send_message(client, "params")
assert_receive {:text, ~s(%{"key" => "value"})}
end

test "returns pong from async request" do
assert {:ok, client} = WebsocketClient.start_link(self(), "#{@path}?key=value", :noop)
WebsocketClient.send_message(client, "ping")
assert_receive {:text, "pong"}
end
end

0 comments on commit 55ac62d

Please sign in to comment.