From 65b5d248a82b36460474d3732e6e7b4433934e5b Mon Sep 17 00:00:00 2001 From: Mat Trudel Date: Tue, 30 Jan 2024 11:07:59 -0500 Subject: [PATCH] Add Bandit to tests in /integration - requires Elixir 1.12+ --- mix.exs | 1 + mix.lock | 2 + .../integration/long_poll_channels_test.exs | 529 +++++++++--------- .../integration/long_poll_socket_test.exs | 156 +++--- .../integration/websocket_channels_test.exs | 70 ++- .../integration/websocket_socket_test.exs | 212 +++---- test/test_helper.exs | 1 + 7 files changed, 521 insertions(+), 450 deletions(-) diff --git a/mix.exs b/mix.exs index 6272eef344..721d59bc53 100644 --- a/mix.exs +++ b/mix.exs @@ -84,6 +84,7 @@ defmodule Phoenix.MixProject do # Optional deps {:plug_cowboy, "~> 2.7", optional: true}, + {:bandit, "~> 1.0", optional: true}, {:jason, "~> 1.0", optional: true}, # Docs dependencies (some for cross references) diff --git a/mix.lock b/mix.lock index 691cc5c1bf..1589f0261d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "bandit": {:hex, :bandit, "1.1.3", "0c504f50029381f41203788851df8e43554d79b0a073e993b424b5897ee2fb8d", [:mix], [{:hpax, "~> 0.1.1", [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", "5953cd4e924c85d61a3afbac298bfa76a1b3b9eae2cee192e23f2e5aaa4d5b73"}, "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, @@ -34,6 +35,7 @@ "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "thousand_island": {:hex, :thousand_island, "1.3.2", "bc27f9afba6e1a676dd36507d42e429935a142cf5ee69b8e3f90bff1383943cd", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e085b93012cd1057b378fce40cbfbf381ff6d957a382bfdd5eca1a98eec2535"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [: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", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, } diff --git a/test/phoenix/integration/long_poll_channels_test.exs b/test/phoenix/integration/long_poll_channels_test.exs index 7320f66660..87d61291dc 100644 --- a/test/phoenix/integration/long_poll_channels_test.exs +++ b/test/phoenix/integration/long_poll_channels_test.exs @@ -12,14 +12,6 @@ defmodule Phoenix.Integration.LongPollChannelsTest do @port 5808 @pool_size 1 - Application.put_env(:phoenix, Endpoint, - https: false, - http: [port: @port], - secret_key_base: String.duplicate("abcdefgh", 8), - server: true, - pubsub_server: __MODULE__ - ) - defmodule RoomChannel do use Phoenix.Channel, log_join: :info, log_handle_in: :info @@ -138,20 +130,6 @@ defmodule Phoenix.Integration.LongPollChannelsTest do ] end - setup_all do - capture_log(fn -> start_supervised!(Endpoint) end) - start_supervised!({Phoenix.PubSub, name: __MODULE__, pool_size: @pool_size}) - :ok - end - - setup config do - for {_, pid, _, _} <- DynamicSupervisor.which_children(Phoenix.Transports.LongPoll.Supervisor) do - DynamicSupervisor.terminate_child(Phoenix.Transports.LongPoll.Supervisor, pid) - end - - {:ok, topic: "room:" <> to_string(config.test)} - end - def assert_down(topic) do ref = Process.monitor(Process.whereis(:"#{topic}")) assert_receive {:DOWN, ^ref, :process, _pid, _} @@ -305,288 +283,341 @@ defmodule Phoenix.Integration.LongPollChannelsTest do } end - for mode <- [:local, :pubsub] do + for mode <- [:local, :pubsub], + adapter <- [Phoenix.Endpoint.Cowboy2Adapter, Bandit.PhoenixAdapter] do @mode mode @vsn "1.0.0" + @adapter adapter + + describe "for mode #{mode} running on #{inspect(adapter)}" do + setup do + Application.put_env(:phoenix, Endpoint, + adapter: @adapter, + https: false, + http: [port: @port], + secret_key_base: String.duplicate("abcdefgh", 8), + server: true, + pubsub_server: __MODULE__ + ) + + capture_log(fn -> start_supervised!(Endpoint) end) + start_supervised!({Phoenix.PubSub, name: __MODULE__, pool_size: @pool_size}) + :ok + end - test "#{@mode}: joins and poll messages" do - session = join("/ws", "room:lobby", @vsn, "1", @mode) - - # pull messages - resp = poll(:get, "/ws", @vsn, session) - assert resp.body["status"] == 200 - - [phx_reply, user_entered, status_msg] = resp.body["messages"] - - assert phx_reply == %Message{ - event: "phx_reply", - payload: %{"response" => %{}, "status" => "ok"}, - ref: "1", - topic: "room:lobby" - } - - assert %Message{ - event: "joined", - payload: %{"status" => "connected", "user_id" => nil}, - ref: nil, - join_ref: nil, - topic: "room:lobby" - } = status_msg - - assert user_entered == %Message{ - event: "user_entered", - payload: %{"user" => nil}, - ref: nil, - join_ref: nil, - topic: "room:lobby" - } - - # poll without messages sends 204 no_content - resp = poll(:get, "/ws", @vsn, session) - assert resp.body["status"] == 204 - end + setup config do + for {_, pid, _, _} <- + DynamicSupervisor.which_children(Phoenix.Transports.LongPoll.Supervisor) do + DynamicSupervisor.terminate_child(Phoenix.Transports.LongPoll.Supervisor, pid) + end - test "#{@mode}: transport x_headers are extracted to the socket connect_info" do - session = - join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, %{ - "x-application" => "Phoenix" - }) + {:ok, topic: "room:" <> to_string(config.test)} + end - # pull messages - resp = poll(:get, "/ws/connect_info", @vsn, session) - assert resp.body["status"] == 200 + test "#{@mode}: joins and poll messages" do + session = join("/ws", "room:lobby", @vsn, "1", @mode) - [_phx_reply, _user_entered, status_msg] = resp.body["messages"] + # pull messages + resp = poll(:get, "/ws", @vsn, session) + assert resp.body["status"] == 200 - assert %{"connect_info" => %{"x_headers" => %{"x-application" => "Phoenix"}}} = - status_msg.payload - end + [phx_reply, user_entered, status_msg] = resp.body["messages"] - test "#{@mode}: transport trace_context_headers are extracted to the socket connect_info" do - ctx_headers = %{ - "traceparent" => "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", - "tracestate" => "congo=t61rcWkgMz" - } + assert phx_reply == %Message{ + event: "phx_reply", + payload: %{"response" => %{}, "status" => "ok"}, + ref: "1", + topic: "room:lobby" + } - session = join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, ctx_headers) + assert %Message{ + event: "joined", + payload: %{"status" => "connected", "user_id" => nil}, + ref: nil, + join_ref: nil, + topic: "room:lobby" + } = status_msg + + assert user_entered == %Message{ + event: "user_entered", + payload: %{"user" => nil}, + ref: nil, + join_ref: nil, + topic: "room:lobby" + } - # pull messages - resp = poll(:get, "/ws/connect_info", @vsn, session) - assert resp.body["status"] == 200 + # poll without messages sends 204 no_content + resp = poll(:get, "/ws", @vsn, session) + assert resp.body["status"] == 204 + end - [_phx_reply, _user_entered, status_msg] = resp.body["messages"] + test "#{@mode}: transport x_headers are extracted to the socket connect_info" do + session = + join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, %{ + "x-application" => "Phoenix" + }) - assert %{"connect_info" => %{"trace_context_headers" => ^ctx_headers}} = status_msg.payload - end + # pull messages + resp = poll(:get, "/ws/connect_info", @vsn, session) + assert resp.body["status"] == 200 - test "#{@mode}: transport peer_data is extracted to the socket connect_info" do - session = - join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, %{ - "x-application" => "Phoenix" - }) + [_phx_reply, _user_entered, status_msg] = resp.body["messages"] - # pull messages - resp = poll(:get, "/ws/connect_info", @vsn, session) - assert resp.body["status"] == 200 + assert %{"connect_info" => %{"x_headers" => %{"x-application" => "Phoenix"}}} = + status_msg.payload + end - [_phx_reply, _user_entered, status_msg] = resp.body["messages"] + test "#{@mode}: transport trace_context_headers are extracted to the socket connect_info" do + ctx_headers = %{ + "traceparent" => "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + "tracestate" => "congo=t61rcWkgMz" + } - assert %{"connect_info" => %{"peer_data" => %{"address" => "127.0.0.1"}}} = - status_msg.payload - end + session = join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, ctx_headers) - test "#{@mode}: transport uri is extracted to the socket connect_info" do - session = - join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, %{ - "x-application" => "Phoenix" - }) - - # pull messages - resp = poll(:get, "/ws/connect_info", @vsn, session) - assert resp.body["status"] == 200 - - [_phx_reply, _user_entered, status_msg] = resp.body["messages"] - query = "vsn=#{@vsn}" - - assert %{ - "connect_info" => %{ - "uri" => %{ - "host" => "127.0.0.1", - "path" => "/ws/connect_info/longpoll", - "query" => ^query, - "scheme" => "http" - } - } - } = status_msg.payload - end + # pull messages + resp = poll(:get, "/ws/connect_info", @vsn, session) + assert resp.body["status"] == 200 - test "#{@mode}: publishing events" do - Phoenix.PubSub.subscribe(__MODULE__, "room:lobby") - session = join("/ws", "room:lobby", @vsn, "1", @mode) + [_phx_reply, _user_entered, status_msg] = resp.body["messages"] - # Publish successfully - resp = - poll(:post, "/ws", @vsn, session, %{ - "topic" => "room:lobby", - "event" => "new_msg", - "ref" => "1", - "payload" => %{"body" => "hi!"} - }) + assert %{"connect_info" => %{"trace_context_headers" => ^ctx_headers}} = status_msg.payload + end - assert resp.body["status"] == 200 - assert_receive %Broadcast{event: "new_msg", payload: %{"body" => "hi!"}} + test "#{@mode}: transport peer_data is extracted to the socket connect_info" do + session = + join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, %{ + "x-application" => "Phoenix" + }) - # Get published message - resp = poll(:get, "/ws", @vsn, session) - assert resp.body["status"] == 200 + # pull messages + resp = poll(:get, "/ws/connect_info", @vsn, session) + assert resp.body["status"] == 200 - assert List.last(resp.body["messages"]) == %Message{ - event: "new_msg", - payload: %{"transport" => ":longpoll", "body" => "hi!"}, - ref: nil, - join_ref: nil, - topic: "room:lobby" - } + [_phx_reply, _user_entered, status_msg] = resp.body["messages"] - # Publish event to an unjoined room - capture_log(fn -> - Phoenix.PubSub.subscribe(__MODULE__, "room:private-room") + assert %{"connect_info" => %{"peer_data" => %{"address" => "127.0.0.1"}}} = + status_msg.payload + end + test "#{@mode}: transport uri is extracted to the socket connect_info" do + session = + join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, %{ + "x-application" => "Phoenix" + }) + + # pull messages + resp = poll(:get, "/ws/connect_info", @vsn, session) + assert resp.body["status"] == 200 + + [_phx_reply, _user_entered, status_msg] = resp.body["messages"] + query = "vsn=#{@vsn}" + + assert %{ + "connect_info" => %{ + "uri" => %{ + "host" => "127.0.0.1", + "path" => "/ws/connect_info/longpoll", + "query" => ^query, + "scheme" => "http" + } + } + } = status_msg.payload + end + + test "#{@mode}: publishing events" do + Phoenix.PubSub.subscribe(__MODULE__, "room:lobby") + session = join("/ws", "room:lobby", @vsn, "1", @mode) + + # Publish successfully resp = poll(:post, "/ws", @vsn, session, %{ - "topic" => "room:private-room", + "topic" => "room:lobby", "event" => "new_msg", - "ref" => "12300", - "payload" => %{"body" => "this method shouldn't send!'"} + "ref" => "1", + "payload" => %{"body" => "hi!"} }) assert resp.body["status"] == 200 - refute_receive %Broadcast{event: "new_msg"} + assert_receive %Broadcast{event: "new_msg", payload: %{"body" => "hi!"}} - # Get join error + # Get published message resp = poll(:get, "/ws", @vsn, session) assert resp.body["status"] == 200 assert List.last(resp.body["messages"]) == %Message{ + event: "new_msg", + payload: %{"transport" => ":longpoll", "body" => "hi!"}, + ref: nil, join_ref: nil, - event: "phx_reply", - payload: %{"response" => %{"reason" => "unmatched topic"}, "status" => "error"}, - ref: "12300", - topic: "room:private-room" + topic: "room:lobby" } - end) - end - test "#{@mode}: lonpoll publishing batch events on v2 protocol" do - vsn = "2.0.0" - Phoenix.PubSub.subscribe(__MODULE__, "room:lobby") - session = join("/ws", "room:lobby", vsn, "1", @mode) - # Publish successfully - resp = - poll(:post, "/ws", vsn, session, [ - %{ - "topic" => "room:lobby", - "event" => "new_msg", - "ref" => "2", - "join_ref" => "1", - "payload" => %{"body" => "hi1"} - }, - %{ - "topic" => "room:lobby", - "event" => "new_msg", - "ref" => "3", - "join_ref" => "1", - "payload" => %{"body" => "hi2"} - } - ]) + # Publish event to an unjoined room + capture_log(fn -> + Phoenix.PubSub.subscribe(__MODULE__, "room:private-room") - assert resp.body["status"] == 200 - assert_receive %Broadcast{event: "new_msg", payload: %{"body" => "hi1"}} - assert_receive %Broadcast{event: "new_msg", payload: %{"body" => "hi2"}} + resp = + poll(:post, "/ws", @vsn, session, %{ + "topic" => "room:private-room", + "event" => "new_msg", + "ref" => "12300", + "payload" => %{"body" => "this method shouldn't send!'"} + }) - # Publish base64 binary successfully - resp = - poll(:post, "/ws", vsn, session, [ - %{ - "topic" => "room:lobby", - "event" => "bin", - "join_ref" => "1", - "ref" => "4", - "payload" => {:binary, <<1, 2, 3>>} - }, - %{ - "topic" => "room:lobby", - "event" => "new_msg", - "ref" => "5", - "join_ref" => "1", - "payload" => %{"body" => "hi3"} - } - ]) + assert resp.body["status"] == 200 + refute_receive %Broadcast{event: "new_msg"} - assert resp.body["status"] == 200 - assert_receive %Broadcast{event: "bin_ack", payload: %{}} - assert_receive %Broadcast{event: "new_msg", payload: %{"body" => "hi3"}} + # Get join error + resp = poll(:get, "/ws", @vsn, session) + assert resp.body["status"] == 200 - # Get published message - resp = poll(:get, "/ws", vsn, session) - assert resp.body["status"] == 200 + assert List.last(resp.body["messages"]) == %Message{ + join_ref: nil, + event: "phx_reply", + payload: %{"response" => %{"reason" => "unmatched topic"}, "status" => "error"}, + ref: "12300", + topic: "room:private-room" + } + end) + end - assert [ - _phx_reply, - _user_entered, - _joined, - %Message{ - topic: "room:lobby", - event: "new_msg", - payload: %{"body" => "hi1", "transport" => ":longpoll"}, - ref: nil, - join_ref: "1" - }, - %Message{ - topic: "room:lobby", - event: "new_msg", - payload: %{"body" => "hi2", "transport" => ":longpoll"}, - ref: nil, - join_ref: "1" - }, - %Message{ - topic: "room:lobby", - event: "bin_ack", - payload: %{"transport" => ":longpoll"}, - ref: nil, - join_ref: "1" - }, - %Message{ - topic: "room:lobby", - event: "new_msg", - payload: %{"body" => "hi3", "transport" => ":longpoll"}, - ref: nil, - join_ref: "1" - } - ] = resp.body["messages"] - end + test "#{@mode}: lonpoll publishing batch events on v2 protocol" do + vsn = "2.0.0" + Phoenix.PubSub.subscribe(__MODULE__, "room:lobby") + session = join("/ws", "room:lobby", vsn, "1", @mode) + # Publish successfully + resp = + poll(:post, "/ws", vsn, session, [ + %{ + "topic" => "room:lobby", + "event" => "new_msg", + "ref" => "2", + "join_ref" => "1", + "payload" => %{"body" => "hi1"} + }, + %{ + "topic" => "room:lobby", + "event" => "new_msg", + "ref" => "3", + "join_ref" => "1", + "payload" => %{"body" => "hi2"} + } + ]) + + assert resp.body["status"] == 200 + assert_receive %Broadcast{event: "new_msg", payload: %{"body" => "hi1"}} + assert_receive %Broadcast{event: "new_msg", payload: %{"body" => "hi2"}} + + # Publish base64 binary successfully + resp = + poll(:post, "/ws", vsn, session, [ + %{ + "topic" => "room:lobby", + "event" => "bin", + "join_ref" => "1", + "ref" => "4", + "payload" => {:binary, <<1, 2, 3>>} + }, + %{ + "topic" => "room:lobby", + "event" => "new_msg", + "ref" => "5", + "join_ref" => "1", + "payload" => %{"body" => "hi3"} + } + ]) + + assert resp.body["status"] == 200 + assert_receive %Broadcast{event: "bin_ack", payload: %{}} + assert_receive %Broadcast{event: "new_msg", payload: %{"body" => "hi3"}} - test "#{@mode}: shuts down after timeout" do - session = join("/ws", "room:lobby", @vsn, "1", @mode) + # Get published message + resp = poll(:get, "/ws", vsn, session) + assert resp.body["status"] == 200 - channel = Process.whereis(:"room:lobby") - assert channel - Process.monitor(channel) + assert [ + _phx_reply, + _user_entered, + _joined, + %Message{ + topic: "room:lobby", + event: "new_msg", + payload: %{"body" => "hi1", "transport" => ":longpoll"}, + ref: nil, + join_ref: "1" + }, + %Message{ + topic: "room:lobby", + event: "new_msg", + payload: %{"body" => "hi2", "transport" => ":longpoll"}, + ref: nil, + join_ref: "1" + }, + %Message{ + topic: "room:lobby", + event: "bin_ack", + payload: %{"transport" => ":longpoll"}, + ref: nil, + join_ref: "1" + }, + %Message{ + topic: "room:lobby", + event: "new_msg", + payload: %{"body" => "hi3", "transport" => ":longpoll"}, + ref: nil, + join_ref: "1" + } + ] = resp.body["messages"] + end - assert_receive({:DOWN, _, :process, ^channel, {:shutdown, :inactive}}, 5000) - resp = poll(:post, "/ws", @vsn, session) - assert resp.body["status"] == 410 + test "#{@mode}: shuts down after timeout" do + session = join("/ws", "room:lobby", @vsn, "1", @mode) + + channel = Process.whereis(:"room:lobby") + assert channel + Process.monitor(channel) + + assert_receive({:DOWN, _, :process, ^channel, {:shutdown, :inactive}}, 5000) + resp = poll(:post, "/ws", @vsn, session) + assert resp.body["status"] == 410 + end end end for {serializer, vsn, join_ref} <- [ {V1.JSONSerializer, "1.0.0", nil}, {V2.JSONSerializer, "2.0.0", "1"} - ] do + ], + adapter <- [Phoenix.Endpoint.Cowboy2Adapter, Bandit.PhoenixAdapter] do @vsn vsn @join_ref join_ref - describe "with #{vsn} serializer #{inspect(serializer)}" do + describe "with #{vsn} serializer #{inspect(serializer)} on adapter #{inspect(adapter)}" do + setup do + Application.put_env(:phoenix, Endpoint, + adapter: @adapter, + https: false, + http: [port: @port], + secret_key_base: String.duplicate("abcdefgh", 8), + server: true, + pubsub_server: __MODULE__ + ) + + capture_log(fn -> start_supervised!(Endpoint) end) + start_supervised!({Phoenix.PubSub, name: __MODULE__, pool_size: @pool_size}) + :ok + end + + setup config do + for {_, pid, _, _} <- + DynamicSupervisor.which_children(Phoenix.Transports.LongPoll.Supervisor) do + DynamicSupervisor.terminate_child(Phoenix.Transports.LongPoll.Supervisor, pid) + end + + {:ok, topic: "room:" <> to_string(config.test)} + end + test "refuses connects that error with 403 response" do resp = poll(:get, "/ws", @vsn, %{"reject" => "true"}, %{}) assert resp.body["status"] == 403 diff --git a/test/phoenix/integration/long_poll_socket_test.exs b/test/phoenix/integration/long_poll_socket_test.exs index 47357f66c3..5502d7db23 100644 --- a/test/phoenix/integration/long_poll_socket_test.exs +++ b/test/phoenix/integration/long_poll_socket_test.exs @@ -11,17 +11,6 @@ defmodule Phoenix.Integration.LongPollSocketTest do @port 5908 @pool_size 1 - Application.put_env( - :phoenix, - Endpoint, - https: false, - http: [port: @port], - debug_errors: false, - secret_key_base: String.duplicate("abcdefgh", 8), - server: true, - pubsub_server: __MODULE__ - ) - defmodule UserSocket do @behaviour Phoenix.Socket.Transport @@ -71,20 +60,6 @@ defmodule Phoenix.Integration.LongPollSocketTest do custom: :value end - setup_all do - capture_log(fn -> start_supervised! Endpoint end) - start_supervised! {Phoenix.PubSub, name: __MODULE__, pool_size: @pool_size} - :ok - end - - setup do - for {_, pid, _, _} <- DynamicSupervisor.which_children(Phoenix.Transports.LongPoll.Supervisor) do - DynamicSupervisor.terminate_child(Phoenix.Transports.LongPoll.Supervisor, pid) - end - - :ok - end - def poll(method, path, params, body \\ nil, headers \\ %{}) do headers = Map.merge(%{"content-type" => "application/json"}, headers) url = "http://127.0.0.1:#{@port}/#{path}?" <> URI.encode_query(params) @@ -92,56 +67,87 @@ defmodule Phoenix.Integration.LongPollSocketTest do update_in(resp.body, &Phoenix.json_library().decode!(&1)) end - test "refuses unallowed origins" do - capture_log(fn -> - resp = poll(:get, "ws/longpoll", %{}, nil, %{"origin" => "https://example.com"}) - assert resp.body["status"] == 410 - - resp = poll(:get, "ws/longpoll", %{}, nil, %{"origin" => "http://notallowed.com"}) - assert resp.body["status"] == 403 - end) - end - - test "returns params with sync request" do - resp = poll(:get, "ws/longpoll", %{"hello" => "world"}, nil) - assert resp.body["token"] - assert resp.body["status"] == 410 - assert resp.status == 200 - secret = Map.take(resp.body, ["token"]) - resp = poll(:post, "ws/longpoll", secret, "params") - assert resp.body["status"] == 200 - - resp = poll(:get, "ws/longpoll", secret, nil) - assert resp.body["messages"] == [~s(%{"hello" => "world"})] - end - - test "allows a path with variables" do - path = "custom/123/456/path" - resp = poll(:get, path, %{"key" => "value"}, nil) - secret = Map.take(resp.body, ["token"]) - - resp = poll(:post, path, secret, "params") - assert resp.body["status"] == 200 - - resp = poll(:get, path, secret, nil) - [params] = resp.body["messages"] - assert params =~ ~s("key" => "value") - assert params =~ ~s("socket_var" => "123") - assert params =~ ~s(path_var" => "456") - end - - test "returns pong from async request" do - resp = poll(:get, "ws/longpoll", %{"hello" => "world"}, nil) - assert resp.body["token"] - assert resp.body["status"] == 410 - assert resp.status == 200 - secret = Map.take(resp.body, ["token"]) - - resp = poll(:post, "ws/longpoll", secret, "ping") - assert resp.body["status"] == 200 - - resp = poll(:get, "ws/longpoll", secret, nil) - assert resp.body["messages"] == ["pong"] + for adapter <- [Phoenix.Endpoint.Cowboy2Adapter, Bandit.PhoenixAdapter] do + @adapter adapter + + describe "running on #{inspect(adapter)}" do + setup do + Application.put_env(:phoenix, Endpoint, + adapter: @adapter, + https: false, + http: [port: @port], + debug_errors: false, + secret_key_base: String.duplicate("abcdefgh", 8), + server: true, + pubsub_server: __MODULE__ + ) + + capture_log(fn -> start_supervised! Endpoint end) + start_supervised! {Phoenix.PubSub, name: __MODULE__, pool_size: @pool_size} + :ok + end + + setup do + for {_, pid, _, _} <- DynamicSupervisor.which_children(Phoenix.Transports.LongPoll.Supervisor) do + DynamicSupervisor.terminate_child(Phoenix.Transports.LongPoll.Supervisor, pid) + end + + :ok + end + + test "refuses unallowed origins" do + capture_log(fn -> + resp = poll(:get, "ws/longpoll", %{}, nil, %{"origin" => "https://example.com"}) + assert resp.body["status"] == 410 + + resp = poll(:get, "ws/longpoll", %{}, nil, %{"origin" => "http://notallowed.com"}) + assert resp.body["status"] == 403 + end) + end + + test "returns params with sync request" do + resp = poll(:get, "ws/longpoll", %{"hello" => "world"}, nil) + assert resp.body["token"] + assert resp.body["status"] == 410 + assert resp.status == 200 + secret = Map.take(resp.body, ["token"]) + + resp = poll(:post, "ws/longpoll", secret, "params") + assert resp.body["status"] == 200 + + resp = poll(:get, "ws/longpoll", secret, nil) + assert resp.body["messages"] == [~s(%{"hello" => "world"})] + end + + test "allows a path with variables" do + path = "custom/123/456/path" + resp = poll(:get, path, %{"key" => "value"}, nil) + secret = Map.take(resp.body, ["token"]) + + resp = poll(:post, path, secret, "params") + assert resp.body["status"] == 200 + + resp = poll(:get, path, secret, nil) + [params] = resp.body["messages"] + assert params =~ ~s("key" => "value") + assert params =~ ~s("socket_var" => "123") + assert params =~ ~s(path_var" => "456") + end + + test "returns pong from async request" do + resp = poll(:get, "ws/longpoll", %{"hello" => "world"}, nil) + assert resp.body["token"] + assert resp.body["status"] == 410 + assert resp.status == 200 + secret = Map.take(resp.body, ["token"]) + + resp = poll(:post, "ws/longpoll", secret, "ping") + assert resp.body["status"] == 200 + + resp = poll(:get, "ws/longpoll", secret, nil) + assert resp.body["messages"] == ["pong"] + end + end end end diff --git a/test/phoenix/integration/websocket_channels_test.exs b/test/phoenix/integration/websocket_channels_test.exs index ac8f62f1d7..be3ebba92d 100644 --- a/test/phoenix/integration/websocket_channels_test.exs +++ b/test/phoenix/integration/websocket_channels_test.exs @@ -1,7 +1,7 @@ Code.require_file "../../support/websocket_client.exs", __DIR__ defmodule Phoenix.Integration.WebSocketChannelsTest do - use ExUnit.Case + use ExUnit.Case, async: false import ExUnit.CaptureLog alias Phoenix.Integration.WebsocketClient @@ -9,16 +9,6 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do alias __MODULE__.Endpoint @moduletag :capture_log - @port 5807 - - Application.put_env(:phoenix, Endpoint, [ - https: false, - http: [port: @port], - debug_errors: false, - server: true, - pubsub_server: __MODULE__, - secret_key_base: String.duplicate("a", 64) - ]) defp lobby do "room:lobby#{System.unique_integer()}" @@ -205,21 +195,37 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do end end - setup_all do - capture_log fn -> start_supervised! Endpoint end - start_supervised! {Phoenix.PubSub, name: __MODULE__} - :ok - end - @endpoint Endpoint - for {serializer, vsn, join_ref} <- [{V1.JSONSerializer, "1.0.0", nil}, {V2.JSONSerializer, "2.0.0", "11"}] do + for {serializer, vsn, join_ref} <- [ + {V1.JSONSerializer, "1.0.0", nil}, + {V2.JSONSerializer, "2.0.0", "11"} + ], + adapter <- [Phoenix.Endpoint.Cowboy2Adapter, Bandit.PhoenixAdapter] do + @port 5807 @serializer serializer @vsn vsn @vsn_path "ws://127.0.0.1:#{@port}/ws/websocket?vsn=#{@vsn}" @join_ref join_ref + @adapter adapter + + describe "with #{vsn} serializer #{inspect(serializer)} on #{inspect(adapter)}" do + setup do + Application.put_env(:phoenix, Endpoint, + adapter: @adapter, + https: false, + http: [port: @port], + debug_errors: false, + server: true, + pubsub_server: __MODULE__, + secret_key_base: String.duplicate("a", 64) + ) + + capture_log(fn -> start_supervised!(Endpoint) end) + start_supervised!({Phoenix.PubSub, name: __MODULE__}) + :ok + end - describe "with #{vsn} serializer #{inspect serializer}" do test "endpoint handles multiple mount segments" do {:ok, sock} = WebsocketClient.connect(self(), "ws://127.0.0.1:#{@port}/ws/admin/websocket?vsn=#{@vsn}", @serializer) WebsocketClient.join(sock, "room:admin-lobby1", %{}) @@ -482,7 +488,11 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do WebsocketClient.close(sock) assert_receive {:DOWN, _, :process, ^channel, shutdown} - when shutdown in [:shutdown, {:shutdown, :closed}] + when shutdown in [ + :shutdown, + {:shutdown, :closed}, + {:shutdown, :local_closed} + ] end test "refuses websocket events that haven't joined" do @@ -539,10 +549,12 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do assert_receive {:DOWN, _, :process, ^sock, :normal} assert_receive {:DOWN, _, :process, ^chan1, shutdown} - #shutdown for cowboy, {:shutdown, :closed} for cowboy 2 - assert shutdown in [:shutdown, {:shutdown, :closed}] + # shutdown for cowboy + # {:shutdown, :closed} for cowboy 2 + # {:shutdown, :disconnected} for Bandit + assert shutdown in [:shutdown, {:shutdown, :closed}, {:shutdown, :disconnected}] assert_receive {:DOWN, _, :process, ^chan2, shutdown} - assert shutdown in [:shutdown, {:shutdown, :closed}] + assert shutdown in [:shutdown, {:shutdown, :closed}, {:shutdown, :disconnected}] end test "duplicate join event closes existing channel" do @@ -604,6 +616,12 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do @vsn "2.0.0" @vsn_path "ws://127.0.0.1:#{@port}/ws/websocket?vsn=#{@vsn}" + setup do + capture_log(fn -> start_supervised!(Endpoint) end) + start_supervised!({Phoenix.PubSub, name: __MODULE__}) + :ok + end + test "join, ignore, error, and event messages" do {:ok, sock} = WebsocketClient.connect(self(), @vsn_path, @serializer) @@ -641,6 +659,12 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do @vsn "2.0.0" @join_ref "11" + setup do + capture_log(fn -> start_supervised!(Endpoint) end) + start_supervised!({Phoenix.PubSub, name: __MODULE__}) + :ok + end + test "messages can be pushed and received" do topic = "room:bin" diff --git a/test/phoenix/integration/websocket_socket_test.exs b/test/phoenix/integration/websocket_socket_test.exs index aa249f4138..d2562e66bd 100644 --- a/test/phoenix/integration/websocket_socket_test.exs +++ b/test/phoenix/integration/websocket_socket_test.exs @@ -1,24 +1,14 @@ Code.require_file("../../support/websocket_client.exs", __DIR__) +Code.require_file("../../support/http_client.exs", __DIR__) defmodule Phoenix.Integration.WebSocketTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false import ExUnit.CaptureLog alias Phoenix.Integration.{HTTPClient, WebsocketClient} alias __MODULE__.Endpoint @moduletag :capture_log - @port 5907 - @path "ws://127.0.0.1:#{@port}/ws/websocket" - - Application.put_env( - :phoenix, - Endpoint, - https: false, - http: [port: @port], - debug_errors: false, - server: true - ) defmodule UserSocket do @behaviour Phoenix.Socket.Transport @@ -65,7 +55,7 @@ defmodule Phoenix.Integration.WebSocketTest do def init(state), do: {:ok, state} def handle_in({"ping:start", _}, state) do - {:reply, :ok, :ping, state} + {:reply, :ok, {:ping, ""}, state} end def handle_in({"ping:start:" <> payload, _}, state) do {:reply, :ok, {:ping, payload}, state} @@ -99,95 +89,111 @@ defmodule Phoenix.Integration.WebSocketTest do websocket: true end - setup_all do - capture_log(fn -> Endpoint.start_link() end) - :ok - end - - test "handles invalid upgrade requests" do - capture_log(fn -> - path = String.replace_prefix(@path, "ws", "http") - assert {:ok, %{body: body, status: 400}} = HTTPClient.request(:get, path, %{}) - assert body =~ "'connection' header must contain 'upgrade'" - end) - end - - test "refuses unallowed origins" do - capture_log(fn -> - headers = [{"origin", "https://example.com"}] - assert {:ok, _} = WebsocketClient.connect(self(), @path, :noop, headers) - - headers = [{"origin", "http://notallowed.com"}] - assert {:error, %Mint.WebSocket.UpgradeFailureError{status_code: 403}} = - WebsocketClient.connect(self(), @path, :noop, headers) - end) - end - - test "refuses unallowed Websocket subprotocols" do - assert capture_log(fn -> - headers = [{"sec-websocket-protocol", "sip"}] - assert {:ok, _} = WebsocketClient.connect(self(), @path, :noop, headers) - - headers = [] - assert {:ok, _} = WebsocketClient.connect(self(), @path, :noop, headers) - - headers = [{"sec-websocket-protocol", "mqtt"}] - assert {:error, %Mint.WebSocket.UpgradeFailureError{status_code: 403}} = - WebsocketClient.connect(self(), @path, :noop, headers) - end) =~ "Could not check Websocket subprotocols" - end - - test "returns params with sync request" do - assert {:ok, client} = WebsocketClient.connect(self(), "#{@path}?key=value", :noop) - WebsocketClient.send(client, {:text, "params"}) - assert_receive {:text, ~s(%{"key" => "value"})} - end - - test "ignores control frames when handle_control/2 is not defined" do - assert {:ok, client} = WebsocketClient.connect(self(), @path, :noop) - WebsocketClient.send(client, :ping) - WebsocketClient.send(client, {:text, "ping"}) - assert_receive {:text, "pong"} - end - - test "returns pong from async request" do - assert {:ok, client} = WebsocketClient.connect(self(), "#{@path}?key=value", :noop) - WebsocketClient.send(client, {:text, "ping"}) - assert_receive {:text, "pong"} - end - - test "allows a custom path" do - path = "ws://127.0.0.1:#{@port}/custom/some_path/nested/path" - assert {:ok, _} = WebsocketClient.connect(self(), "#{path}?key=value", :noop) - end - - test "allows a path with variables" do - path = "ws://127.0.0.1:#{@port}/custom/123/456/path" - assert {:ok, client} = WebsocketClient.connect(self(), "#{path}?key=value", :noop) - WebsocketClient.send(client, {:text, "params"}) - assert_receive {:text, params} - assert params =~ ~s("key" => "value") - assert params =~ ~s("socket_var" => "123") - assert params =~ ~s(path_var" => "456") - end - - test "allows using control frames with a payload" do - path = "ws://127.0.0.1:#{@port}/ws/ping/websocket" - assert {:ok, client} = WebsocketClient.connect(self(), path, :noop) - WebsocketClient.send(client, {:ping, ""}) - assert_receive {:pong, ""} - assert_receive {:text, "ping:"} - - WebsocketClient.send(client, {:ping, "123"}) - assert_receive {:pong, "123"} - assert_receive {:text, "ping:123"} - - WebsocketClient.send(client, {:text, "ping:start"}) - assert_receive {:ping, ""} - assert_receive {:text, "pong:"} - - WebsocketClient.send(client, {:text, "ping:start:123"}) - assert_receive {:ping, "123"} - assert_receive {:text, "pong:123"} + for adapter <- [Phoenix.Endpoint.Cowboy2Adapter, Bandit.PhoenixAdapter] do + @port 5907 + @adapter adapter + @path "ws://127.0.0.1:#{@port}/ws/websocket" + + describe "running on #{inspect(adapter)}" do + setup do + Application.put_env(:phoenix, Endpoint, + adapter: @adapter, + https: false, + http: [port: @port], + debug_errors: false, + server: true + ) + + capture_log(fn -> start_supervised!(Endpoint) end) + :ok + end + + test "handles invalid upgrade requests" do + capture_log(fn -> + path = String.replace_prefix(@path, "ws", "http") + assert {:ok, %{body: body, status: 400}} = HTTPClient.request(:get, path, %{}) + assert body =~ "'connection' header must contain 'upgrade'" + end) + end + + test "refuses unallowed origins" do + capture_log(fn -> + headers = [{"origin", "https://example.com"}] + assert {:ok, _} = WebsocketClient.connect(self(), @path, :noop, headers) + + headers = [{"origin", "http://notallowed.com"}] + assert {:error, %Mint.WebSocket.UpgradeFailureError{status_code: 403}} = + WebsocketClient.connect(self(), @path, :noop, headers) + end) + end + + test "refuses unallowed Websocket subprotocols" do + assert capture_log(fn -> + headers = [{"sec-websocket-protocol", "sip"}] + assert {:ok, _} = WebsocketClient.connect(self(), @path, :noop, headers) + + headers = [] + assert {:ok, _} = WebsocketClient.connect(self(), @path, :noop, headers) + + headers = [{"sec-websocket-protocol", "mqtt"}] + assert {:error, %Mint.WebSocket.UpgradeFailureError{status_code: 403}} = + WebsocketClient.connect(self(), @path, :noop, headers) + end) =~ "Could not check Websocket subprotocols" + end + + test "returns params with sync request" do + assert {:ok, client} = WebsocketClient.connect(self(), "#{@path}?key=value", :noop) + WebsocketClient.send(client, {:text, "params"}) + assert_receive {:text, ~s(%{"key" => "value"})} + end + + test "ignores control frames when handle_control/2 is not defined" do + assert {:ok, client} = WebsocketClient.connect(self(), @path, :noop) + WebsocketClient.send(client, :ping) + WebsocketClient.send(client, {:text, "ping"}) + assert_receive {:text, "pong"} + end + + test "returns pong from async request" do + assert {:ok, client} = WebsocketClient.connect(self(), "#{@path}?key=value", :noop) + WebsocketClient.send(client, {:text, "ping"}) + assert_receive {:text, "pong"} + end + + test "allows a custom path" do + path = "ws://127.0.0.1:#{@port}/custom/some_path/nested/path" + assert {:ok, _} = WebsocketClient.connect(self(), "#{path}?key=value", :noop) + end + + test "allows a path with variables" do + path = "ws://127.0.0.1:#{@port}/custom/123/456/path" + assert {:ok, client} = WebsocketClient.connect(self(), "#{path}?key=value", :noop) + WebsocketClient.send(client, {:text, "params"}) + assert_receive {:text, params} + assert params =~ ~s("key" => "value") + assert params =~ ~s("socket_var" => "123") + assert params =~ ~s(path_var" => "456") + end + + test "allows using control frames with a payload" do + path = "ws://127.0.0.1:#{@port}/ws/ping/websocket" + assert {:ok, client} = WebsocketClient.connect(self(), path, :noop) + WebsocketClient.send(client, {:ping, ""}) + assert_receive {:pong, ""} + assert_receive {:text, "ping:"} + + WebsocketClient.send(client, {:ping, "123"}) + assert_receive {:pong, "123"} + assert_receive {:text, "ping:123"} + + WebsocketClient.send(client, {:text, "ping:start"}) + assert_receive {:ping, ""} + assert_receive {:text, "pong:"} + + WebsocketClient.send(client, {:text, "ping:start:123"}) + assert_receive {:ping, "123"} + assert_receive {:text, "pong:123"} + end + end end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 3202405518..30da6d2db4 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,6 +2,7 @@ Code.require_file("support/router_helper.exs", __DIR__) # Starts web server applications Application.ensure_all_started(:plug_cowboy) +Application.ensure_all_started(:bandit) # Used whenever a router fails. We default to simply # rendering a short string.