Skip to content

Instantly share code, notes, and snippets.

@pulkit110
Last active February 24, 2023 04:21
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save pulkit110/b8fe73fe7db7f424bcb3a88f89806ff7 to your computer and use it in GitHub Desktop.
Save pulkit110/b8fe73fe7db7f424bcb3a88f89806ff7 to your computer and use it in GitHub Desktop.
Supplementary Code for Testing WebSocket Clients in Elixir with a Mock Server
defmodule Commerce.Orders.MockWebsocketServer do
use Plug.Router
plug(:match)
plug(:dispatch)
match _ do
send_resp(conn, 200, "Hello from plug")
end
def start(pid) when is_pid(pid) do
ref = make_ref()
port = get_port()
{:ok, agent_pid} = Agent.start_link(fn -> :ok end)
url = "ws://localhost:#{port}/ws"
opts = [dispatch: dispatch({pid, agent_pid}), port: port, ref: ref]
case Plug.Adapters.Cowboy.http(__MODULE__, [], opts) do
{:ok, _} ->
{:ok, {ref, url}}
{:error, :eaddrinuse} ->
start(pid)
end
end
def shutdown(ref) do
Plug.Adapters.Cowboy.shutdown(ref)
end
def receive_socket_pid do
receive do
pid when is_pid(pid) -> pid
after
500 -> raise "No Server Socket pid"
end
end
defp dispatch(tuple) do
[{:_, [{"/ws", Commerce.Orders.TestSocket, [tuple]}]}]
end
# ... get_port and start_ports_agent
end
defmodule Commerce.Orders.PaymentsClient do
use WebSockex
def start_link(%{order: _order, url: url} = state), do: WebSockex.start_link(url, __MODULE__, state, opts)
@impl WebSockex
def handle_connect(_conn, state) do
WebSockex.cast(self(), {:send_message, %{initiate_payment: true}})
{:ok, state}
end
@impl WebSockex
def handle_disconnect(_status, %{close: true} = state), do: {:ok, state}
def handle_disconnect(_status, state), do: {:reconnect, state}
@impl WebSockex
def handle_frame(_frame, %{close: true} = state), do: {:close, state}
def handle_frame({:text, text}, state) do
Logger.debug("Handle frame - #{to_string(text)}")
case Jason.decode(text) do
{:ok, message} ->
handle_message(message, state)
{:error, _error} ->
Logger.warn("Invalid frame received. Do something...")
{:ok, state}
end
end
def handle_frame(any, state) do
Logger.warn("Unknown frame - #{inspect(any)}. Do something...")
{:ok, state}
end
@impl WebSockex
def handle_cast({:send_message, message}, state), do: {:reply, frame(message), state}
def handle_cast(:close, state), do: {:close, state |> Map.put(:close, true)}
defp handle_message(%{payment_methods: [method | _rest]}, state) do
Logger.debug("Received some payment methods. Do something. For the example, we will simply initiate payment for first method")
{:reply, {:text, Jason.encode!(%{initiate_payment: method})}, state}
end
defp handle_message(%{payment_page_url: url}, state) do
Logger.debug("Received a payment page url. Do something...")
{:ok, state}
end
defp handle_message(%{state: "FULFILL"}, %{order: order} = state) do
Commerce.Orders.fulfill_order(order)
{:ok, state}
end
defp handle_message(%{state: "CANCEL"}, %{order: order} = state) do
Commerce.Orders.cancel_order(order)
{:close, state}
end
end
defmodule Commerce.Orders.TestSocket do
@behaviour :cowboy_websocket
@impl :cowboy_websocket
def init(req, [{test_pid, agent_pid}]) do
case Agent.get(agent_pid, fn x -> x end) do
:ok ->
{:cowboy_websocket, req, [{test_pid, agent_pid}]}
end
end
@impl :cowboy_websocket
def terminate(_reason, _req, _state), do: :ok
@impl :cowboy_websocket
def websocket_init([{test_pid, agent_pid}]) do
send(test_pid, self())
{:ok, %{pid: test_pid, agent_pid: agent_pid}}
end
@impl :cowboy_websocket
# If you are using other frame types, you will need to update the matching here.
# Supported frames: See `InFrame` at https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_websocket/
def websocket_handle({:text, msg}, state) do
send(state.pid, to_string(msg))
handle_websocket_message(msg, state)
end
@impl :cowboy_websocket
def websocket_info(:close, state), do: {:reply, :close, state}
def websocket_info({:close, code, reason}, state) do
{:reply, {:close, code, reason}, state}
end
def websocket_info({:send, frame}, state) do
{:reply, frame, state}
end
# Hardcode commonly used expected frames and responses here
# (This is just a convenience if you want to avoid having to respond to common frames from the test code)
defp handle_websocket_message("{\"initiate_payment\": true}", state) do
{:reply, {:text, Jason.encode!(%{payment_methods: ~w[credit_card apple]a})}, state}
end
defp handle_websocket_message("another expected frame" <> _rest, state) do
# no reply
{:ok, state}
end
defp handle_websocket_message(_other, state), do: {:ok, state}
end
@zacksiri
Copy link

@pulkit110 This was very helpful and well made! Thank you. You could probably make this into a library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment