From d0c1997d483f918b46bdf45cecef82d8aabcb5f1 Mon Sep 17 00:00:00 2001 From: Sean King Date: Sat, 19 Mar 2022 23:33:37 -0600 Subject: Rewrite integration-test websocket client with Mint.WebSocket --- test/support/websocket_client.ex | 193 +++++++++++++++++++++++++++++++++------ 1 file changed, 166 insertions(+), 27 deletions(-) (limited to 'test/support/websocket_client.ex') diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex index d149b324e..afcd0e8c7 100644 --- a/test/support/websocket_client.ex +++ b/test/support/websocket_client.ex @@ -3,60 +3,199 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Integration.WebsocketClient do - # https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs + @moduledoc """ + A WebSocket client used to test Mastodon API streaming - @doc """ - Starts the WebSocket server for given ws URL. Received Socket.Message's - are forwarded to the sender pid + Based on Phoenix Framework's WebsocketClient + https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs """ - def start_link(sender, url, headers \\ []) do - :crypto.start() - :ssl.start() - :websocket_client.start_link( - String.to_charlist(url), - __MODULE__, - [sender], - extra_headers: headers - ) + use GenServer + import Kernel, except: [send: 2] + + defstruct [ + :conn, + :request_ref, + :websocket, + :caller, + :status, + :resp_headers, + :sender, + closing?: false + ] + + @doc """ + Starts the WebSocket client for given ws URL. `Phoenix.Socket.Message`s + received from the server are forwarded to the sender pid. + """ + def connect(sender, url, headers \\ []) do + with {:ok, socket} <- GenServer.start_link(__MODULE__, {sender}), + {:ok, :connected} <- GenServer.call(socket, {:connect, url, headers}) do + {:ok, socket} + end end @doc """ Closes the socket """ def close(socket) do - send(socket, :close) + GenServer.cast(socket, :close) end @doc """ Sends a low-level text message to the client. """ def send_text(server_pid, msg) do - send(server_pid, {:text, msg}) + GenServer.call(server_pid, {:text, msg}) end @doc false - def init([sender], _conn_state) do - {:ok, %{sender: sender}} + def init({sender}) do + state = %__MODULE__{sender: sender} + + {:ok, state} end @doc false - def websocket_handle(frame, _conn_state, state) do - send(state.sender, frame) - {:ok, state} + def handle_call({:connect, url, headers}, from, state) do + uri = URI.parse(url) + + http_scheme = + case uri.scheme do + "ws" -> :http + "wss" -> :https + end + + ws_scheme = + case uri.scheme do + "ws" -> :ws + "wss" -> :wss + end + + path = + case uri.query do + nil -> uri.path + query -> uri.path <> "?" <> query + end + + with {:ok, conn} <- Mint.HTTP.connect(http_scheme, uri.host, uri.port), + {:ok, conn, ref} <- Mint.WebSocket.upgrade(ws_scheme, conn, path, headers) do + state = %{state | conn: conn, request_ref: ref, caller: from} + {:noreply, state} + else + {:error, reason} -> + {:reply, {:error, reason}, state} + + {:error, conn, reason} -> + {:reply, {:error, reason}, put_in(state.conn, conn)} + end end @doc false - def websocket_info({:text, msg}, _conn_state, state) do - {:reply, {:text, msg}, state} + def handle_info(message, state) do + case Mint.WebSocket.stream(state.conn, message) do + {:ok, conn, responses} -> + state = put_in(state.conn, conn) |> handle_responses(responses) + if state.closing?, do: do_close(state), else: {:noreply, state} + + {:error, conn, reason, _responses} -> + state = put_in(state.conn, conn) |> reply({:error, reason}) + {:noreply, state} + + :unknown -> + {:noreply, state} + end end - def websocket_info(:close, _conn_state, _state) do - {:close, <<>>, "done"} + defp do_close(state) do + # Streaming a close frame may fail if the server has already closed + # for writing. + _ = stream_frame(state, :close) + Mint.HTTP.close(state.conn) + {:stop, :normal, state} end - @doc false - def websocket_terminate(_reason, _conn_state, _state) do - :ok + defp handle_responses(state, responses) + + defp handle_responses(%{request_ref: ref} = state, [{:status, ref, status} | rest]) do + put_in(state.status, status) + |> handle_responses(rest) + end + + defp handle_responses(%{request_ref: ref} = state, [{:headers, ref, resp_headers} | rest]) do + put_in(state.resp_headers, resp_headers) + |> handle_responses(rest) + end + + defp handle_responses(%{request_ref: ref} = state, [{:done, ref} | rest]) do + case Mint.WebSocket.new(state.conn, ref, state.status, state.resp_headers) do + {:ok, conn, websocket} -> + %{state | conn: conn, websocket: websocket, status: nil, resp_headers: nil} + |> reply({:ok, :connected}) + |> handle_responses(rest) + + {:error, conn, reason} -> + put_in(state.conn, conn) + |> reply({:error, reason}) + end + end + + defp handle_responses(%{request_ref: ref, websocket: websocket} = state, [ + {:data, ref, data} | rest + ]) + when websocket != nil do + case Mint.WebSocket.decode(websocket, data) do + {:ok, websocket, frames} -> + put_in(state.websocket, websocket) + |> handle_frames(frames) + |> handle_responses(rest) + + {:error, websocket, reason} -> + put_in(state.websocket, websocket) + |> reply({:error, reason}) + end + end + + defp handle_responses(state, [_response | rest]) do + handle_responses(state, rest) + end + + defp handle_responses(state, []), do: state + + defp handle_frames(state, frames) do + {frames, state} = + Enum.flat_map_reduce(frames, state, fn + # prepare to close the connection when a close frame is received + {:close, _code, _data}, state -> + {[], put_in(state.closing?, true)} + + frame, state -> + {[frame], state} + end) + + Enum.each(frames, &Kernel.send(state.sender, &1)) + + state + end + + defp reply(state, response) do + if state.caller, do: GenServer.reply(state.caller, response) + put_in(state.caller, nil) + end + + # Encodes a frame as a binary and sends it along the wire, keeping `conn` + # and `websocket` up to date in `state`. + defp stream_frame(state, frame) do + with {:ok, websocket, data} <- Mint.WebSocket.encode(state.websocket, frame), + state = put_in(state.websocket, websocket), + {:ok, conn} <- Mint.WebSocket.stream_request_body(state.conn, state.request_ref, data) do + {:ok, put_in(state.conn, conn)} + else + {:error, %Mint.WebSocket{} = websocket, reason} -> + {:error, put_in(state.websocket, websocket), reason} + + {:error, conn, reason} -> + {:error, put_in(state.conn, conn), reason} + end end end -- cgit v1.2.3 From 4194559ea6d3e0f219ae8e77b468782ac115d134 Mon Sep 17 00:00:00 2001 From: Sean King Date: Sun, 20 Mar 2022 17:26:07 -0600 Subject: Fix lint errors --- test/support/websocket_client.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'test/support/websocket_client.ex') diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex index afcd0e8c7..43f2854de 100644 --- a/test/support/websocket_client.ex +++ b/test/support/websocket_client.ex @@ -188,8 +188,8 @@ defmodule Pleroma.Integration.WebsocketClient do defp stream_frame(state, frame) do with {:ok, websocket, data} <- Mint.WebSocket.encode(state.websocket, frame), state = put_in(state.websocket, websocket), - {:ok, conn} <- Mint.WebSocket.stream_request_body(state.conn, state.request_ref, data) do - {:ok, put_in(state.conn, conn)} + {:ok, conn} <- Mint.WebSocket.stream_request_body(state.conn, state.request_ref, data) do + {:ok, put_in(state.conn, conn)} else {:error, %Mint.WebSocket{} = websocket, reason} -> {:error, put_in(state.websocket, websocket), reason} -- cgit v1.2.3 From eb42e90c4f9ca35a6dc0e84e6f87b6f4b680173c Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Fri, 19 Aug 2022 13:56:39 -0400 Subject: Use Websockex to replace websocket_client --- test/support/websocket_client.ex | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) (limited to 'test/support/websocket_client.ex') diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex index 34b955474..2660f6151 100644 --- a/test/support/websocket_client.ex +++ b/test/support/websocket_client.ex @@ -5,18 +5,17 @@ defmodule Pleroma.Integration.WebsocketClient do # https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs + use WebSockex + @doc """ Starts the WebSocket server for given ws URL. Received Socket.Message's are forwarded to the sender pid """ def start_link(sender, url, headers \\ []) do - :crypto.start() - :ssl.start() - - :websocket_client.start_link( - String.to_charlist(url), + WebSockex.start_link( + url, __MODULE__, - [sender], + %{ sender: sender }, extra_headers: headers ) end @@ -36,27 +35,26 @@ defmodule Pleroma.Integration.WebsocketClient do end @doc false - def init([sender], _conn_state) do - {:ok, %{sender: sender}} - end - - @doc false - def websocket_handle(frame, _conn_state, state) do + @impl true + def handle_frame(frame, state) do send(state.sender, frame) {:ok, state} end @doc false - def websocket_info({:text, msg}, _conn_state, state) do + @impl true + def handle_info({:text, msg}, state) do {:reply, {:text, msg}, state} end - def websocket_info(:close, _conn_state, _state) do + @impl true + def handle_info(:close, _state) do {:close, <<>>, "done"} end @doc false - def websocket_terminate(_reason, _conn_state, _state) do + @impl true + def terminate(_reason, _state) do :ok end end -- cgit v1.2.3 From 3522852c6196cafa63804240f52dd593e09ba694 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Fri, 19 Aug 2022 14:09:42 -0400 Subject: Test that server will disconnect websocket upon token revocation --- test/support/websocket_client.ex | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'test/support/websocket_client.ex') diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex index 2660f6151..abe7d5eda 100644 --- a/test/support/websocket_client.ex +++ b/test/support/websocket_client.ex @@ -41,6 +41,12 @@ defmodule Pleroma.Integration.WebsocketClient do {:ok, state} end + @impl true + def handle_disconnect(conn_status, state) do + send(state.sender, {:close, conn_status}) + {:ok, state} + end + @doc false @impl true def handle_info({:text, msg}, state) do -- cgit v1.2.3 From f459c1260b43396fb7173e97e29ccef441a615ec Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Fri, 19 Aug 2022 14:10:07 -0400 Subject: Lint --- test/support/websocket_client.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test/support/websocket_client.ex') diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex index abe7d5eda..70d331999 100644 --- a/test/support/websocket_client.ex +++ b/test/support/websocket_client.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Integration.WebsocketClient do WebSockex.start_link( url, __MODULE__, - %{ sender: sender }, + %{sender: sender}, extra_headers: headers ) end -- cgit v1.2.3 From b439e91f57d1d7f26e94acc62703a64c747773da Mon Sep 17 00:00:00 2001 From: Haelwenn Date: Fri, 2 Sep 2022 22:35:40 +0000 Subject: Revert "Merge branch 'rewrite/integration-test-websocket-client' into 'develop'" This reverts merge request !3649 --- test/support/websocket_client.ex | 193 ++++++--------------------------------- 1 file changed, 27 insertions(+), 166 deletions(-) (limited to 'test/support/websocket_client.ex') diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex index 43f2854de..d149b324e 100644 --- a/test/support/websocket_client.ex +++ b/test/support/websocket_client.ex @@ -3,199 +3,60 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Integration.WebsocketClient do - @moduledoc """ - A WebSocket client used to test Mastodon API streaming - - Based on Phoenix Framework's WebsocketClient - https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs - """ - - use GenServer - import Kernel, except: [send: 2] - - defstruct [ - :conn, - :request_ref, - :websocket, - :caller, - :status, - :resp_headers, - :sender, - closing?: false - ] + # https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs @doc """ - Starts the WebSocket client for given ws URL. `Phoenix.Socket.Message`s - received from the server are forwarded to the sender pid. + Starts the WebSocket server for given ws URL. Received Socket.Message's + are forwarded to the sender pid """ - def connect(sender, url, headers \\ []) do - with {:ok, socket} <- GenServer.start_link(__MODULE__, {sender}), - {:ok, :connected} <- GenServer.call(socket, {:connect, url, headers}) do - {:ok, socket} - end + def start_link(sender, url, headers \\ []) do + :crypto.start() + :ssl.start() + + :websocket_client.start_link( + String.to_charlist(url), + __MODULE__, + [sender], + extra_headers: headers + ) end @doc """ Closes the socket """ def close(socket) do - GenServer.cast(socket, :close) + send(socket, :close) end @doc """ Sends a low-level text message to the client. """ def send_text(server_pid, msg) do - GenServer.call(server_pid, {:text, msg}) + send(server_pid, {:text, msg}) end @doc false - def init({sender}) do - state = %__MODULE__{sender: sender} - - {:ok, state} + def init([sender], _conn_state) do + {:ok, %{sender: sender}} end @doc false - def handle_call({:connect, url, headers}, from, state) do - uri = URI.parse(url) - - http_scheme = - case uri.scheme do - "ws" -> :http - "wss" -> :https - end - - ws_scheme = - case uri.scheme do - "ws" -> :ws - "wss" -> :wss - end - - path = - case uri.query do - nil -> uri.path - query -> uri.path <> "?" <> query - end - - with {:ok, conn} <- Mint.HTTP.connect(http_scheme, uri.host, uri.port), - {:ok, conn, ref} <- Mint.WebSocket.upgrade(ws_scheme, conn, path, headers) do - state = %{state | conn: conn, request_ref: ref, caller: from} - {:noreply, state} - else - {:error, reason} -> - {:reply, {:error, reason}, state} - - {:error, conn, reason} -> - {:reply, {:error, reason}, put_in(state.conn, conn)} - end + def websocket_handle(frame, _conn_state, state) do + send(state.sender, frame) + {:ok, state} end @doc false - def handle_info(message, state) do - case Mint.WebSocket.stream(state.conn, message) do - {:ok, conn, responses} -> - state = put_in(state.conn, conn) |> handle_responses(responses) - if state.closing?, do: do_close(state), else: {:noreply, state} - - {:error, conn, reason, _responses} -> - state = put_in(state.conn, conn) |> reply({:error, reason}) - {:noreply, state} - - :unknown -> - {:noreply, state} - end - end - - defp do_close(state) do - # Streaming a close frame may fail if the server has already closed - # for writing. - _ = stream_frame(state, :close) - Mint.HTTP.close(state.conn) - {:stop, :normal, state} - end - - defp handle_responses(state, responses) - - defp handle_responses(%{request_ref: ref} = state, [{:status, ref, status} | rest]) do - put_in(state.status, status) - |> handle_responses(rest) - end - - defp handle_responses(%{request_ref: ref} = state, [{:headers, ref, resp_headers} | rest]) do - put_in(state.resp_headers, resp_headers) - |> handle_responses(rest) - end - - defp handle_responses(%{request_ref: ref} = state, [{:done, ref} | rest]) do - case Mint.WebSocket.new(state.conn, ref, state.status, state.resp_headers) do - {:ok, conn, websocket} -> - %{state | conn: conn, websocket: websocket, status: nil, resp_headers: nil} - |> reply({:ok, :connected}) - |> handle_responses(rest) - - {:error, conn, reason} -> - put_in(state.conn, conn) - |> reply({:error, reason}) - end + def websocket_info({:text, msg}, _conn_state, state) do + {:reply, {:text, msg}, state} end - defp handle_responses(%{request_ref: ref, websocket: websocket} = state, [ - {:data, ref, data} | rest - ]) - when websocket != nil do - case Mint.WebSocket.decode(websocket, data) do - {:ok, websocket, frames} -> - put_in(state.websocket, websocket) - |> handle_frames(frames) - |> handle_responses(rest) - - {:error, websocket, reason} -> - put_in(state.websocket, websocket) - |> reply({:error, reason}) - end + def websocket_info(:close, _conn_state, _state) do + {:close, <<>>, "done"} end - defp handle_responses(state, [_response | rest]) do - handle_responses(state, rest) - end - - defp handle_responses(state, []), do: state - - defp handle_frames(state, frames) do - {frames, state} = - Enum.flat_map_reduce(frames, state, fn - # prepare to close the connection when a close frame is received - {:close, _code, _data}, state -> - {[], put_in(state.closing?, true)} - - frame, state -> - {[frame], state} - end) - - Enum.each(frames, &Kernel.send(state.sender, &1)) - - state - end - - defp reply(state, response) do - if state.caller, do: GenServer.reply(state.caller, response) - put_in(state.caller, nil) - end - - # Encodes a frame as a binary and sends it along the wire, keeping `conn` - # and `websocket` up to date in `state`. - defp stream_frame(state, frame) do - with {:ok, websocket, data} <- Mint.WebSocket.encode(state.websocket, frame), - state = put_in(state.websocket, websocket), - {:ok, conn} <- Mint.WebSocket.stream_request_body(state.conn, state.request_ref, data) do - {:ok, put_in(state.conn, conn)} - else - {:error, %Mint.WebSocket{} = websocket, reason} -> - {:error, put_in(state.websocket, websocket), reason} - - {:error, conn, reason} -> - {:error, put_in(state.conn, conn), reason} - end + @doc false + def websocket_terminate(_reason, _conn_state, _state) do + :ok end end -- cgit v1.2.3 From 6d148b6637af5eb96435cd802886d71b461b760e Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Fri, 19 Aug 2022 13:56:39 -0400 Subject: Use Websockex to replace websocket_client --- test/support/websocket_client.ex | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) (limited to 'test/support/websocket_client.ex') diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex index d149b324e..cf2972c38 100644 --- a/test/support/websocket_client.ex +++ b/test/support/websocket_client.ex @@ -5,18 +5,17 @@ defmodule Pleroma.Integration.WebsocketClient do # https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs + use WebSockex + @doc """ Starts the WebSocket server for given ws URL. Received Socket.Message's are forwarded to the sender pid """ def start_link(sender, url, headers \\ []) do - :crypto.start() - :ssl.start() - - :websocket_client.start_link( - String.to_charlist(url), + WebSockex.start_link( + url, __MODULE__, - [sender], + %{sender: sender}, extra_headers: headers ) end @@ -36,27 +35,26 @@ defmodule Pleroma.Integration.WebsocketClient do end @doc false - def init([sender], _conn_state) do - {:ok, %{sender: sender}} - end - - @doc false - def websocket_handle(frame, _conn_state, state) do + @impl true + def handle_frame(frame, state) do send(state.sender, frame) {:ok, state} end @doc false - def websocket_info({:text, msg}, _conn_state, state) do + @impl true + def handle_info({:text, msg}, state) do {:reply, {:text, msg}, state} end - def websocket_info(:close, _conn_state, _state) do + @impl true + def handle_info(:close, _state) do {:close, <<>>, "done"} end @doc false - def websocket_terminate(_reason, _conn_state, _state) do + @impl true + def terminate(_reason, _state) do :ok end end -- cgit v1.2.3