diff --git a/CHANGELOG.md b/CHANGELOG.md index 63601db..fa6e46d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,11 @@ Requires Elixir 1.15 or higher. All public functions in `TestServer` have been moved to `TestServer.HTTP`. The HTTP server adapters in `TestServer.HTTPServer.*` have been moved to -`TestServer.HTTP.Server.*`. +`TestServer.HTTP.Server.*`. `add/2` has been renamed to `handle/2`, and `websocket_info/2` has been renamed to `websocket_send/2` and now takes options as second argument. TestServer now has SSH support with `TestServer.SSH`. -- Fixed bug where `:match` functions that raised errors always matched in `TestServer.HTTP.add/2` and `TestServer.HTTP.websocket_handle/3` +- Fixed bug where `:match` functions that raised errors always matched in `TestServer.HTTP.handle/2` and `TestServer.HTTP.websocket_handle/2` - Fixed UTF-8 response body handling for `TestServer.HTTP.Server.Httpd` - Fixed invalid host header port parsing in `TestServer.HTTP.Server.Httpd` - Fixed bracketed IPv6 host header parsing in `TestServer.HTTP.Server.Httpd` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cc5554f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing + +Thanks for considering contributing to TestServer! + +## Test suite + +TestServer supports several HTTP server adapter. The adapter is selected via the `HTTP_SERVER` environment variable: + +```bash +HTTP_SERVER=Bandit mix test +HTTP_SERVER=Plug.Cowboy mix test +HTTP_SERVER=Httpd mix test +``` + +CI runs the [full matrix](.github/workflows/ci.yml) across `Bandit`, `Plug.Cowboy`, and `:httpd`. + +## Code quality + +Elixir formatter, `Credo`, and `:dialyzer` are used: + +```bash +mix compile --warnings-as-errors +mix format --check-formatted +mix credo --strict +mix dialyzer +``` + +## Protocol architecture + +TestServer consists of different protocols with normally these functions: + +* `start(options)` - Starts the instance the server +* `stop(instance)` - Stops the server and the instance +* `handle(instance, options)` - Adds an expectation handler. Takes `:to` and `:match` function option. + +If the protocol is bidirectional you would also have: + +* `send(instance, options)` - Sends message to the active process and handles the reply the same way as `handle`. Takes `:to` function option. + +### Option conventions + +- Options should mirror the underlying OTP option names verbatim when it makes sense (e.g. `:ipfamily` or `:no_auth_needed`) +- Options should otherwise use Elixir conventions like snake case (`:listen_options`, `:recv_timeout`). + +## Submitting a PR + +- Write a focused pull request description and link any related issue +- Update `CHANGELOG.md` under `## Unreleased` +- If behavior changes, add or update tests +- Ensure the full adapter matrix passes locally before pushing \ No newline at end of file diff --git a/lib/test_server/http.ex b/lib/test_server/http.ex index 0a98028..ab82f99 100644 --- a/lib/test_server/http.ex +++ b/lib/test_server/http.ex @@ -9,12 +9,25 @@ defmodule TestServer.HTTP do alias TestServer.HTTP.{Instance, Server} @type route :: reference() + @type handler_fun :: (conn :: Plug.Conn.t() -> Plug.Conn.t()) + @type plug_module :: module() + @type plug_fun :: (conn :: Plug.Conn.t() -> Plug.Conn.t()) + @type match_fun :: (conn :: Plug.Conn.t() -> boolean()) + @type websocket_socket :: {TestServer.instance(), route()} - @type websocket_frame :: {atom(), any()} - @type websocket_state :: any() + @type websocket_frame :: {:text | :binary, iodata()} + @type websocket_state :: term() + @type websocket_handler_fun :: (frame :: websocket_frame(), state :: websocket_state() -> + websocket_reply()) + @type websocket_match_fun :: (frame :: websocket_frame(), state :: websocket_state() -> + boolean()) + @type websocket_send_fun :: (state :: websocket_state() -> websocket_reply()) + @type websocket_reply :: {:reply, websocket_frame(), websocket_state()} | {:ok, websocket_state()} + @type x509_suite :: %{cert: binary(), cacerts: [binary()]} + @doc """ Start a test server HTTP instance. @@ -24,17 +37,24 @@ defmodule TestServer.HTTP do * `:port` - integer of port number, defaults to random port that can be opened; + * `:scheme` - an atom for the http scheme. Defaults to `:http`; + * `:http_server` - HTTP server configuration. Defaults to `{TestServer.HTTP.Server.Bandit, []}`, `{TestServer.HTTP.Server.Plug.Cowboy, []}`, or `{TestServer.HTTP.Server.Httpd, []}` depending on which web server is available in the project dependencies; + * `:tls` - Passthru options for TLS configuration handled by the webserver; + * `:ipfamily` - The IP address type to use, either `:inet` or `:inet6`. Defaults to `:inet`; + * `:suppress_warning` - Suppresses IO warnings on expectation failures + related to this instance. Defaults to `false`; + ## Examples TestServer.HTTP.start( @@ -43,7 +63,7 @@ defmodule TestServer.HTTP do http_server: {TestServer.HTTP.Server.Bandit, [ip: :any]} ) - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} @@ -61,7 +81,7 @@ defmodule TestServer.HTTP do assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} = Req.get(TestServer.HTTP.url(), req_options) """ - @spec start(keyword()) :: {:ok, pid()} + @spec start(keyword()) :: {:ok, TestServer.instance()} def start(options \\ []) do TestServer.start_instance(__MODULE__, options, &verify_instance!/1) end @@ -122,7 +142,7 @@ defmodule TestServer.HTTP do @doc """ Shuts down a test server instance. """ - @spec stop(pid()) :: :ok | {:error, term()} + @spec stop(TestServer.instance()) :: :ok | {:error, term()} def stop(instance) do TestServer.ensure_instance_alive!(__MODULE__, instance) @@ -134,7 +154,7 @@ defmodule TestServer.HTTP do @spec url() :: binary() def url, do: url("") - @spec url(binary() | keyword() | pid()) :: binary() + @spec url(binary() | keyword() | TestServer.instance()) :: binary() def url(uri) when is_binary(uri), do: url(uri, []) def url(options) when is_list(options), do: url("", options) def url(instance) when is_pid(instance), do: url(instance, "", []) @@ -159,7 +179,7 @@ defmodule TestServer.HTTP do def url(uri, options) when is_binary(uri), do: url(TestServer.fetch_instance!(__MODULE__), uri, options) - @spec url(pid(), binary()) :: binary() + @spec url(TestServer.instance(), binary()) :: binary() def url(instance, uri) when is_pid(instance), do: url(instance, uri, []) @doc """ @@ -167,7 +187,7 @@ defmodule TestServer.HTTP do See `url/2` for options. """ - @spec url(pid(), binary(), keyword()) :: binary() + @spec url(TestServer.instance(), binary(), keyword()) :: binary() def url(instance, uri, options) do TestServer.ensure_instance_alive!(__MODULE__, instance) @@ -177,8 +197,8 @@ defmodule TestServer.HTTP do "#{Keyword.fetch!(options, :scheme)}://#{domain}:#{Keyword.fetch!(options, :port)}#{uri}" end - @spec add(binary()) :: :ok - def add(uri), do: add(uri, []) + @spec handle(binary()) :: :ok + def handle(uri), do: handle(uri, []) @doc """ Adds a route to the current test server. @@ -191,14 +211,16 @@ defmodule TestServer.HTTP do * `:via` - matches the route against some specific HTTP method(s) specified as an atom, like `:get` or `:put`, or a list, like `[:get, :post]`; - * `:match` - an anonymous function that will be called to see if a - route matches, defaults to matching with arguments of uri and `:via` option; - * `:to` - a Plug or anonymous function that will be called when the - route matches, defaults to return the http scheme; + + * `:match` - an `t:match_fun/0` function that returns a boolean. + Defaults to matching with arguments of uri and `:via` option; + + * `:to` - a `t:handler_fun/0` or `t:plug_module/0` that will be + called when the route matches, defaults to return the http scheme; ## Examples - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", match: fn conn -> conn.query_params["a"] == "1" end, @@ -206,30 +228,32 @@ defmodule TestServer.HTTP do Plug.Conn.resp(conn, 200, "a = 1") end) - TestServer.HTTP.add("/", to: &Plug.Conn.resp(&1, 200, "PONG")) - TestServer.HTTP.add("/") + TestServer.HTTP.handle("/", to: &Plug.Conn.resp(&1, 200, "PONG")) + TestServer.HTTP.handle("/") assert {:ok, %Req.Response{status: 200, body: "PONG"}} = Req.get(TestServer.HTTP.url("/")) assert {:ok, %Req.Response{status: 200, body: "HTTP/1.1"}} = Req.post(TestServer.HTTP.url("/")) assert {:ok, %Req.Response{status: 200, body: "a = 1"}} = Req.get(TestServer.HTTP.url("/?a=1")) """ - @spec add(binary(), keyword()) :: :ok - def add(uri, options) when is_binary(uri) do + @spec handle(binary(), keyword()) :: :ok + def handle(uri, options) when is_binary(uri) do {:ok, instance} = TestServer.autostart_instance(__MODULE__) - add(instance, uri, options) + handle(instance, uri, options) end - @spec add(pid(), binary()) :: :ok - def add(instance, uri) when is_pid(instance) and is_binary(uri), do: add(instance, uri, []) + @spec handle(TestServer.instance(), binary()) :: :ok + def handle(instance, uri) when is_pid(instance) and is_binary(uri), + do: handle(instance, uri, []) @doc """ Adds a route to a test server instance. - See `add/2` for options. + See `handle/2` for options. """ - @spec add(pid(), binary(), keyword()) :: :ok - def add(instance, uri, options) when is_pid(instance) and is_binary(uri) and is_list(options) do + @spec handle(TestServer.instance(), binary(), keyword()) :: :ok + def handle(instance, uri, options) + when is_pid(instance) and is_binary(uri) and is_list(options) do options = Keyword.put_new(options, :to, &default_response_handler/1) {:ok, _route} = register_route(instance, uri, options) @@ -265,7 +289,7 @@ defmodule TestServer.HTTP do %{conn | body_params: Jason.decode!(body)} end) """ - @spec plug(module() | function()) :: :ok + @spec plug(plug_module() | plug_fun()) :: :ok def plug(plug) do {:ok, instance} = TestServer.autostart_instance(__MODULE__) @@ -273,11 +297,11 @@ defmodule TestServer.HTTP do end @doc """ - Adds a route to a test server instance. + Adds a plug to a test server instance. See `plug/1` for more. """ - @spec plug(pid(), module() | function()) :: :ok + @spec plug(TestServer.instance(), plug_module() | plug_fun()) :: :ok def plug(instance, plug) do [_first_module_entry | stacktrace] = TestServer.get_pruned_stacktrace(__MODULE__) @@ -292,15 +316,15 @@ defmodule TestServer.HTTP do ## Examples TestServer.HTTP.start(scheme: :https) - TestServer.HTTP.add("/") + TestServer.HTTP.handle("/") - cacerts = TestServer.HTTP.x509_suite().cacerts + %{cert: _, cacerts: cacerts} = TestServer.HTTP.x509_suite() req_options = [connect_options: [transport_opts: [cacerts: cacerts]]] assert {:ok, %Req.Response{status: 200, body: "HTTP/1.1"}} = Req.get(TestServer.HTTP.url(), req_options) """ - @spec x509_suite() :: term() + @spec x509_suite() :: x509_suite() def x509_suite, do: x509_suite(TestServer.fetch_instance!(__MODULE__)) @doc """ @@ -308,7 +332,7 @@ defmodule TestServer.HTTP do See `x509_suite/0` for more. """ - @spec x509_suite(pid()) :: term() + @spec x509_suite(TestServer.instance()) :: x509_suite() def x509_suite(instance) do TestServer.ensure_instance_alive!(__MODULE__, instance) @@ -326,19 +350,19 @@ defmodule TestServer.HTTP do end end - @spec websocket_init(binary()) :: {:ok, websocket_socket()} | {:error, term()} + @spec websocket_init(binary()) :: {:ok, websocket_socket()} def websocket_init(uri) when is_binary(uri), do: websocket_init(uri, []) @doc """ Adds a websocket route to current test server. - The `:to` option can be overridden the same way as for `add/2`, and will be - called during the HTTP handshake. If the `conn.state` is `:unset` the + The `:to` option can be overridden the same way as for `handle/2`, and will + be called during the HTTP handshake. If the `conn.state` is `:unset` the websocket will be initiated otherwise response is returned as-is. ## Options - Takes the same options as `add/2`, except `:to`. + Takes the same options as `handle/2`, except `:to`. ## Examples @@ -374,7 +398,7 @@ defmodule TestServer.HTTP do websocket_init(instance, uri, options) end - @spec websocket_init(pid(), binary()) :: {:ok, websocket_socket()} + @spec websocket_init(TestServer.instance(), binary()) :: {:ok, websocket_socket()} def websocket_init(instance, uri) when is_pid(instance) and is_binary(uri) do websocket_init(instance, uri, []) end @@ -384,7 +408,7 @@ defmodule TestServer.HTTP do See `websocket_init/2` for options. """ - @spec websocket_init(pid(), binary(), keyword()) :: {:ok, websocket_socket()} + @spec websocket_init(TestServer.instance(), binary(), keyword()) :: {:ok, websocket_socket()} def websocket_init(instance, uri, options) do options = options @@ -396,7 +420,7 @@ defmodule TestServer.HTTP do {:ok, {instance, ref}} end - @spec websocket_handle(websocket_socket()) :: :ok | {:error, term()} + @spec websocket_handle(websocket_socket()) :: :ok def websocket_handle(socket), do: websocket_handle(socket, []) @doc """ @@ -408,10 +432,11 @@ defmodule TestServer.HTTP do ## Options - * `:match` - an anonymous function that will be called to see if a - message matches, defaults to matching anything; - * `:to` - an anonymous function that will be called when the message - matches, defaults to returning received message; + * `:match` - an `t:websocket_match_fun/0` function that returns a + boolean. Defaults to matching anything; + + * `:to` - a `t:websocket_handler_fun/0` function called when the + handler matches. Defaults to send back the received message; ## Examples @@ -450,34 +475,35 @@ defmodule TestServer.HTTP do do: {:reply, frame, state} @doc """ - Sends an message to a websocket instance. + Sends a message to a websocket instance. + + * `:to` - a `t:websocket_send_fun/0` that will run on the server; ## Examples {:ok, socket} = TestServer.HTTP.websocket_init("/ws") {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) - assert TestServer.HTTP.websocket_info(socket, fn state -> + assert TestServer.HTTP.websocket_send(socket, to: fn state -> {:reply, {:text, "hello"}, state} end) == :ok assert WebSocketClient.receive_message(client) == {:ok, "hello"} """ - @spec websocket_info(websocket_socket(), function() | nil) :: :ok - def websocket_info({instance, _route_ref} = socket, callback \\ nil) - when is_function(callback) or is_nil(callback) do + @spec websocket_send(websocket_socket(), keyword()) :: :ok + def websocket_send({instance, _route_ref} = socket, options \\ []) do TestServer.ensure_instance_alive!(__MODULE__, instance) [_first_module_entry | stacktrace] = TestServer.get_pruned_stacktrace(__MODULE__) - callback = callback || (&default_websocket_info/1) + options = Keyword.put_new(options, :to, &default_websocket_send/1) - for pid <- Instance.active_websocket_connections(socket) do - send(pid, {callback, stacktrace}) + for pid <- Instance.websocket_connections(socket) do + send(pid, {options, stacktrace}) end :ok end - defp default_websocket_info(state), do: {:reply, {:text, "ping"}, state} + defp default_websocket_send(state), do: {:reply, {:text, "ping"}, state} end diff --git a/lib/test_server/http/README.md b/lib/test_server/http/README.md index f93e372..43e8e49 100644 --- a/lib/test_server/http/README.md +++ b/lib/test_server/http/README.md @@ -8,12 +8,12 @@ Mock HTTP/1, HTTP/2, and WebSocket endpoints with route expectations, plug pipel ### HTTP -Add route request expectations with `TestServer.HTTP.add/2`: +Add route request expectations with `TestServer.HTTP.handle/2`: ```elixir test "fetch_url/0" do # The test server will autostart the current test server, if not already running - TestServer.HTTP.add("/", via: :get) + TestServer.HTTP.handle("/", via: :get) # The URL is derived from the current test server instance Application.put_env(:my_app, :fetch_url, TestServer.HTTP.url()) @@ -22,26 +22,26 @@ test "fetch_url/0" do end ``` -`TestServer.HTTP.add/2` can route a request to an anonymous function or plug with `:to` option. +`TestServer.HTTP.handle/2` can route a request to an anonymous function or plug with `:to` option. ```elixir -TestServer.HTTP.add("/", to: fn conn -> +TestServer.HTTP.handle("/", to: fn conn -> Plug.Conn.send_resp(conn, 200, "OK") end) -TestServer.HTTP.add("/", to: MyPlug) +TestServer.HTTP.handle("/", to: MyPlug) ``` The method listened to can be defined with `:via` option. By default any method is matched. ```elixir -TestServer.HTTP.add("/", via: :post) +TestServer.HTTP.handle("/", via: :post) ``` A custom match function can be set with `:match` option: ```elixir -TestServer.HTTP.add("/", match: fn +TestServer.HTTP.handle("/", match: fn %{params: %{"a" => "1"}} = _conn -> true _conn -> false end) @@ -50,8 +50,8 @@ end) When a route is matched it'll be removed from active routes list. The route will be triggered in the order they were added: ```elixir -TestServer.HTTP.add("/", via: :get, to: &Plug.Conn.send_resp(&1, 200, "first")) -TestServer.HTTP.add("/", via: :get, to: &Plug.Conn.send_resp(&1, 200, "second")) +TestServer.HTTP.handle("/", via: :get, to: &Plug.Conn.send_resp(&1, 200, "first")) +TestServer.HTTP.handle("/", via: :get, to: &Plug.Conn.send_resp(&1, 200, "second")) {:ok, "first"} = fetch_request() {:ok, "second"} = fetch_request() @@ -101,7 +101,7 @@ assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} = ### WebSocket -WebSocket endpoint can be set up by calling `TestServer.HTTP.websocket_init/2`. By default, `TestServer.HTTP.websocket_handle/2` will echo the message received. Messages can be send from the test server with `TestServer.HTTP.websocket_info/2`. +WebSocket endpoint can be set up by calling `TestServer.HTTP.websocket_init/2`. By default, `TestServer.HTTP.websocket_handle/2` will echo the message received. Messages can be send from the test server with `TestServer.HTTP.websocket_send/2`. ```elixir test "WebSocketClient" do @@ -122,7 +122,7 @@ test "WebSocketClient" do :ok = WebSocketClient.send(client, "hi") {:ok, "hi"} = WebSocketClient.receive(client) - :ok = TestServer.HTTP.websocket_info(socket, fn state -> {:reply, {:text, "ping"}, state} end) + :ok = TestServer.HTTP.websocket_send(socket, to: fn state -> {:reply, {:text, "ping"}, state} end) {:ok, "ping"} = WebSocketClient.receive(client) end ``` @@ -147,7 +147,7 @@ Use the `:ipfamily` option to test with IPv6 when starting the test server with TestServer.HTTP.start(ipfamily: :inet6) assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} diff --git a/lib/test_server/http/instance.ex b/lib/test_server/http/instance.ex index 5c31781..9b03481 100644 --- a/lib/test_server/http/instance.ex +++ b/lib/test_server/http/instance.ex @@ -13,7 +13,10 @@ defmodule TestServer.HTTP.Instance do GenServer.stop(instance) end - @spec register(pid(), {:plug_router_to, {binary(), keyword(), TestServer.stacktrace()}}) :: + @spec register( + TestServer.instance(), + {:plug_router_to, {binary(), keyword(), TestServer.stacktrace()}} + ) :: {:ok, %{ref: reference()}} def register(instance, {:plug_router_to, {uri, options, stacktrace}}) do ensure_plug!(options[:to]) @@ -22,7 +25,7 @@ defmodule TestServer.HTTP.Instance do GenServer.call(instance, {:register, {:plug_router_to, {uri, options, stacktrace}}}) end - @spec register(pid(), {:plug, {atom() | function(), TestServer.stacktrace()}}) :: + @spec register(TestServer.instance(), {:plug, {atom() | function(), TestServer.stacktrace()}}) :: {:ok, map()} def register(instance, {:plug, {plug, stacktrace}}) do ensure_plug!(plug) @@ -51,7 +54,7 @@ defmodule TestServer.HTTP.Instance do defp ensure_function!(fun) when is_function(fun), do: :ok defp ensure_function!(fun), do: raise(BadFunctionError, term: fun) - @spec dispatch(pid(), {:plug, Plug.Conn.t()}) :: + @spec dispatch(TestServer.instance(), {:plug, Plug.Conn.t()}) :: {:ok, Plug.Conn.t()} | {:error, {:not_found, Plug.Conn.t()}} | {:error, {term(), list()}} @@ -73,27 +76,29 @@ defmodule TestServer.HTTP.Instance do @spec dispatch( TestServer.HTTP.websocket_socket(), - {:websocket, {:info, function(), TestServer.stacktrace()}, + {:websocket, {:info, keyword(), TestServer.stacktrace()}, TestServer.HTTP.websocket_state()} ) :: {:ok, TestServer.HTTP.websocket_reply()} | {:error, {term(), TestServer.stacktrace()}} def dispatch( {instance, _router_ref} = socket, - {:websocket, {:info, callback, stacktrace}, state} + {:websocket, {:info, options, stacktrace}, state} ) do + to = Keyword.fetch!(options, :to) + GenServer.call( instance, - {:dispatch, {:websocket, socket, {:info, callback, stacktrace}, state}} + {:dispatch, {:websocket, socket, {:info, to, stacktrace}, state}} ) end - @spec get_options(pid()) :: keyword() + @spec get_options(TestServer.instance()) :: keyword() def get_options(instance) do GenServer.call(instance, :options) end - @spec routes(pid()) :: [map()] + @spec routes(TestServer.instance()) :: [map()] def routes(instance) do GenServer.call(instance, :routes) end @@ -103,8 +108,8 @@ defmodule TestServer.HTTP.Instance do GenServer.cast(instance, {:put, :websocket_connection, route_ref, pid}) end - @spec active_websocket_connections(TestServer.HTTP.websocket_socket()) :: [pid()] - def active_websocket_connections({instance, route_ref}) do + @spec websocket_connections(TestServer.HTTP.websocket_socket()) :: [pid()] + def websocket_connections({instance, route_ref}) do GenServer.call(instance, {:get, :websocket_connections, route_ref}) end @@ -120,7 +125,7 @@ defmodule TestServer.HTTP.Instance do end) end - @spec websocket_handlers(pid()) :: [map()] + @spec websocket_handlers(TestServer.instance()) :: [map()] def websocket_handlers(instance) do GenServer.call(instance, :websocket_handlers) end @@ -137,7 +142,7 @@ defmodule TestServer.HTTP.Instance do end) end - @spec report_error(pid(), {struct(), TestServer.stacktrace()}) :: :ok + @spec report_error(TestServer.instance(), {struct(), TestServer.stacktrace()}) :: :ok def report_error(instance, {exception, stacktrace}) do options = get_options(instance) caller = Keyword.fetch!(options, :caller) @@ -400,7 +405,7 @@ defmodule TestServer.HTTP.Instance do def maybe_put_websocket(conn, route) do case route.options[:websocket] do true -> - websocket = {{self(), route.ref}, Keyword.get(route.options, :init_state)} + websocket = {{self(), route.ref}, %{}} Map.put(conn, :private, %{websocket: websocket}) _false -> @@ -447,7 +452,10 @@ defmodule TestServer.HTTP.Instance do error -> {:error, {error, __STACKTRACE__}} end - defp validate_websocket_frame!({:reply, _frame, _state} = response, _stacktrace), do: response + defp validate_websocket_frame!({:reply, {opcode, _data}, _state} = response, _stacktrace) + when opcode in [:text, :binary], + do: response + defp validate_websocket_frame!({:ok, _state} = response, _stacktrace), do: response defp validate_websocket_frame!(response, stacktrace) do @@ -456,8 +464,8 @@ defmodule TestServer.HTTP.Instance do Expected one of the following: - - {:reply, {:text, message}, state} - - {:reply, {:binary, message}, state} + - {:reply, {:text, iodata}, state} + - {:reply, {:binary, iodata}, state} - {:ok, state} #{Enum.map_join(stacktrace, "\n ", &Exception.format_stacktrace_entry/1)} diff --git a/lib/test_server/http/server.ex b/lib/test_server/http/server.ex index d1aeb4d..0882b91 100644 --- a/lib/test_server/http/server.ex +++ b/lib/test_server/http/server.ex @@ -8,38 +8,37 @@ defmodule TestServer.HTTP.Server do @behaviour TestServer.HTTP.Server @impl TestServer.HTTP.Server - def start(instance, port, scheme, tls_options, server_options) do + def start(instance, port, scheme, options, server_options) do my_http_server_options = server_options |> Keyword.put(:port, port) |> Keyword.put_new(:ipfamily, options[:ipfamily]) case MyHTTPServer.start(my_http_server_options) do - {:ok, pid} -> {:ok, pid, my_http_server_options} + {:ok, server_pid} -> {:ok, server_pid, my_http_server_options} {:error, error} -> {:error, error} end end @impl TestServer.HTTP.Server - def stop(instance, server_options), do: MyHTTPServer.stop() + def stop(server_pid, server_options), do: MyHTTPServer.stop(server_pid) @impl TestServer.HTTP.Server def get_socket_pid(%{adapter: {_, data}}), do: data.pid # or however your adapter provides the pid end """ @type scheme :: :http | :https - @type instance :: pid() @type port_number :: :inet.port_number() @type options :: [tls: keyword(), ipfamily: :inet | :inet6] @type server_options :: keyword() - @callback start(instance(), port_number(), scheme(), options(), server_options()) :: - {:ok, pid(), server_options()} | {:error, any()} - @callback stop(instance(), server_options()) :: :ok | {:error, any()} + @callback start(TestServer.instance(), port_number(), scheme(), options(), server_options()) :: + {:ok, term(), server_options()} | {:error, term()} + @callback stop(term(), server_options()) :: :ok | {:error, term()} @callback get_socket_pid(Plug.Conn.t()) :: pid() @doc false - @spec start(pid(), keyword()) :: {:ok, keyword()} | {:error, any()} + @spec start(TestServer.instance(), keyword()) :: {:ok, keyword()} | {:error, term()} def start(instance, options) do port = TestServer.open_port(options) scheme = parse_scheme(options) @@ -79,12 +78,14 @@ defmodule TestServer.HTTP.Server do case Keyword.take(tls_options, [:key, :keyfile]) do [] -> suite = X509.Test.Suite.new() + server_key = X509.PrivateKey.to_der(suite.server_key) + cert = X509.Certificate.to_der(suite.valid) {[ - key: {:RSAPrivateKey, X509.PrivateKey.to_der(suite.server_key)}, - cert: X509.Certificate.to_der(suite.valid), + key: {:RSAPrivateKey, server_key}, + cert: cert, cacerts: suite.chain ++ suite.cacerts - ], x509_suite: suite} + ], x509_suite: %{cert: cert, cacerts: suite.cacerts}} [_ | _] -> {tls_options, []} @@ -117,7 +118,7 @@ defmodule TestServer.HTTP.Server do end @doc false - @spec stop(keyword()) :: :ok | {:error, any()} + @spec stop(keyword()) :: :ok | {:error, term()} def stop(options) do {mod, server_options} = Keyword.fetch!(options, :http_server) reference = Keyword.fetch!(options, :http_server_reference) diff --git a/lib/test_server/http/server/bandit.ex b/lib/test_server/http/server/bandit.ex index 26b1219..f562f21 100644 --- a/lib/test_server/http/server/bandit.ex +++ b/lib/test_server/http/server/bandit.ex @@ -81,8 +81,8 @@ if Code.ensure_loaded?(Bandit) do do: WebSocket.handle_frame({opcode, data}, {socket, state}) @impl WebSock - def handle_info({callback, stacktrace}, {socket, state}), - do: WebSocket.handle_info({callback, stacktrace}, {socket, state}) + def handle_info({options, stacktrace}, {socket, state}), + do: WebSocket.handle_info({options, stacktrace}, {socket, state}) def handle_info(_, {socket, state}), do: {:ok, {socket, state}} diff --git a/lib/test_server/http/server/plug_cowboy.ex b/lib/test_server/http/server/plug_cowboy.ex index af69e36..d6cf785 100644 --- a/lib/test_server/http/server/plug_cowboy.ex +++ b/lib/test_server/http/server/plug_cowboy.ex @@ -4,7 +4,7 @@ if Code.ensure_loaded?(Plug.Cowboy) do HTTP server adapter using `Plug.Cowboy`. By default only one acceptor process is started which is enough for - testing. This adapter will be used by default if `Bandit` is not loaded + testing. This adapter will be used by default if `Bandit` is not loaded and `Plug.Cowboy` is loaded in the project. ## Usage @@ -107,8 +107,8 @@ if Code.ensure_loaded?(Plug.Cowboy) do end @impl :cowboy_websocket - def websocket_info({callback, stacktrace}, {socket, state}) do - {callback, stacktrace} + def websocket_info({options, stacktrace}, {socket, state}) do + {options, stacktrace} |> WebSocket.handle_info({socket, state}) |> handle_reply() end diff --git a/lib/test_server/http/websocket.ex b/lib/test_server/http/websocket.ex index d2ee54d..ff548bc 100644 --- a/lib/test_server/http/websocket.ex +++ b/lib/test_server/http/websocket.ex @@ -75,8 +75,8 @@ defmodule TestServer.HTTP.WebSocket do defp handle_reply({:reply, frame, state}, socket), do: {:reply, :ok, frame, {socket, state}} defp handle_reply({:ok, state}, socket), do: {:ok, {socket, state}} - def handle_info({callback, stacktrace}, {socket, state}) do - case Instance.dispatch(socket, {:websocket, {:info, callback, stacktrace}, state}) do + def handle_info({options, stacktrace}, {socket, state}) do + case Instance.dispatch(socket, {:websocket, {:info, options, stacktrace}, state}) do {:ok, result} -> handle_reply(result, socket) {:error, {error, stacktrace}} -> reply_with_error({socket, state}, {error, stacktrace}) end diff --git a/lib/test_server/instance_manager.ex b/lib/test_server/instance_manager.ex index 56e939b..92ae29d 100644 --- a/lib/test_server/instance_manager.ex +++ b/lib/test_server/instance_manager.ex @@ -9,7 +9,8 @@ defmodule TestServer.InstanceManager do GenServer.start_link(__MODULE__, options, name: __MODULE__) end - @spec start_instance(pid(), module(), keyword()) :: {:ok, pid()} | {:error, term()} + @spec start_instance(pid(), module(), keyword()) :: + {:ok, TestServer.instance()} | {:error, term()} def start_instance(caller, protocol_module, options) do [_first | stacktrace] = TestServer.get_pruned_stacktrace(protocol_module) @@ -29,7 +30,7 @@ defmodule TestServer.InstanceManager do end end - @spec stop_instance(pid()) :: :ok | {:error, :not_found} + @spec stop_instance(TestServer.instance()) :: :ok | {:error, :not_found} def stop_instance(instance) do res = DynamicSupervisor.terminate_child(InstanceSupervisor, instance) GenServer.call(__MODULE__, {:remove, instance}) @@ -37,7 +38,7 @@ defmodule TestServer.InstanceManager do res end - @spec fetch_instance(pid(), module()) :: {:ok, pid()} | :error + @spec fetch_instance(pid(), module()) :: {:ok, TestServer.instance()} | :error def fetch_instance(caller, protocol_module) do case GenServer.call(__MODULE__, {:get_by_caller, caller, protocol_module}) do [] -> :error diff --git a/lib/test_server/ssh.ex b/lib/test_server/ssh.ex index 99270e4..1e619c2 100644 --- a/lib/test_server/ssh.ex +++ b/lib/test_server/ssh.ex @@ -7,7 +7,7 @@ defmodule TestServer.SSH do alias TestServer.SSH.{Instance, Server} - @type channel :: {pid(), channel_ref()} + @type channel :: {TestServer.instance(), channel_ref()} @type channel_ref :: reference() @type channel_id :: :ssh.channel_id() @type state :: term() @@ -43,24 +43,34 @@ defmodule TestServer.SSH do * `:port` - integer of port number, defaults to random port that can be opened; + * `:host_keys` - list of host keys, or a `c::ssh_server_key_api.host_key/2` function. Default will autogenerate keys for algorithms specified in `t::ssh.pubkey_alg/0` and they can be fetched from `host_keys/1`; - * `:auth_keys` - list of `{"user", public_key}` tuples, or a + + * `:auth_keys` - list of `{"user", public_key}` tuples, or a `c::ssh_server_key_api.is_auth_key/3` function. Defaults to an empty list. - * `:user_passwords` - list of `{"user", "password"}` tuples; + + * `:user_passwords` - list of `{"user", "password"}` tuples; + * `:no_auth_needed` - boolean value indicating whether to allow connections with no authentication. Defaults to `true` if `:auth_keys` and `:user_passwords` has not been set, otherwise `false`; + * `:ipfamily` - The IP address type to use, either `:inet` or `:inet6`. Defaults to `:inet`; + * `:suppress_ssh_strict_kex_ordering_log` - boolean that suppresses OTP SSH debug messages for strict KEX ordering. Defaults to `true`. Note: the filter is installed when the server starts and removed when it stops. If a test crashes the filter may persist into the next test; - * `:daemon` - options to pass directly to `:ssh.daemon/2`. + + * `:daemon` - options to pass directly to `:ssh.daemon/2`; + + * `:suppress_warning` - Suppresses IO warnings on expectation failures + related to this instance. Defaults to `false`; ## Examples @@ -111,7 +121,7 @@ defmodule TestServer.SSH do assert {:ok, "pong"} = SSHClient.receive_data(conn, channel_id) assert :ok = SSHClient.close(conn) """ - @spec start(keyword()) :: {:ok, pid()} + @spec start(keyword()) :: {:ok, TestServer.instance()} def start(options \\ []) do TestServer.start_instance(__MODULE__, options, &verify!/1) end @@ -172,11 +182,11 @@ defmodule TestServer.SSH do @doc """ Shuts down a test server SSH instance. """ - @spec stop(pid()) :: :ok | {:error, term()} + @spec stop(TestServer.instance()) :: :ok | {:error, term()} def stop(instance) do TestServer.ensure_instance_alive!(__MODULE__, instance) - Server.stop(Instance.get_options(instance)) + :ok = Server.stop(Instance.get_options(instance)) TestServer.stop_instance(__MODULE__, instance) end @@ -199,7 +209,7 @@ defmodule TestServer.SSH do assert TestServer.SSH.address() == {"localhost", 2222} assert TestServer.SSH.address(host: "myserver.test") == {"myserver.test", 2222} """ - @spec address(keyword() | pid()) :: {binary(), non_neg_integer()} + @spec address(keyword() | TestServer.instance()) :: {binary(), non_neg_integer()} def address(options) when is_list(options), do: address(TestServer.fetch_instance!(__MODULE__), options) @@ -210,7 +220,7 @@ defmodule TestServer.SSH do See `address/1` for options. """ - @spec address(pid(), keyword()) :: {binary(), non_neg_integer()} + @spec address(TestServer.instance(), keyword()) :: {binary(), non_neg_integer()} def address(instance, options) when is_pid(instance) and is_list(options) do TestServer.ensure_instance_alive!(__MODULE__, instance) @@ -228,7 +238,7 @@ defmodule TestServer.SSH do ## Options - * `:listen` - list of message types to dispatch to handlers, or `:all`. + * `:messages` - list of message types to dispatch to handlers, or `:all`. Defaults to `[:exec, :data]`. Available types: `:exec`, `:data`, `:env`, `:pty`, `:shell`, `:eof`; @@ -255,13 +265,13 @@ defmodule TestServer.SSH do See `channel/1` for options. """ - @spec channel(pid(), keyword()) :: {:ok, channel()} + @spec channel(TestServer.instance(), keyword()) :: {:ok, channel()} def channel(instance, options) do TestServer.ensure_instance_alive!(__MODULE__, instance) [_first_module_entry | stacktrace] = TestServer.get_pruned_stacktrace(__MODULE__) - options = Keyword.put_new(options, :listen, [:exec, :data]) + options = Keyword.put_new(options, :messages, [:exec, :data]) {:ok, channel} = Instance.register(instance, {:channel, {options, stacktrace}}) @@ -288,6 +298,7 @@ defmodule TestServer.SSH do * `:match` - a `t:match_fun/0` function that returns a boolean. Defaults to matching anything; + * `:to` - a `t:handler_fun/0` or `t:raw_handler_fun/0` function called when the handler matches. Defaults to send back the received data for `:exec` and `:data` type channel messages, with no `:data` message @@ -300,7 +311,8 @@ defmodule TestServer.SSH do * `:data_type_code` - an integer SSH data type code to send with the reply, defaults to `0` (SSH_MSG_CHANNEL_DATA); - * `:exit_status` - an integer exit status to send when finishing an `:exec` + + * `:exit_status` - an integer exit status to send when finishing an `:exec` channel, defaults to `0`. Ignored for other channel types; ## Examples @@ -363,7 +375,7 @@ defmodule TestServer.SSH do See `host_keys/0` for more. """ - @spec host_keys(pid()) :: [host_key()] + @spec host_keys(TestServer.instance()) :: [host_key()] def host_keys(instance) do TestServer.ensure_instance_alive!(__MODULE__, instance) diff --git a/lib/test_server/ssh/README.md b/lib/test_server/ssh/README.md index f22392b..dcc8d7c 100644 --- a/lib/test_server/ssh/README.md +++ b/lib/test_server/ssh/README.md @@ -47,11 +47,11 @@ test "SSHClient" do end ``` -By default, only `:exec` and `:data` messages are dispatched to handlers. Use the `:listen` option on `TestServer.SSH.channel/2` to control which message types are dispatched: +By default, only `:exec` and `:data` messages are dispatched to handlers. Use the `:messages` option on `TestServer.SSH.channel/2` to control which message types are dispatched: ```elixir -{:ok, channel_1} = TestServer.SSH.channel(listen: :all) -{:ok, channel_2} = TestServer.SSH.channel(listen: [:data, :env, :pty]) +{:ok, channel_1} = TestServer.SSH.channel(messages: :all) +{:ok, channel_2} = TestServer.SSH.channel(messages: [:data, :env, :pty]) ``` ### Host keys @@ -60,9 +60,9 @@ Host keys can be configured in `TestServer.SSH.start/1`. By default host keys ar ```elixir host_key = :public_key.generate_key({:rsa, 2048, 65_537}) -{:ok, _instance} = TestServer.SSH.start(host_keys: [host_key]) +{:ok, instance} = TestServer.SSH.start(host_keys: [host_key]) -[%{fingerprint: fingerprint}] = TestServer.SSH.host_keys() +[%{fingerprint: fingerprint}] = TestServer.SSH.host_keys(instance) ``` ### Authentication @@ -90,5 +90,4 @@ assert {:ok, conn} = SSHClient.connect(TestServer.SSH.address(), inet6: true) assert {{0, 0, 0, 0, 0, 0, 0, 1}, _port} = SSHClient.sockname(conn) ``` - diff --git a/lib/test_server/ssh/channel.ex b/lib/test_server/ssh/channel.ex index bfc49d2..0d9799c 100644 --- a/lib/test_server/ssh/channel.ex +++ b/lib/test_server/ssh/channel.ex @@ -80,9 +80,9 @@ defmodule TestServer.SSH.Channel do @impl true def handle_ssh_msg({:ssh_cm, connection, frame}, state) do type = elem(frame, 0) - listen = Keyword.fetch!(state.channel.options, :listen) + messages = Keyword.fetch!(state.channel.options, :messages) - case dispatch(listen, type, connection, frame, state) do + case dispatch(messages, type, connection, frame, state) do {:raw, {:ok, channel_state}} -> {:ok, %{state | state: channel_state}} @@ -94,8 +94,8 @@ defmodule TestServer.SSH.Channel do end end - defp dispatch(listen, type, connection, frame, state) do - case listen == :all or type in listen do + defp dispatch(messages, type, connection, frame, state) do + case messages == :all or type in messages do true -> Instance.dispatch( state.instance, diff --git a/lib/test_server/ssh/instance.ex b/lib/test_server/ssh/instance.ex index 7f3975b..9590ab7 100644 --- a/lib/test_server/ssh/instance.ex +++ b/lib/test_server/ssh/instance.ex @@ -11,16 +11,16 @@ defmodule TestServer.SSH.Instance do GenServer.stop(instance) end - @spec register(pid(), {:channel, {keyword(), TestServer.stacktrace()}}) :: + @spec register(TestServer.instance(), {:channel, {keyword(), TestServer.stacktrace()}}) :: {:ok, %{ref: TestServer.SSH.channel_ref()}} def register(instance, {:channel, {options, stacktrace}}) do - options[:listen] && ensure_listen!(options[:listen]) + options[:messages] && ensure_messages!(options[:messages]) GenServer.call(instance, {:register, {:channel, {options, stacktrace}}}) end @spec register( - pid(), + TestServer.instance(), {:handle, {TestServer.SSH.channel_ref(), keyword(), TestServer.stacktrace()}} ) :: {:ok, map()} @@ -31,30 +31,33 @@ defmodule TestServer.SSH.Instance do GenServer.call(instance, {:register, {:handle, {channel_ref, options, stacktrace}}}) end - @listen_events ~w(exec data env pty shell eof)a + @message_events ~w(exec data env pty shell eof)a - defp ensure_listen!(listen) when is_list(listen) do - case Enum.all?(listen, &(&1 in @listen_events)) do + defp ensure_messages!(messages) when is_list(messages) do + case Enum.all?(messages, &(&1 in @message_events)) do true -> :ok false -> raise ArgumentError, - "expected list to only include #{inspect(@listen_events)}, got: #{inspect(listen)}" + "expected list to only include #{inspect(@message_events)}, got: #{inspect(messages)}" end end - defp ensure_listen!(listen) do - case listen do + defp ensure_messages!(messages) do + case messages do :all -> :ok - _ -> raise ArgumentError, "expected :all, got: #{inspect(listen)}" + _ -> raise ArgumentError, "expected :all, got: #{inspect(messages)}" end end defp ensure_function!(fun) when is_function(fun), do: :ok defp ensure_function!(fun), do: raise(BadFunctionError, term: fun) - @spec dispatch(pid(), {:channel_up, TestServer.SSH.channel_id(), TestServer.SSH.connection()}) :: + @spec dispatch( + TestServer.instance(), + {:channel_up, TestServer.SSH.channel_id(), TestServer.SSH.connection()} + ) :: {:ok, {TestServer.SSH.channel_ref(), keyword(), TestServer.stacktrace()}} | {:error, :not_found} def dispatch(instance, {:channel_up, channel_id, connection}) do @@ -62,7 +65,7 @@ defmodule TestServer.SSH.Instance do end @spec dispatch( - pid(), + TestServer.instance(), {:handle, TestServer.SSH.channel_id(), TestServer.SSH.connection(), TestServer.SSH.channel_msg(), TestServer.SSH.state()} ) :: @@ -79,17 +82,17 @@ defmodule TestServer.SSH.Instance do ) end - @spec handlers(pid()) :: [map()] + @spec handlers(TestServer.instance()) :: [map()] def handlers(instance) do GenServer.call(instance, :handlers) end - @spec channels(pid()) :: [map()] + @spec channels(TestServer.instance()) :: [map()] def channels(instance) do GenServer.call(instance, :channels) end - @spec get_options(pid()) :: keyword() + @spec get_options(TestServer.instance()) :: keyword() def get_options(instance) do GenServer.call(instance, :options) end @@ -118,7 +121,7 @@ defmodule TestServer.SSH.Instance do end) end - @spec report_error(pid(), {struct(), TestServer.stacktrace()}) :: :ok + @spec report_error(TestServer.instance(), {struct(), TestServer.stacktrace()}) :: :ok def report_error(instance, {exception, stacktrace}) do options = get_options(instance) caller = Keyword.fetch!(options, :caller) diff --git a/lib/test_server/ssh/server.ex b/lib/test_server/ssh/server.ex index a1622ba..323f5a2 100644 --- a/lib/test_server/ssh/server.ex +++ b/lib/test_server/ssh/server.ex @@ -2,7 +2,7 @@ defmodule TestServer.SSH.Server do @moduledoc false @doc false - @spec start(pid(), keyword()) :: {:ok, keyword()} | {:error, any()} + @spec start(TestServer.instance(), keyword()) :: {:ok, keyword()} | {:error, term()} def start(instance, options) do port = TestServer.open_port(options) {host_keys, daemon_options} = daemon_options(instance, options) @@ -54,7 +54,7 @@ defmodule TestServer.SSH.Server do { TestServer.SSH.Channel, options - |> Keyword.take([:listen]) + |> Keyword.take([:messages]) |> Keyword.put(:instance, instance) } diff --git a/mix.exs b/mix.exs index 21ace5d..c9eefc0 100644 --- a/mix.exs +++ b/mix.exs @@ -60,7 +60,7 @@ defmodule TestServer.MixProject do "GitHub" => @source_url, "Sponsor" => "https://github.com/sponsors/danschultzer" }, - files: ~w(lib LICENSE mix.exs README.md) + files: ~w(CHANGELOG.md lib LICENSE mix.exs README.md) ] end diff --git a/test/http/server/bandit/adapter_test.exs b/test/test_server/http/server/bandit/adapter_test.exs similarity index 92% rename from test/http/server/bandit/adapter_test.exs rename to test/test_server/http/server/bandit/adapter_test.exs index 82cff9b..fc0caa1 100644 --- a/test/http/server/bandit/adapter_test.exs +++ b/test/test_server/http/server/bandit/adapter_test.exs @@ -12,7 +12,7 @@ defmodule TestServer.HTTP.Server.Bandit.AdapterTest do test "Plug.Conn.send_resp/3" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> Plug.Conn.send_resp(conn, 200, "test") end @@ -23,7 +23,7 @@ defmodule TestServer.HTTP.Server.Bandit.AdapterTest do test "Plug.Conn.send_file/1" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> Plug.Conn.send_file(conn, 200, __ENV__.file) end @@ -36,7 +36,7 @@ defmodule TestServer.HTTP.Server.Bandit.AdapterTest do test "Plug.Conn.send_chunked/1 and Plug.Conn.chunk/1" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> conn = Plug.Conn.send_chunked(conn, 200) {:ok, conn} = Plug.Conn.chunk(conn, "Hello\n") @@ -51,7 +51,7 @@ defmodule TestServer.HTTP.Server.Bandit.AdapterTest do test "Plug.Conn.get_peer_data/1" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert %{address: {127, 0, 0, 1}} = Plug.Conn.get_peer_data(conn) @@ -64,7 +64,7 @@ defmodule TestServer.HTTP.Server.Bandit.AdapterTest do test "Plug.Conn.get_http_protocol/1" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert Plug.Conn.get_http_protocol(conn) == :"HTTP/2" Plug.Conn.send_resp(conn, 200, "OK") @@ -76,7 +76,7 @@ defmodule TestServer.HTTP.Server.Bandit.AdapterTest do test "Plug.Conn.read_body/1" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert {:ok, body, _data} = Plug.Conn.read_body(conn) Plug.Conn.resp(conn, 200, body) diff --git a/test/http/server/httpd_test.exs b/test/test_server/http/server/httpd_test.exs similarity index 92% rename from test/http/server/httpd_test.exs rename to test/test_server/http/server/httpd_test.exs index 2001f64..8f12114 100644 --- a/test/http/server/httpd_test.exs +++ b/test/test_server/http/server/httpd_test.exs @@ -15,7 +15,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do describe "conn/1" do test "with no host header" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> refute List.keyfind(conn.req_headers, "host", 0) assert conn.host == "" @@ -31,7 +31,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do test "with host header without port" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.host == "localhost" refute conn.port @@ -46,7 +46,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do test "with host header with invalid port" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.host == "localhost" refute conn.port @@ -61,7 +61,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do test "with host header with extra colon" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.host == "localhost" refute conn.port @@ -77,7 +77,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do @tag ipfamily: :inet6 test "with IPv6 host header" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.host == "::1" assert conn.port == 8080 @@ -93,7 +93,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do @tag ipfamily: :inet6 test "with IPv6 host header without port" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.host == "::1" refute conn.port @@ -108,7 +108,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do test "with extra query segment" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.query_string == "foo=bar" @@ -125,7 +125,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do %{port: port} = URI.parse(url) assert :ok = - TestServer.HTTP.add("/a/1", + TestServer.HTTP.handle("/a/1", to: fn conn -> assert conn.host == "localhost" assert conn.method == "GET" @@ -153,7 +153,7 @@ defmodule TestServer.HTTP.Server.HttpdTest do body = "héllo 👋" assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> Plug.Conn.resp(conn, 200, body) end diff --git a/test/test_server/http_test.exs b/test/test_server/http_test.exs index 782aa51..df43558 100644 --- a/test/test_server/http_test.exs +++ b/test/test_server/http_test.exs @@ -45,7 +45,7 @@ defmodule TestServer.HTTPTest do options = TestServer.HTTP.Instance.get_options(instance) - assert %X509.Test.Suite{} = options[:x509_suite] + assert %{cert: _, cacerts: _} = options[:x509_suite] http_options = fn cacerts -> @@ -63,15 +63,15 @@ defmodule TestServer.HTTPTest do ] end - valid_cacerts = TestServer.HTTP.x509_suite().cacerts - invalid_cacerts = X509.Test.Suite.new().cacerts + %{cacerts: valid_cacerts} = TestServer.HTTP.x509_suite() + %{cacerts: invalid_cacerts} = X509.Test.Suite.new() assert {:error, {:failed_connect, _}} = http1_request(TestServer.HTTP.url("/"), http_options: http_options.(invalid_cacerts) ) - assert :ok = TestServer.HTTP.add("/") + assert :ok = TestServer.HTTP.handle("/") assert {:ok, _} = http1_request(TestServer.HTTP.url("/"), http_options: http_options.(valid_cacerts)) @@ -84,7 +84,7 @@ defmodule TestServer.HTTPTest do assert options[:ipfamily] == :inet6 assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} @@ -198,7 +198,7 @@ defmodule TestServer.HTTPTest do TestServer.HTTP.start() assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.remote_ip == {127, 0, 0, 1} assert conn.host == "custom-host" @@ -214,7 +214,7 @@ defmodule TestServer.HTTPTest do TestServer.HTTP.start(ipfamily: :inet6, http_server: {TestServer.HTTP.Server.Httpd, []}) assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} assert conn.host == "custom-host" @@ -240,7 +240,7 @@ defmodule TestServer.HTTPTest do end end - describe "add/3" do + describe "handle/3" do test "when instance not running" do assert_raise RuntimeError, ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, @@ -249,17 +249,17 @@ defmodule TestServer.HTTPTest do assert :ok = TestServer.HTTP.stop() - TestServer.HTTP.add(instance, "/") + TestServer.HTTP.handle(instance, "/") end end test "with invalid options" do assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.HTTP.add("/", match: :invalid) + TestServer.HTTP.handle("/", match: :invalid) end assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.HTTP.add("/", to: :invalid) + TestServer.HTTP.handle("/", to: :invalid) end end @@ -268,12 +268,12 @@ defmodule TestServer.HTTPTest do {:ok, _instance_2} = TestServer.HTTP.start() assert_raise RuntimeError, - ~r/Multiple instances running, please pass instance to `TestServer\.HTTP\.add\/2`/, + ~r/Multiple instances running, please pass instance to `TestServer\.HTTP\.handle\/2`/, fn -> - TestServer.HTTP.add("/") + TestServer.HTTP.handle("/") end - assert :ok = TestServer.HTTP.add(instance_1, "/") + assert :ok = TestServer.HTTP.handle(instance_1, "/") TestServer.HTTP.stop(instance_1) end @@ -285,7 +285,7 @@ defmodule TestServer.HTTPTest do test "fails" do {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.HTTP.add("/") + assert :ok = TestServer.HTTP.handle("/") assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/path")) end end @@ -299,7 +299,7 @@ defmodule TestServer.HTTPTest do test "fails" do {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.HTTP.add("/", via: :post) + assert :ok = TestServer.HTTP.handle("/", via: :post) assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end @@ -315,7 +315,7 @@ defmodule TestServer.HTTPTest do test "fails" do {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.HTTP.add("/") + assert :ok = TestServer.HTTP.handle("/") assert {:ok, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/?a=1")) end @@ -331,7 +331,7 @@ defmodule TestServer.HTTPTest do use ExUnit.Case test "fails" do - assert :ok = TestServer.HTTP.add("/") + assert :ok = TestServer.HTTP.handle("/") end end @@ -346,7 +346,7 @@ defmodule TestServer.HTTPTest do def call(conn, _options), do: Plug.Conn.resp(conn, 200, to_string(__MODULE__)) end - assert :ok = TestServer.HTTP.add("/", to: ToPlug) + assert :ok = TestServer.HTTP.handle("/", to: ToPlug) assert http1_request(TestServer.HTTP.url("/")) == {:ok, to_string(ToPlug)} end @@ -357,7 +357,7 @@ defmodule TestServer.HTTPTest do test "fails" do {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.HTTP.add("/", to: fn _conn -> raise "boom" end) + assert :ok = TestServer.HTTP.handle("/", to: fn _conn -> raise "boom" end) assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end end @@ -374,7 +374,7 @@ defmodule TestServer.HTTPTest do test "fails" do {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.HTTP.add("/", to: fn conn -> Plug.Conn.halt(conn) end) + assert :ok = TestServer.HTTP.handle("/", to: fn conn -> Plug.Conn.halt(conn) end) assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end end @@ -385,7 +385,7 @@ defmodule TestServer.HTTPTest do test "with `:to` function" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> Plug.Conn.resp(conn, 200, "function called") end ) @@ -400,7 +400,7 @@ defmodule TestServer.HTTPTest do {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", match: fn _conn -> raise "boom" end ) @@ -415,7 +415,7 @@ defmodule TestServer.HTTPTest do test "with `:match` function" do assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", match: fn %{params: %{"a" => "1"}} = _conn -> true _conn -> false @@ -426,8 +426,8 @@ defmodule TestServer.HTTPTest do end test "with `:via` option" do - assert :ok = TestServer.HTTP.add("/", via: :get) - assert :ok = TestServer.HTTP.add("/", via: :post) + assert :ok = TestServer.HTTP.handle("/", via: :get) + assert :ok = TestServer.HTTP.handle("/", via: :post) assert {:ok, _} = http1_request(TestServer.HTTP.url("/")) assert {:ok, _} = http1_request(TestServer.HTTP.url("/"), method: :post) end @@ -437,7 +437,7 @@ defmodule TestServer.HTTPTest do test "with HTTP/2" do {:ok, _instance} = TestServer.HTTP.start(scheme: :https) - assert :ok = TestServer.HTTP.add("/") + assert :ok = TestServer.HTTP.handle("/") assert {:ok, "HTTP/2"} = http2_request(TestServer.HTTP.url()) end @@ -445,7 +445,7 @@ defmodule TestServer.HTTPTest do {:ok, _instance} = TestServer.HTTP.start(scheme: :https) assert :ok = - TestServer.HTTP.add("/", + TestServer.HTTP.handle("/", to: fn conn -> assert Plug.Conn.get_http_protocol(conn) == :"HTTP/2" assert {:ok, body, _data} = Plug.Conn.read_body(conn) @@ -473,7 +473,9 @@ defmodule TestServer.HTTPTest do end) assert :ok = - TestServer.HTTP.add("/", to: &Plug.Conn.resp(&1, 200, URI.encode_query(&1.params))) + TestServer.HTTP.handle("/", + to: &Plug.Conn.resp(&1, 200, URI.encode_query(&1.params)) + ) assert {:ok, query} = http1_request(TestServer.HTTP.url("/")) assert URI.decode_query(query) == %{"plug" => "anonymous function", "body" => ""} @@ -487,7 +489,7 @@ defmodule TestServer.HTTPTest do end assert :ok = TestServer.HTTP.plug(ModulePlug) - assert :ok = TestServer.HTTP.add("/", to: &Plug.Conn.resp(&1, 200, &1.params["plug"])) + assert :ok = TestServer.HTTP.handle("/", to: &Plug.Conn.resp(&1, 200, &1.params["plug"])) assert http1_request(TestServer.HTTP.url("/")) == {:ok, to_string(ModulePlug)} end @@ -516,7 +518,7 @@ defmodule TestServer.HTTPTest do {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) assert :ok = TestServer.HTTP.plug(fn conn -> Plug.Conn.halt(conn) end) - assert :ok = TestServer.HTTP.add("/") + assert :ok = TestServer.HTTP.handle("/") assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end end @@ -606,8 +608,8 @@ defmodule TestServer.HTTPTest do # a 501 response, so no socket is ever initialized to be handled. end - describe "websocket_info/2" do - # No tests for `websocket_info/2` as `websocket_init/3` always returns + describe "websocket_send/2" do + # No tests for `websocket_send/2` as `websocket_init/3` always returns # a 501 response, so no socket is ever initialized to be handled. end else @@ -818,14 +820,11 @@ defmodule TestServer.HTTPTest do end test "with `:match` function" do - assert {:ok, socket} = - TestServer.HTTP.websocket_init("/ws", init_state: %{custom: true}) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") assert :ok = TestServer.HTTP.websocket_handle(socket, - match: fn _frame, %{custom: true} -> - true - end + match: fn {:text, message}, _state -> message == "hello" end ) assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) @@ -857,7 +856,7 @@ defmodule TestServer.HTTPTest do end end - describe "websocket_info/2" do + describe "websocket_send/2" do test "when instance not running" do {:ok, instance} = TestServer.HTTP.start() assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") @@ -866,7 +865,7 @@ defmodule TestServer.HTTPTest do assert_raise RuntimeError, ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, fn -> - TestServer.HTTP.websocket_info(socket) + TestServer.HTTP.websocket_send(socket) end end @@ -879,7 +878,7 @@ defmodule TestServer.HTTPTest do assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) - assert :ok = TestServer.HTTP.websocket_info(socket, fn _state -> :invalid end) + assert :ok = TestServer.HTTP.websocket_send(socket, to: fn _state -> :invalid end) assert {:ok, message} = WebSocketClient.receive_message(client) assert message =~ "(RuntimeError) Invalid callback response, got: :invalid." end @@ -899,7 +898,7 @@ defmodule TestServer.HTTPTest do assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) assert :ok = - TestServer.HTTP.websocket_info(socket, fn _state -> raise "boom" end) + TestServer.HTTP.websocket_send(socket, to: fn _state -> raise "boom" end) assert {:ok, message} = WebSocketClient.receive_message(client) assert message =~ "(RuntimeError) boom" @@ -916,9 +915,11 @@ defmodule TestServer.HTTPTest do assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) assert :ok = - TestServer.HTTP.websocket_info(socket, fn state -> - {:reply, {:text, "pong"}, state} - end) + TestServer.HTTP.websocket_send(socket, + to: fn state -> + {:reply, {:text, "pong"}, state} + end + ) assert {:ok, "pong"} = WebSocketClient.receive_message(client) end @@ -927,7 +928,7 @@ defmodule TestServer.HTTPTest do assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) - assert :ok = TestServer.HTTP.websocket_info(socket) + assert :ok = TestServer.HTTP.websocket_send(socket) assert {:ok, "ping"} = WebSocketClient.receive_message(client) end end diff --git a/test/test_server/ssh_test.exs b/test/test_server/ssh_test.exs index 96ccbd2..caae5eb 100644 --- a/test/test_server/ssh_test.exs +++ b/test/test_server/ssh_test.exs @@ -452,13 +452,13 @@ defmodule TestServer.SSHTest do test "with invalid options" do assert_raise ArgumentError, ~r/expected :all, got: :invalid/, fn -> - TestServer.SSH.channel(listen: :invalid) + TestServer.SSH.channel(messages: :invalid) end assert_raise ArgumentError, ~r/expected list to only include \[:exec, :data, :env, :pty, :shell, :eof\], got: \[:invalid\]/, fn -> - TestServer.SSH.channel(listen: [:invalid]) + TestServer.SSH.channel(messages: [:invalid]) end end @@ -534,8 +534,8 @@ defmodule TestServer.SSHTest do assert io =~ "The following channels have been used:" end - test "with `listen: :all` option" do - {:ok, channel} = TestServer.SSH.channel(listen: :all) + test "with `messages: :all` option" do + {:ok, channel} = TestServer.SSH.channel(messages: :all) :ok = TestServer.SSH.handle(channel, @@ -597,8 +597,8 @@ defmodule TestServer.SSHTest do assert SSHClient.close(conn, channel_id) == :ok end - test "with `:listen` option filtering messages" do - {:ok, channel} = TestServer.SSH.channel(listen: []) + test "with `:messages` option" do + {:ok, channel} = TestServer.SSH.channel(messages: []) TestServer.SSH.handle(channel, to: fn msg, _state ->