Last active
February 22, 2025 17:22
-
-
Save pulkit110/b8fe73fe7db7f424bcb3a88f89806ff7 to your computer and use it in GitHub Desktop.
Supplementary Code for Testing WebSocket Clients in Elixir with a Mock Server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@pulkit110 This was very helpful and well made! Thank you. You could probably make this into a library.