diff --git a/lib/minch/test_server.ex b/lib/minch/test_server.ex index f0b7aa0..8b3cd46 100644 --- a/lib/minch/test_server.ex +++ b/lib/minch/test_server.ex @@ -1,34 +1,55 @@ -if Code.ensure_loaded?(:cowboy) do +if Code.ensure_loaded?(Bandit) and Code.ensure_loaded?(WebSockAdapter) do defmodule Minch.TestServer do @moduledoc false - defmacro __using__(_) do - quote do - @behaviour :cowboy_websocket + defmodule UpgradePlug do + @moduledoc false + use Plug.Builder, copy_opts_to_assign: :opts + + plug(:connect) + plug(:upgrade) + + defp connect(%{assigns: %{opts: {handler, state, upgrade_opts}}} = conn, _) do + {conn, state} = handler.connect(conn, state) + assign(conn, :opts, {handler, state, upgrade_opts}) + end + + defp upgrade(%{assigns: %{opts: {handler, state, upgrade_opts}}} = conn, _) do + conn + |> WebSockAdapter.upgrade(handler, state, upgrade_opts) + |> Plug.Conn.halt() + end + end - def child_spec(opts) when is_list(opts) do - {state, transport_opts} = Keyword.pop(opts, :state) + def start_link(handler, state, upgrade_opts \\ [], bandit_opts \\ []) do + bandit_opts + |> Keyword.put(:plug, {UpgradePlug, {handler, state, upgrade_opts}}) + |> Keyword.put_new(:startup_log, false) + |> Bandit.start_link() + end - so_reuse_port = - case :os.type() do - {:unix, :linux} -> [{:raw, 0x1, 0xF, <<1::32-native>>}] - {:unix, :darwin} -> [{:raw, 0xFFFF, 0x0200, <<1::32-native>>}] - _ -> [] - end + defmacro __using__(_) do + quote location: :keep do + @behaviour WebSock - transport_opts = transport_opts ++ so_reuse_port + def child_spec(opts) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, [opts]} + } + end - :ranch.child_spec(make_ref(), :ranch_tcp, transport_opts, :cowboy_clear, %{ - env: %{dispatch: :cowboy_router.compile([{:_, [{:_, __MODULE__, state}]}])} - }) + def start_link(opts) do + {state, opts} = Keyword.pop(opts, :state) + {upgrade_opts, bandit_opts} = Keyword.split(opts, [:upgrade_opts]) + Minch.TestServer.start_link(__MODULE__, state, upgrade_opts, bandit_opts) end - @impl true - def init(req, state) do - {:cowboy_websocket, req, state} + def connect(conn, state) do + {conn, state} end - defoverridable init: 2 + defoverridable connect: 2 end end end diff --git a/mix.exs b/mix.exs index 55f1982..dbfccff 100644 --- a/mix.exs +++ b/mix.exs @@ -31,10 +31,11 @@ defmodule Minch.MixProject do defp deps do [ - {:cowboy, "~> 2.9", optional: true}, + {:bandit, "~> 1.4", optional: true}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.27", only: :dev, runtime: false}, - {:mint_web_socket, "~> 1.0"} + {:mint_web_socket, "~> 1.0"}, + {:websock_adapter, "~> 0.5.4", optional: true} ] end diff --git a/mix.lock b/mix.lock index 6d16e36..7b0bb3d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,5 @@ %{ - "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, - "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"}, + "bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, @@ -9,8 +8,14 @@ "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, "mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, - "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, + "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, } diff --git a/test/minch/client_test.exs b/test/minch/client_test.exs index 6d6b9d9..42a93a8 100644 --- a/test/minch/client_test.exs +++ b/test/minch/client_test.exs @@ -56,7 +56,7 @@ defmodule Minch.ClientTest do port = 8881 url = "ws://localhost:#{port}" - server_state = Map.merge(%{receiver: self(), init_result: :ok}, ctx[:server_state] || %{}) + server_state = Map.merge(%{receiver: self(), connect_result: :ok}, ctx[:server_state] || %{}) start_link_supervised!({Server, state: server_state, port: port}) client_state = Map.merge(%{receiver: self(), url: url}, ctx[:client_state] || %{}) @@ -75,7 +75,7 @@ defmodule Minch.ClientTest do assert_receive {:client, :handle_connect, [%{status: 101, headers: _}, _state]} end - @tag server_state: %{init_result: :unauthorized} + @tag server_state: %{connect_result: :unauthorized} test "handle_disconnect/2 is called after upgrading error" do assert_receive {:client, :handle_disconnect, [%Mint.WebSocket.UpgradeFailureError{status_code: 401}, 1, _]} @@ -94,7 +94,7 @@ defmodule Minch.ClientTest do test "replies from a callback", ctx do assert_receive {:client, :handle_connect, _} send(ctx.client, {:reply, :ping}) - assert_receive {:server, :frame, :ping} + assert_receive {:server, :frame, {:ping, ""}} end test "sends pong to server ping automatically", ctx do @@ -120,7 +120,7 @@ defmodule Minch.ClientTest do test "handle_disconnect/2 is called when received a :close frame from server", ctx do assert_receive {:client, :handle_connect, _} - Server.send_frame(ctx.server, :close) + Server.close(ctx.server) assert_receive {:client, :handle_disconnect, _} end diff --git a/test/minch/simple_client_test.exs b/test/minch/simple_client_test.exs index 0aa23c0..f57aaf7 100644 --- a/test/minch/simple_client_test.exs +++ b/test/minch/simple_client_test.exs @@ -3,7 +3,7 @@ defmodule Minch.SimpleClientTest do setup ctx do port = 8882 - server_state = Map.merge(%{receiver: self(), init_result: :ok}, ctx[:server_state] || %{}) + server_state = Map.merge(%{receiver: self(), connect_result: :ok}, ctx[:server_state] || %{}) start_link_supervised!({Server, state: server_state, port: port}) [url: "ws://localhost:#{port}"] end @@ -12,20 +12,21 @@ defmodule Minch.SimpleClientTest do assert {:error, %Mint.TransportError{reason: :nxdomain}} = Minch.connect("ws://example.test") end - @tag server_state: %{init_result: :timeout} + @tag server_state: %{connect_result: :timeout} test "handles the connection timeout", %{url: url} do {:error, :timeout} = Minch.connect(url, [], transport_opts: [timeout: 100]) end test "sends headers to the server while connecting", %{url: url} do {:ok, _pid, _ref} = Minch.connect(url, [{"x-hello", "world"}]) - assert_receive {:server, :request, %{headers: %{"x-hello" => "world"}}} + assert_receive {:server, :connect, conn} + assert Plug.Conn.get_req_header(conn, "x-hello") == ["world"] end test "sends frames to the server", %{url: url} do {:ok, pid, _ref} = Minch.connect(url) :ok = Minch.send_frame(pid, :ping) - assert_receive {:server, :frame, :ping} + assert_receive {:server, :frame, {:ping, ""}} end test "sends received frames to the parent process", %{url: url} do @@ -57,7 +58,7 @@ defmodule Minch.SimpleClientTest do {:ok, pid, _ref} = Minch.connect(url) monitor_ref = Process.monitor(pid) assert_receive {:server, :init, server} - Server.send_frame(server, :close) + Server.close(server) assert_receive {:DOWN, ^monitor_ref, :process, _pid, {:shutdown, _}} end end diff --git a/test/support/server.ex b/test/support/server.ex index 66f764d..2472c34 100644 --- a/test/support/server.ex +++ b/test/support/server.ex @@ -8,40 +8,62 @@ defmodule Server do :ok end - def init(req, state) do - send(state.receiver, {:server, :request, req}) + def close(server) do + send(server, :close) + :ok + end - case state.init_result do - :ok -> - {:cowboy_websocket, req, state} + def connect(conn, state) do + send(state.receiver, {:server, :connect, conn}) - :unauthorized -> - send(state.receiver, {:server, :init, nil}) - req = :cowboy_req.reply(401, %{}, "Unauthorized", req) - {:ok, req, state} + case state.connect_result do + :ok -> + {conn, state} :timeout -> send(state.receiver, {:server, :init, nil}) Process.sleep(:infinity) + + :unauthorized -> + send(state.receiver, {:server, :init, nil}) + {conn |> Plug.Conn.send_resp(401, "Unauthorized") |> Plug.Conn.halt(), state} end end - def websocket_init(state) do + def init(state) do send(state.receiver, {:server, :init, self()}) - {[], state} + {:ok, state} end - def websocket_handle(frame, state) do - send(state.receiver, {:server, :frame, frame}) - {[], state} + def terminate(reason, state) do + send(state.receiver, {:server, :terminate, reason}) + :ok end - def websocket_info({:send_frame, frame}, state) do - {List.wrap(frame), state} + def handle_in(message, state) do + send(state.receiver, {:server, :frame, to_frame(message)}) + {:ok, state} end - def terminate(reason, _req, state) do - send(state.receiver, {:server, :terminate, reason}) - :ok + def handle_control(message, state) do + send(state.receiver, {:server, :frame, to_frame(message)}) + {:ok, state} + end + + def handle_info({:send_frame, frame}, state) do + {:push, frame, state} + end + + def handle_info(:close, state) do + {:stop, :normal, state} + end + + def handle_info(message, state) do + send(state.receiver, {:server, :handle_info, [message, state]}) + {:ok, state} + end + + defp to_frame({data, [opcode: code]}) do + {code, data} end end