From 48b96e72a7587441ce6392c5a5f45446afa94b2e Mon Sep 17 00:00:00 2001 From: Richard Ash Date: Wed, 4 Mar 2026 17:37:35 -0800 Subject: [PATCH] Refactor TestServer to TestServer.HTTP --- CHANGELOG.md | 4 + README.md | 150 +---- lib/test_server.ex | 635 ++---------------- lib/test_server/http.ex | 494 ++++++++++++++ lib/test_server/http/README.md | 159 +++++ lib/test_server/{ => http}/instance.ex | 36 +- lib/test_server/{ => http}/plug.ex | 6 +- .../{http_server.ex => http/server.ex} | 22 +- .../{http_server => http/server}/bandit.ex | 20 +- .../server}/bandit/adapter.ex | 2 +- .../server}/bandit/plug.ex | 10 +- .../{http_server => http/server}/httpd.ex | 16 +- .../server}/plug_cowboy.ex | 24 +- lib/test_server/{ => http}/websocket.ex | 6 +- lib/test_server/instance_manager.ex | 69 +- mix.exs | 16 +- ...ttp2_adapter_test.exs => adapter_test.exs} | 31 +- test/test_helper.exs | 2 +- .../http_test.exs} | 465 ++++++------- 19 files changed, 1097 insertions(+), 1070 deletions(-) create mode 100644 lib/test_server/http.ex create mode 100644 lib/test_server/http/README.md rename lib/test_server/{ => http}/instance.ex (93%) rename lib/test_server/{ => http}/plug.ex (91%) rename lib/test_server/{http_server.ex => http/server.ex} (89%) rename lib/test_server/{http_server => http/server}/bandit.ex (81%) rename lib/test_server/{http_server => http/server}/bandit/adapter.ex (98%) rename lib/test_server/{http_server => http/server}/bandit/plug.ex (67%) rename lib/test_server/{http_server => http/server}/httpd.ex (93%) rename lib/test_server/{http_server => http/server}/plug_cowboy.ex (83%) rename lib/test_server/{ => http}/websocket.ex (92%) rename test/http_server/bandit/{http2_adapter_test.exs => adapter_test.exs} (71%) rename test/{test_server_test.exs => test_server/http_test.exs} (58%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d9b57..a276cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- BREAKING CHANGE: Moved HTTP and WebSocket API to `TestServer.HTTP` and server adapters to `TestServer.HTTP.Server.*` + ## v0.1.22 (2026-03-05) Requires Elixir 1.14 or higher. diff --git a/README.md b/README.md index 9b38aeb..6ef52cf 100644 --- a/README.md +++ b/README.md @@ -15,155 +15,9 @@ Features: - Built-in TLS with self-signed certificates - Plug route matching -## Usage +## Protocols -Add route request expectations with `TestServer.add/2`: - -```elixir -test "fetch_url/0" do - # The test server will autostart the current test server, if not already running - TestServer.add("/", via: :get) - - # The URL is derived from the current test server instance - Application.put_env(:my_app, :fetch_url, TestServer.url()) - - {:ok, "HTTP"} = MyModule.fetch_url() -end -``` - -`TestServer.add/2` can route a request to an anonymous function or plug with `:to` option. - -```elixir -TestServer.add("/", to: fn conn -> - Plug.Conn.send_resp(conn, 200, "OK") -end) - -TestServer.add("/", to: MyPlug) -``` - -The method listened to can be defined with `:via` option. By default any method is matched. - -```elixir -TestServer.add("/", via: :post) -``` - -A custom match function can be set with `:match` option: - -```elixir -TestServer.add("/", match: fn - %{params: %{"a" => "1"}} = _conn -> true - _conn -> false -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.add("/", via: :get, to: &Plug.Conn.send_resp(&1, 200, "first")) -TestServer.add("/", via: :get, to: &Plug.Conn.send_resp(&1, 200, "second")) - -{:ok, "first"} = fetch_request() -{:ok, "second"} = fetch_request() -``` - -Plugs can be added to the pipeline with `TestServer.plug/1`. All plugs will run before any routes are matched. `Plug.Conn.fetch_query_params/1` is used if no plugs are set. - -```elixir -TestServer.plug(fn conn -> - Plug.Conn.fetch_query_params(conn) -end) - -TestServer.plug(fn conn -> - {:ok, body, _conn} = Plug.Conn.read_body(conn, []) - - %{conn | body_params: Jason.decode!(body)} -end) - -TestServer.plug(MyPlug) -``` - -### HTTPS - -By default the test server is set up to serve plain HTTP. HTTPS can be enabled with the `:scheme` option when calling `TestServer.start/1`. - -Custom SSL certificates can also be used by defining the `:tls` option: - -```elixir -TestServer.start(scheme: :https, tls: [keyfile: key, certfile: cert]) -``` - -A self-signed certificate suite is automatically generated if you don't set the `:tls` options: - -```elixir -TestServer.start(scheme: :https) - -req_opts = [ - connect_options: [ - transport_opts: [cacerts: TestServer.x509_suite().cacerts], - protocols: [:http2] - ] -] - -assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} = - Req.get(TestServer.url(), req_opts) -``` - -### WebSocket - -WebSocket endpoint can be set up by calling `TestServer.websocket_init/2`. By default, `TestServer.websocket_handle/2` will echo the message received. Messages can be send from the test server with `TestServer.websocket_info/2`. - -```elixir -test "WebSocketClient" do - {:ok, socket} = TestServer.websocket_init("/ws") - - :ok = TestServer.websocket_handle(socket) - :ok = TestServer.websocket_handle(socket, to: fn {:text, "ping"}, state -> {:reply, {:text, "pong"}, state} end) - :ok = TestServer.websocket_handle(socket, match: fn {:text, message}, _state -> message == "hi" end) - - {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) - - :ok = WebSocketClient.send(client, "hello") - {:ok, "hello"} = WebSocketClient.receive(client) - - :ok = WebSocketClient.send(client, "ping") - {:ok, "pong"} = WebSocketClient.receive(client) - - :ok = WebSocketClient.send("hi") - {:ok, "hi"} = WebSocketClient.receive(client) - - :ok = TestServer.websocket_info(socket, fn state -> {:reply, {:text, "ping"}, state} end) - {:ok, "ping"} = WebSocketClient.receive(client) -end -``` - -*Note: WebSocket is not supported by the `:httpd` adapter.* - -### HTTP Server Adapter - -TestServer supports `Bandit`, `Plug.Cowboy`, and `:httpd` out of the box. The HTTP adapter will be selected in this order depending which is available in the dependencies. You can also explicitly set the http server in the configuration when calling `TestServer.start/1`: - -```elixir -TestServer.start(http_server: {TestServer.HTTPServer.Bandit, []}) -``` - -You can create your own plug based HTTP Server Adapter by using the `TestServer.HTTPServer` behaviour. - -### IPv6 - -Use the `:ipfamily` option to test with IPv6 when starting the test server with `TestServer.start/1`: - -```elixir -TestServer.start(ipfamily: :inet6) - -assert :ok = - TestServer.add("/", - to: fn conn -> - assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} - - Plug.Conn.resp(conn, 200, "OK") - end - ) -``` +- `TestServer.HTTP` - HTTP/1, HTTP/2, and WebSocket. diff --git a/lib/test_server.ex b/lib/test_server.ex index b37bfd1..49d0583 100644 --- a/lib/test_server.ex +++ b/lib/test_server.ex @@ -5,633 +5,110 @@ defmodule TestServer do |> String.split("") |> Enum.fetch!(1) - alias Plug.Conn - alias TestServer.{Instance, InstanceManager} - @type instance :: pid() - @type route :: reference() @type stacktrace :: list() - @type websocket_socket :: {instance(), route()} - @type websocket_frame :: {atom(), any()} - @type websocket_state :: any() - @type websocket_reply :: - {:reply, websocket_frame(), websocket_state()} | {:ok, websocket_state()} - - @doc """ - Start a test server instance. - - The instance will be terminated when the test case finishes. - ## Options + alias TestServer.InstanceManager - * `: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.HTTPServer.Bandit, []}`, - `{TestServer.HTTPServer.Plug.Cowboy, []}`, or - `{TestServer.HTTPServer.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`; + @doc false + def get_pruned_stacktrace(calling_module) do + {:current_stacktrace, [{Process, :info, _, _} | stacktrace]} = + Process.info(self(), :current_stacktrace) - ## Examples + first_module_entry = + stacktrace + |> Enum.reverse() + |> Enum.find(fn {mod, _, _, _} -> mod == calling_module end) - TestServer.start( - scheme: :https, - ipfamily: :inet6, - http_server: {TestServer.HTTPServer.Bandit, [ip: :any]} - ) + [first_module_entry] ++ prune_stacktrace(stacktrace, calling_module) + end - TestServer.add("/", - to: fn conn -> - assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} + defp prune_stacktrace([{TestServer, _, _, _} | t], mod), do: prune_stacktrace(t, mod) - Plug.Conn.resp(conn, 200, to_string(Plug.Conn.get_http_protocol(conn))) - end - ) + defp prune_stacktrace([{TestServer.InstanceManager, _, _, _} | t], mod), + do: prune_stacktrace(t, mod) - req_opts = [ - connect_options: [ - transport_opts: [cacerts: TestServer.x509_suite().cacerts], - protocols: [:http2] - ] - ] + defp prune_stacktrace([{mod, _, _, _} | t], mod), do: prune_stacktrace(t, mod) + defp prune_stacktrace([{ExUnit.Assertions, _, _, _} | t], mod), do: prune_stacktrace(t, mod) + defp prune_stacktrace([{ExUnit.Runner, _, _, _} | _], _mod), do: [] + defp prune_stacktrace([h | t], mod), do: [h | prune_stacktrace(t, mod)] + defp prune_stacktrace([], _mod), do: [] - assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} = - Req.get(TestServer.url(), req_opts) - """ - @spec start(keyword()) :: {:ok, pid()} - def start(options \\ []) do + @doc false + def start_instance(protocol_module, options, verify_fn) do case ExUnit.fetch_test_supervisor() do - {:ok, sup} -> - start_with_ex_unit(options, sup) - - :error -> - raise ArgumentError, "can only be called in a test process" + {:ok, _sup} -> start_with_ex_unit(protocol_module, options, verify_fn) + :error -> raise ArgumentError, "can only be called in a test process" end end - defp start_with_ex_unit(options, _sup) do - [_first_module_entry | stacktrace] = get_stacktrace() + defp start_with_ex_unit(protocol_module, options, verify_fn) do + caller = self() - case InstanceManager.start_instance(self(), stacktrace, options) do + case InstanceManager.start_instance(caller, protocol_module, options) do {:ok, instance} -> - put_ex_unit_on_exit_callback(instance) - + put_ex_unit_on_exit_callback(instance, verify_fn) {:ok, instance} {:error, error} -> - raise_start_failure({:error, error}) + raise_start_failure(protocol_module, error) end end - defp put_ex_unit_on_exit_callback(instance) do + defp put_ex_unit_on_exit_callback(instance, verify_fn) do ExUnit.Callbacks.on_exit(fn -> - case Process.alive?(instance) do - true -> - verify_routes!(instance) - verify_websocket_handlers!(instance) - stop(instance) - - false -> - :ok + if Process.alive?(instance) do + verify_fn.(instance) + InstanceManager.stop_instance(instance) end end) end - defp raise_start_failure({:error, {{:EXIT, reason}, _spec}}) do - raise_start_failure({:error, reason}) - end - - defp raise_start_failure({:error, error}) do - raise """ - EXIT when starting #{inspect(__MODULE__.Instance)}: - - #{Exception.format_exit(error)} - """ + defp raise_start_failure(_calling_module, {:EXIT, reason}) do + raise_start_failure(nil, reason) end - defp verify_routes!(instance) do - instance - |> Instance.routes() - |> Enum.reject(& &1.suspended) - |> case do - [] -> - :ok - - active_routes -> - raise """ - #{Instance.format_instance(instance)} did not receive a request for these routes before the test ended: - - #{Instance.format_routes(active_routes)} - """ - end - end - - defp verify_websocket_handlers!(instance) do - instance - |> Instance.websocket_handlers() - |> Enum.reject(& &1.suspended) - |> case do - [] -> - :ok - - active_websocket_handlers -> - raise """ - #{Instance.format_instance(instance)} did not receive a frame for these websocket handlers before the test ended: - - #{Instance.format_websocket_handlers(active_websocket_handlers)} - """ - end + defp raise_start_failure(calling_module, error) do + raise "EXIT when starting #{inspect(calling_module)} instance:\n\n#{Exception.format_exit(error)}" end - @doc """ - Shuts down the current test server. - - ## Examples - - TestServer.start() - url = TestServer.url() - TestServer.stop() - - assert {:error, %Req.TransportError{}} = Req.get(url, retry: false) - """ - @spec stop() :: :ok | {:error, term()} - def stop, do: stop(fetch_instance!()) - - @doc """ - Shuts down a test server instance. - """ - @spec stop(pid()) :: :ok | {:error, term()} - def stop(instance) do - instance_alive!(instance) + @doc false + def stop_instance(protocol_module, instance) do + ensure_instance_alive!(protocol_module, instance) InstanceManager.stop_instance(instance) end - defp instance_alive!(instance) do + @doc false + def ensure_instance_alive!(protocol_module, instance) do case Process.alive?(instance) do true -> :ok - false -> raise "#{Instance.format_instance(instance)} is not running" - end - end - - @doc """ - Gets current test server instance if running. - - ## Examples - - refute TestServer.get_instance() - - {:ok, instance} = TestServer.start() - - assert TestServer.get_instance() == instance - """ - @spec get_instance() :: pid() | nil - def get_instance do - case fetch_instance(false) do - {:ok, instance} -> instance - :error -> nil - end - end - - @spec url() :: binary() - def url, do: url("") - - @spec url(binary() | keyword() | pid()) :: binary() - def url(uri) when is_binary(uri), do: url(uri, []) - def url(opts) when is_list(opts), do: url("", opts) - def url(instance) when is_pid(instance), do: url(instance, "", []) - - @doc """ - Produces a URL for current test server. - - ## Options - * `:host` - binary host value, it'll be added to inet for IP `127.0.0.1` and `::1`, defaults to `"localhost"`; - - ## Examples - - TestServer.start(port: 4444) - - assert TestServer.url() == "http://localhost:4444" - assert TestServer.url("/test") == "http://localhost:4444/test" - assert TestServer.url(host: "example.com") == "http://example.com:4444" - """ - @spec url(binary(), keyword()) :: binary() - def url(uri, opts) when is_binary(uri), do: url(fetch_instance!(), uri, opts) - - @spec url(pid(), binary()) :: binary() - def url(instance, uri) when is_pid(instance), do: url(instance, uri, []) - - @doc """ - Produces a URL for a test server instance. - - See `url/2` for options. - """ - @spec url(pid(), binary(), keyword()) :: binary() - def url(instance, uri, opts) do - instance_alive!(instance) - - unless is_nil(opts[:host]) or is_binary(opts[:host]), - do: raise("Invalid host, got: #{inspect(opts[:host])}") - - domain = maybe_enable_host(opts[:host]) - options = Instance.get_options(instance) - - "#{Keyword.fetch!(options, :scheme)}://#{domain}:#{Keyword.fetch!(options, :port)}#{uri}" - end - - defp fetch_instance! do - case fetch_instance() do - :error -> raise "No current #{inspect(Instance)} running" - {:ok, instance} -> instance - end - end - - defp fetch_instance(function_accepts_instance_arg \\ true) do - case InstanceManager.get_by_caller(self()) do - nil -> - :error - - [instance] -> - {:ok, instance} - - [_instance | _rest] = instances -> - [{m, f, a, _} | _stacktrace] = get_stacktrace() - - message = - case function_accepts_instance_arg do - true -> - "Multiple #{inspect(Instance)}'s running, please pass instance to `#{inspect(m)}.#{f}/#{a}`." - - false -> - "Multiple #{inspect(Instance)}'s running." - end - - formatted_instances = - instances - |> Enum.map(&{&1, Instance.get_options(&1)}) - |> Enum.with_index() - |> Enum.map_join("\n\n", fn {{instance, options}, index} -> - """ - ##{index + 1}: #{Instance.format_instance(instance)} - #{Enum.map_join(options[:stacktrace], "\n ", &Exception.format_stacktrace_entry/1)}")} - """ - end) - - raise """ - #{message} - - #{formatted_instances} - """ + false -> raise "#{format_instance(protocol_module, instance)} is not running" end end - defp maybe_enable_host(nil), do: "localhost" - - defp maybe_enable_host(host) do - :inet_db.set_lookup([:file, :dns]) - :inet_db.add_host({127, 0, 0, 1}, [String.to_charlist(host)]) - :inet_db.add_host({0, 0, 0, 0, 0, 0, 0, 1}, [String.to_charlist(host)]) - - host - end - - @spec add(binary()) :: :ok - def add(uri), do: add(uri, []) - - @doc """ - Adds a route to the current test server. - - Matching routes are handled FIFO (first in, first out). Any requests to - routes not added to the TestServer and any routes that isn't matched will - raise an error in the test case. - - ## Options - - * `: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. - - ## Examples - - TestServer.add("/", - match: fn conn -> - conn.query_params["a"] == "1" - end, - to: fn conn -> - Plug.Conn.resp(conn, 200, "a = 1") - end) - - TestServer.add("/", to: &Plug.Conn.resp(&1, 200, "PONG")) - TestServer.add("/") - - assert {:ok, %Req.Response{status: 200, body: "PONG"}} = Req.get(TestServer.url("/")) - assert {:ok, %Req.Response{status: 200, body: "HTTP/1.1"}} = Req.post(TestServer.url("/")) - assert {:ok, %Req.Response{status: 200, body: "a = 1"}} = Req.get(TestServer.url("/?a=1")) - """ - @spec add(binary(), keyword()) :: :ok - def add(uri, options) when is_binary(uri) do - {:ok, instance} = autostart() - - add(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, []) - - @doc """ - Adds a route to a test server instance. - - See `add/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 - options = Keyword.put_new(options, :to, &default_response_handler/1) - - {:ok, _route} = register_route(instance, uri, options) - - :ok - end - - defp register_route(instance, uri, options) do - instance_alive!(instance) - - [_register_route, _first_module_entry | stacktrace] = get_stacktrace() - - Instance.register(instance, {:plug_router_to, {uri, options, stacktrace}}) - end - - defp get_stacktrace do - {:current_stacktrace, [{Process, :info, _, _} | stacktrace]} = - Process.info(self(), :current_stacktrace) + @doc false + def format_instance(protocol_module, instance) do + instance_module = Module.concat(protocol_module, Instance) - first_module_entry = - stacktrace - |> Enum.reverse() - |> Enum.find(fn {mod, _, _, _} -> mod == __MODULE__ end) - - [first_module_entry] ++ prune_stacktrace(stacktrace) + "#{inspect(instance_module)} #{inspect(instance)}" end - # Remove TestServer - defp prune_stacktrace([{__MODULE__, _, _, _} | t]), do: prune_stacktrace(t) - - # Assertions can pop-up in the middle of the stack - defp prune_stacktrace([{ExUnit.Assertions, _, _, _} | t]), do: prune_stacktrace(t) - - # As soon as we see a Runner, it is time to ignore the stacktrace - defp prune_stacktrace([{ExUnit.Runner, _, _, _} | _]), do: [] - - # All other cases - defp prune_stacktrace([h | t]), do: [h | prune_stacktrace(t)] - defp prune_stacktrace([]), do: [] - - defp autostart do - case fetch_instance() do - :error -> start() + @doc false + def autostart_instance(protocol_module) do + case InstanceManager.fetch_instance(self(), protocol_module) do + :error -> protocol_module.start() {:ok, instance} -> {:ok, instance} end end - defp default_response_handler(conn) do - Conn.resp(conn, 200, to_string(Conn.get_http_protocol(conn))) - end - - @doc """ - Adds a plug to the current test server. - - This plug will be called for all requests before route is matched. - - ## Examples - - TestServer.plug(MyPlug) - - TestServer.plug(fn conn -> - {:ok, body, _conn} = Plug.Conn.read_body(conn, []) - - %{conn | body_params: Jason.decode!(body)} - end) - """ - @spec plug(module() | function()) :: :ok - def plug(plug) do - {:ok, instance} = autostart() - - plug(instance, plug) - end - - @doc """ - Adds a route to a test server instance. - - See `plug/1` for more. - """ - @spec plug(pid(), module() | function()) :: :ok - def plug(instance, plug) do - [_first_module_entry | stacktrace] = get_stacktrace() - - {:ok, _plug} = Instance.register(instance, {:plug, {plug, stacktrace}}) - - :ok - end - - @doc """ - Fetches the generated x509 suite for the current test server. - - ## Examples - - TestServer.start(scheme: :https) - TestServer.add("/") - - cacerts = TestServer.x509_suite().cacerts - req_opts = [connect_options: [transport_opts: [cacerts: cacerts]]] - - assert {:ok, %Req.Response{status: 200, body: "HTTP/1.1"}} = - Req.get(TestServer.url(), req_opts) - """ - @spec x509_suite() :: term() - def x509_suite, do: x509_suite(fetch_instance!()) - - @doc """ - Fetches the generated x509 suite for a test server instance. - - See `x509_suite/0` for more. - """ - @spec x509_suite(pid()) :: term() - def x509_suite(instance) do - instance_alive!(instance) - - options = Instance.get_options(instance) - - cond do - not (options[:scheme] == :https) -> - raise "#{Instance.format_instance(instance)} is not running with `[scheme: :https]` option" - - not Keyword.has_key?(options, :x509_suite) -> - raise "#{Instance.format_instance(instance)} is running with custom SSL" - - true -> - options[:x509_suite] - end - end - - @spec websocket_init(binary()) :: {:ok, websocket_socket()} | {:error, term()} - 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 - websocket will be initiated otherwise response is returned as-is. - - ## Options - - Takes the same options as `add/2`, except `:to`. - - ## Examples - - {:ok, socket} = TestServer.websocket_init("/ws") - TestServer.websocket_handle(socket) - - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) - assert WebSocketClient.send_message(client, "echo") == {:ok, "echo"} - - `:via` and `:match` are called during the HTTP handshake: - - TestServer.websocket_init("/ws", via: :get, match: fn conn -> - conn.params["token"] == "secret" - end) - - assert {:ok, _client} = WebSocketClient.start_link(TestServer.url("/ws?token=secret")) - - `:to` option is also called during the HTTP handshake: - - TestServer.websocket_init("/ws", - to: fn conn -> - Plug.Conn.send_resp(conn, 403, "Forbidden") - end - ) - - assert {:error, %WebSockex.RequestError{code: 403}} = - WebSocketClient.start_link(TestServer.url("/ws")) - """ - @spec websocket_init(binary(), keyword()) :: {:ok, websocket_socket()} - def websocket_init(uri, options) when is_binary(uri) do - {:ok, instance} = autostart() - - websocket_init(instance, uri, options) - end - - @spec websocket_init(pid(), binary()) :: {:ok, websocket_socket()} - def websocket_init(instance, uri) when is_pid(instance) and is_binary(uri) do - websocket_init(instance, uri, []) - end - - @doc """ - Adds a websocket route to a test server. - - See `websocket_init/2` for options. - """ - @spec websocket_init(pid(), binary(), keyword()) :: {:ok, websocket_socket()} - def websocket_init(instance, uri, options) do - options = - options - |> Keyword.put(:websocket, true) - |> Keyword.put_new(:to, & &1) - - {:ok, %{ref: ref}} = register_route(instance, uri, options) - - {:ok, {instance, ref}} - end - - @spec websocket_handle(websocket_socket()) :: :ok | {:error, term()} - def websocket_handle(socket), do: websocket_handle(socket, []) - - @doc """ - Adds a message handler to a websocket instance. - - Messages are matched FIFO (first in, first out). Any messages not expected by - TestServer or any message expectations not receiving a message will raise an - error in the test case. - - ## 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. - - ## Examples - - {:ok, socket} = TestServer.websocket_init("/ws") - - TestServer.websocket_handle( - socket, - to: fn _frame, state -> - {:reply, {:text, "pong"}, state} - end, - match: fn frame, _state -> - frame == {:text, "ping"} - end) - - TestServer.websocket_handle(socket) - - {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) - - assert WebSocketClient.send_message(client, "echo") == {:ok, "echo"} - assert WebSocketClient.send_message(client, "ping") == {:ok, "pong"} - """ - @spec websocket_handle(websocket_socket(), keyword()) :: :ok - def websocket_handle({instance, _route_ref} = socket, options) do - instance_alive!(instance) - - [_first_module_entry | stacktrace] = get_stacktrace() - - options = Keyword.put_new(options, :to, &default_websocket_handle/2) - - {:ok, _handler} = Instance.register(socket, {:websocket, {:handle, options, stacktrace}}) - - :ok - end - - defp default_websocket_handle(frame, state), - do: {:reply, frame, state} - - @doc """ - Sends an message to a websocket instance. - - ## Examples - - {:ok, socket} = TestServer.websocket_init("/ws") - {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) - - assert TestServer.websocket_info(socket, 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 - instance_alive!(instance) + @doc false + def fetch_instance!(protocol_module) do + instance_module = Module.concat(protocol_module, Instance) - [_first_module_entry | stacktrace] = get_stacktrace() - - callback = callback || (&default_websocket_info/1) - - for pid <- Instance.active_websocket_connections(socket) do - send(pid, {callback, stacktrace}) + case InstanceManager.fetch_instance(self(), protocol_module) do + :error -> raise "No current #{inspect(instance_module)} running" + {:ok, instance} -> instance end - - :ok end - - defp default_websocket_info(state), do: {:reply, {:text, "ping"}, state} end diff --git a/lib/test_server/http.ex b/lib/test_server/http.ex new file mode 100644 index 0000000..29e83ba --- /dev/null +++ b/lib/test_server/http.ex @@ -0,0 +1,494 @@ +defmodule TestServer.HTTP do + @external_resource "lib/test_server/http/README.md" + @moduledoc "lib/test_server/http/README.md" + |> File.read!() + |> String.split("") + |> Enum.fetch!(1) + + alias Plug.Conn + alias TestServer.HTTP.{Instance, Server} + + @type route :: reference() + @type websocket_socket :: {TestServer.instance(), route()} + @type websocket_frame :: {atom(), any()} + @type websocket_state :: any() + @type websocket_reply :: + {:reply, websocket_frame(), websocket_state()} | {:ok, websocket_state()} + + @doc """ + Start a test server instance. + + The instance will be terminated when the test case finishes. + + ## Options + + * `: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.HTTPServer.Bandit, []}`, + `{TestServer.HTTPServer.Plug.Cowboy, []}`, or + `{TestServer.HTTPServer.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`; + + ## Examples + + TestServer.HTTP.start( + scheme: :https, + ipfamily: :inet6, + http_server: {TestServer.HTTPServer.Bandit, [ip: :any]} + ) + + TestServer.HTTP.add("/", + to: fn conn -> + assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} + + Plug.Conn.resp(conn, 200, to_string(Plug.Conn.get_http_protocol(conn))) + end + ) + + req_opts = [ + connect_options: [ + transport_opts: [cacerts: TestServer.HTTP.x509_suite().cacerts], + protocols: [:http2] + ] + ] + + assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} = + Req.get(TestServer.HTTP.url(), req_opts) + """ + @spec start(keyword()) :: {:ok, pid()} + def start(options \\ []) do + TestServer.start_instance(__MODULE__, options, &verify_instance!/1) + end + + defp verify_instance!(instance) do + verify_routes!(instance) + verify_websocket_handlers!(instance) + end + + defp verify_routes!(instance) do + instance + |> Instance.routes() + |> Enum.reject(& &1.suspended) + |> case do + [] -> + :ok + + active_routes -> + raise """ + #{TestServer.format_instance(__MODULE__, instance)} did not receive a request for these routes before the test ended: + + #{Instance.format_routes(active_routes)} + """ + end + end + + defp verify_websocket_handlers!(instance) do + instance + |> Instance.websocket_handlers() + |> Enum.reject(& &1.suspended) + |> case do + [] -> + :ok + + active_websocket_handlers -> + raise """ + #{TestServer.format_instance(__MODULE__, instance)} did not receive a frame for these websocket handlers before the test ended: + + #{Instance.format_websocket_handlers(active_websocket_handlers)} + """ + end + end + + @doc """ + Shuts down the current test server. + + ## Examples + + TestServer.HTTP.start() + url = TestServer.HTTP.url() + TestServer.HTTP.stop() + + assert {:error, %Req.TransportError{}} = Req.get(url, retry: false) + """ + @spec stop() :: :ok | {:error, term()} + def stop, do: stop(TestServer.fetch_instance!(__MODULE__)) + + @doc """ + Shuts down a test server instance. + """ + @spec stop(pid()) :: :ok | {:error, term()} + def stop(instance) do + TestServer.ensure_instance_alive!(__MODULE__, instance) + + :ok = Server.stop(Instance.get_options(instance)) + + TestServer.stop_instance(__MODULE__, instance) + end + + @spec url() :: binary() + def url, do: url("") + + @spec url(binary() | keyword() | pid()) :: binary() + def url(uri) when is_binary(uri), do: url(uri, []) + def url(opts) when is_list(opts), do: url("", opts) + def url(instance) when is_pid(instance), do: url(instance, "", []) + + @doc """ + Produces a URL for current test server. + + ## Options + * `:host` - binary host value, it'll be added to inet for IP `127.0.0.1` and `::1`, defaults to `"localhost"`; + + ## Examples + + TestServer.HTTP.start(port: 4444) + + assert TestServer.HTTP.url() == "http://localhost:4444" + assert TestServer.HTTP.url("/test") == "http://localhost:4444/test" + assert TestServer.HTTP.url(host: "example.com") == "http://example.com:4444" + """ + @spec url(binary(), keyword()) :: binary() + def url(uri, opts) when is_binary(uri), + do: url(TestServer.fetch_instance!(__MODULE__), uri, opts) + + @spec url(pid(), binary()) :: binary() + def url(instance, uri) when is_pid(instance), do: url(instance, uri, []) + + @doc """ + Produces a URL for a test server instance. + + See `url/2` for options. + """ + @spec url(pid(), binary(), keyword()) :: binary() + def url(instance, uri, opts) do + TestServer.ensure_instance_alive!(__MODULE__, instance) + + unless is_nil(opts[:host]) or is_binary(opts[:host]), + do: raise("Invalid host, got: #{inspect(opts[:host])}") + + domain = maybe_enable_host(opts[:host]) + options = Instance.get_options(instance) + + "#{Keyword.fetch!(options, :scheme)}://#{domain}:#{Keyword.fetch!(options, :port)}#{uri}" + end + + defp maybe_enable_host(nil), do: "localhost" + + defp maybe_enable_host(host) do + :inet_db.set_lookup([:file, :dns]) + :inet_db.add_host({127, 0, 0, 1}, [String.to_charlist(host)]) + :inet_db.add_host({0, 0, 0, 0, 0, 0, 0, 1}, [String.to_charlist(host)]) + + host + end + + @spec add(binary()) :: :ok + def add(uri), do: add(uri, []) + + @doc """ + Adds a route to the current test server. + + Matching routes are handled FIFO (first in, first out). Any requests to + routes not added to the TestServer and any routes that isn't matched will + raise an error in the test case. + + ## Options + + * `: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. + + ## Examples + + TestServer.HTTP.add("/", + match: fn conn -> + conn.query_params["a"] == "1" + end, + to: fn conn -> + Plug.Conn.resp(conn, 200, "a = 1") + end) + + TestServer.HTTP.add("/", to: &Plug.Conn.resp(&1, 200, "PONG")) + TestServer.HTTP.add("/") + + 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 + {:ok, instance} = TestServer.autostart_instance(__MODULE__) + + add(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, []) + + @doc """ + Adds a route to a test server instance. + + See `add/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 + options = Keyword.put_new(options, :to, &default_response_handler/1) + + {:ok, _route} = register_route(instance, uri, options) + + :ok + end + + defp register_route(instance, uri, options) do + TestServer.ensure_instance_alive!(__MODULE__, instance) + + [_register_route, _first_module_entry | stacktrace] = + TestServer.get_pruned_stacktrace(__MODULE__) + + Instance.register(instance, {:plug_router_to, {uri, options, stacktrace}}) + end + + defp default_response_handler(conn) do + Conn.resp(conn, 200, to_string(Conn.get_http_protocol(conn))) + end + + @doc """ + Adds a plug to the current test server. + + This plug will be called for all requests before route is matched. + + ## Examples + + TestServer.HTTP.plug(MyPlug) + + TestServer.HTTP.plug(fn conn -> + {:ok, body, _conn} = Plug.Conn.read_body(conn, []) + + %{conn | body_params: Jason.decode!(body)} + end) + """ + @spec plug(module() | function()) :: :ok + def plug(plug) do + {:ok, instance} = TestServer.autostart_instance(__MODULE__) + + plug(instance, plug) + end + + @doc """ + Adds a route to a test server instance. + + See `plug/1` for more. + """ + @spec plug(pid(), module() | function()) :: :ok + def plug(instance, plug) do + [_first_module_entry | stacktrace] = TestServer.get_pruned_stacktrace(__MODULE__) + + {:ok, _plug} = Instance.register(instance, {:plug, {plug, stacktrace}}) + + :ok + end + + @doc """ + Fetches the generated x509 suite for the current test server. + + ## Examples + + TestServer.HTTP.start(scheme: :https) + TestServer.HTTP.add("/") + + cacerts = TestServer.HTTP.x509_suite().cacerts + req_opts = [connect_options: [transport_opts: [cacerts: cacerts]]] + + assert {:ok, %Req.Response{status: 200, body: "HTTP/1.1"}} = + Req.get(TestServer.HTTP.url(), req_opts) + """ + @spec x509_suite() :: term() + def x509_suite, do: x509_suite(TestServer.fetch_instance!(__MODULE__)) + + @doc """ + Fetches the generated x509 suite for a test server instance. + + See `x509_suite/0` for more. + """ + @spec x509_suite(pid()) :: term() + def x509_suite(instance) do + TestServer.ensure_instance_alive!(__MODULE__, instance) + + options = Instance.get_options(instance) + + cond do + not (options[:scheme] == :https) -> + raise "#{TestServer.format_instance(__MODULE__, instance)} is not running with `[scheme: :https]` option" + + not Keyword.has_key?(options, :x509_suite) -> + raise "#{TestServer.format_instance(__MODULE__, instance)} is running with custom SSL" + + true -> + options[:x509_suite] + end + end + + @spec websocket_init(binary()) :: {:ok, websocket_socket()} | {:error, term()} + 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 + websocket will be initiated otherwise response is returned as-is. + + ## Options + + Takes the same options as `add/2`, except `:to`. + + ## Examples + + {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + TestServer.HTTP.websocket_handle(socket) + + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) + assert WebSocketClient.send_message(client, "echo") == {:ok, "echo"} + + `:via` and `:match` are called during the HTTP handshake: + + TestServer.HTTP.websocket_init("/ws", via: :get, match: fn conn -> + conn.params["token"] == "secret" + end) + + assert {:ok, _client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws?token=secret")) + + `:to` option is also called during the HTTP handshake: + + TestServer.HTTP.websocket_init("/ws", + to: fn conn -> + Plug.Conn.send_resp(conn, 403, "Forbidden") + end + ) + + assert {:error, %WebSockex.RequestError{code: 403}} = + WebSocketClient.start_link(TestServer.HTTP.url("/ws")) + """ + @spec websocket_init(binary(), keyword()) :: {:ok, websocket_socket()} + def websocket_init(uri, options) when is_binary(uri) do + {:ok, instance} = TestServer.autostart_instance(__MODULE__) + + websocket_init(instance, uri, options) + end + + @spec websocket_init(pid(), binary()) :: {:ok, websocket_socket()} + def websocket_init(instance, uri) when is_pid(instance) and is_binary(uri) do + websocket_init(instance, uri, []) + end + + @doc """ + Adds a websocket route to a test server. + + See `websocket_init/2` for options. + """ + @spec websocket_init(pid(), binary(), keyword()) :: {:ok, websocket_socket()} + def websocket_init(instance, uri, options) do + options = + options + |> Keyword.put(:websocket, true) + |> Keyword.put_new(:to, & &1) + + {:ok, %{ref: ref}} = register_route(instance, uri, options) + + {:ok, {instance, ref}} + end + + @spec websocket_handle(websocket_socket()) :: :ok | {:error, term()} + def websocket_handle(socket), do: websocket_handle(socket, []) + + @doc """ + Adds a message handler to a websocket instance. + + Messages are matched FIFO (first in, first out). Any messages not expected by + TestServer or any message expectations not receiving a message will raise an + error in the test case. + + ## 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. + + ## Examples + + {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + + TestServer.HTTP.websocket_handle( + socket, + to: fn _frame, state -> + {:reply, {:text, "pong"}, state} + end, + match: fn frame, _state -> + frame == {:text, "ping"} + end) + + TestServer.HTTP.websocket_handle(socket) + + {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) + + assert WebSocketClient.send_message(client, "echo") == {:ok, "echo"} + assert WebSocketClient.send_message(client, "ping") == {:ok, "pong"} + """ + @spec websocket_handle(websocket_socket(), keyword()) :: :ok + def websocket_handle({instance, _route_ref} = socket, options) do + TestServer.ensure_instance_alive!(__MODULE__, instance) + + [_first_module_entry | stacktrace] = TestServer.get_pruned_stacktrace(__MODULE__) + + options = Keyword.put_new(options, :to, &default_websocket_handle/2) + + {:ok, _handler} = Instance.register(socket, {:websocket, {:handle, options, stacktrace}}) + + :ok + end + + defp default_websocket_handle(frame, state), + do: {:reply, frame, state} + + @doc """ + Sends an message to a websocket instance. + + ## 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 -> + {: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 + TestServer.ensure_instance_alive!(__MODULE__, instance) + + [_first_module_entry | stacktrace] = TestServer.get_pruned_stacktrace(__MODULE__) + + callback = callback || (&default_websocket_info/1) + + for pid <- Instance.active_websocket_connections(socket) do + send(pid, {callback, stacktrace}) + end + + :ok + end + + defp default_websocket_info(state), do: {:reply, {:text, "ping"}, state} +end diff --git a/lib/test_server/http/README.md b/lib/test_server/http/README.md new file mode 100644 index 0000000..072ad03 --- /dev/null +++ b/lib/test_server/http/README.md @@ -0,0 +1,159 @@ +# HTTP / WebSocket + + + +Mock HTTP/1, HTTP/2, and WebSocket endpoints with route expectations, plug pipelines, and TLS support. + +## Usage + +### HTTP + +Add route request expectations with `TestServer.HTTP.add/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) + + # The URL is derived from the current test server instance + Application.put_env(:my_app, :fetch_url, TestServer.HTTP.url()) + + {:ok, "HTTP"} = MyModule.fetch_url() +end +``` + +`TestServer.HTTP.add/2` can route a request to an anonymous function or plug with `:to` option. + +```elixir +TestServer.HTTP.add("/", to: fn conn -> + Plug.Conn.send_resp(conn, 200, "OK") +end) + +TestServer.HTTP.add("/", to: MyPlug) +``` + +The method listened to can be defined with `:via` option. By default any method is matched. + +```elixir +TestServer.HTTP.add("/", via: :post) +``` + +A custom match function can be set with `:match` option: + +```elixir +TestServer.HTTP.add("/", match: fn + %{params: %{"a" => "1"}} = _conn -> true + _conn -> false +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")) + +{:ok, "first"} = fetch_request() +{:ok, "second"} = fetch_request() +``` + +Plugs can be added to the pipeline with `TestServer.HTTP.plug/1`. All plugs will run before any routes are matched. `Plug.Conn.fetch_query_params/1` is used if no plugs are set. + +```elixir +TestServer.HTTP.plug(fn conn -> + Plug.Conn.fetch_query_params(conn) +end) + +TestServer.HTTP.plug(fn conn -> + {:ok, body, _conn} = Plug.Conn.read_body(conn, []) + + %{conn | body_params: Jason.decode!(body)} +end) + +TestServer.HTTP.plug(MyPlug) +``` + +### HTTPS + +By default the test server is set up to serve plain HTTP. HTTPS can be enabled with the `:scheme` option when calling `TestServer.HTTP.start/1`. + +Custom SSL certificates can also be used by defining the `:tls` option: + +```elixir +TestServer.HTTP.start(scheme: :https, tls: [keyfile: key, certfile: cert]) +``` + +A self-signed certificate suite is automatically generated if you don't set the `:tls` options: + +```elixir +TestServer.HTTP.start(scheme: :https) + +req_opts = [ + connect_options: [ + transport_opts: [cacerts: TestServer.HTTP.x509_suite().cacerts], + protocols: [:http2] + ] +] + +assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} = + Req.get(TestServer.HTTP.url(), req_opts) +``` + +### 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`. + +```elixir +test "WebSocketClient" do + {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + + :ok = TestServer.HTTP.websocket_handle(socket) + :ok = TestServer.HTTP.websocket_handle(socket, to: fn {:text, "ping"}, state -> {:reply, {:text, "pong"}, state} end) + :ok = TestServer.HTTP.websocket_handle(socket, match: fn {:text, message}, _state -> message == "hi" end) + + {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) + + :ok = WebSocketClient.send(client, "hello") + {:ok, "hello"} = WebSocketClient.receive(client) + + :ok = WebSocketClient.send(client, "ping") + {:ok, "pong"} = WebSocketClient.receive(client) + + :ok = WebSocketClient.send("hi") + {:ok, "hi"} = WebSocketClient.receive(client) + + :ok = TestServer.HTTP.websocket_info(socket, fn state -> {:reply, {:text, "ping"}, state} end) + {:ok, "ping"} = WebSocketClient.receive(client) +end +``` + +*Note: WebSocket is not supported by the `:httpd` adapter.* + +### HTTP Server Adapter + +TestServer supports `Bandit`, `Plug.Cowboy`, and `:httpd` out of the box. The HTTP adapter will be selected in this order depending which is available in the dependencies. You can also explicitly set the http server in the configuration when calling `TestServer.HTTP.start/1`: + +```elixir +TestServer.HTTP.start(http_server: {TestServer.HTTPServer.Bandit, []}) +``` + +You can create your own plug based HTTP Server Adapter by using the `TestServer.HTTPServer` behaviour. + +### IPv6 + +Use the `:ipfamily` option to test with IPv6 when starting the test server with `TestServer.HTTP.start/1`: + +```elixir +TestServer.HTTP.start(ipfamily: :inet6) + +assert :ok = + TestServer.HTTP.add("/", + to: fn conn -> + assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} + + Plug.Conn.resp(conn, 200, "OK") + end + ) +``` + + diff --git a/lib/test_server/instance.ex b/lib/test_server/http/instance.ex similarity index 93% rename from lib/test_server/instance.ex rename to lib/test_server/http/instance.ex index a1ae54a..8fa1dc6 100644 --- a/lib/test_server/instance.ex +++ b/lib/test_server/http/instance.ex @@ -1,9 +1,9 @@ -defmodule TestServer.Instance do +defmodule TestServer.HTTP.Instance do @moduledoc false use GenServer - alias TestServer.HTTPServer + alias TestServer.HTTP.Server def start_link(options) do GenServer.start_link(__MODULE__, options) @@ -22,7 +22,8 @@ defmodule TestServer.Instance do GenServer.call(instance, {:register, {:plug_router_to, {uri, options, stacktrace}}}) end - @spec register(pid(), {:plug, {atom() | function(), TestServer.stacktrace()}}) :: {:ok, map()} + @spec register(pid(), {:plug, {atom() | function(), TestServer.stacktrace()}}) :: + {:ok, map()} def register(instance, {:plug, {plug, stacktrace}}) do ensure_plug!(plug) @@ -30,7 +31,7 @@ defmodule TestServer.Instance do end @spec register( - TestServer.websocket_socket(), + TestServer.HTTP.websocket_socket(), {:websocket, {:handle, keyword(), TestServer.stacktrace()}} ) :: {:ok, map()} @@ -59,10 +60,11 @@ defmodule TestServer.Instance do end @spec dispatch( - TestServer.websocket_socket(), - {:websocket, {:handle, TestServer.websocket_frame()}, TestServer.websocket_state()} + TestServer.HTTP.websocket_socket(), + {:websocket, {:handle, TestServer.HTTP.websocket_frame()}, + TestServer.HTTP.websocket_state()} ) :: - {:ok, TestServer.websocket_reply()} + {:ok, TestServer.HTTP.websocket_reply()} | {:error, :not_found} | {:error, {term(), TestServer.stacktrace()}} def dispatch({instance, _router_ref} = socket, {:websocket, {:handle, frame}, state}) do @@ -70,10 +72,11 @@ defmodule TestServer.Instance do end @spec dispatch( - TestServer.websocket_socket(), - {:websocket, {:info, function(), TestServer.stacktrace()}, TestServer.websocket_state()} + TestServer.HTTP.websocket_socket(), + {:websocket, {:info, function(), TestServer.stacktrace()}, + TestServer.HTTP.websocket_state()} ) :: - {:ok, TestServer.websocket_reply()} + {:ok, TestServer.HTTP.websocket_reply()} | {:error, {term(), TestServer.stacktrace()}} def dispatch( {instance, _router_ref} = socket, @@ -95,12 +98,12 @@ defmodule TestServer.Instance do GenServer.call(instance, :routes) end - @spec put_websocket_connection(TestServer.websocket_socket(), pid()) :: :ok + @spec put_websocket_connection(TestServer.HTTP.websocket_socket(), pid()) :: :ok def put_websocket_connection({instance, route_ref}, pid) do GenServer.cast(instance, {:put, :websocket_connection, route_ref, pid}) end - @spec active_websocket_connections(TestServer.websocket_socket()) :: [pid()] + @spec active_websocket_connections(TestServer.HTTP.websocket_socket()) :: [pid()] def active_websocket_connections({instance, route_ref}) do GenServer.call(instance, {:get, :websocket_connections, route_ref}) end @@ -117,11 +120,6 @@ defmodule TestServer.Instance do end) end - @spec format_instance(pid()) :: binary() - def format_instance(instance) do - "#{inspect(__MODULE__)} #{inspect(instance)}" - end - @spec websocket_handlers(pid()) :: [map()] def websocket_handlers(instance) do GenServer.call(instance, :websocket_handlers) @@ -171,7 +169,7 @@ defmodule TestServer.Instance do end defp start_http_server(state) do - case HTTPServer.start(self(), state.options) do + case Server.start(self(), state.options) do {:ok, options} -> state = Map.merge(state, %{options: options}) @@ -315,7 +313,7 @@ defmodule TestServer.Instance do defp run_plugs(conn, state) do state.plugs |> case do - [] -> [%{plug: TestServer.Plug.default_plug(), stacktrace: nil}] + [] -> [%{plug: TestServer.HTTP.Plug.default_plug(), stacktrace: nil}] plugs -> plugs end |> Enum.reduce_while(conn, fn %{plug: plug, stacktrace: stacktrace}, conn -> diff --git a/lib/test_server/plug.ex b/lib/test_server/http/plug.ex similarity index 91% rename from lib/test_server/plug.ex rename to lib/test_server/http/plug.ex index 6642022..6d2c2b6 100644 --- a/lib/test_server/plug.ex +++ b/lib/test_server/http/plug.ex @@ -1,8 +1,8 @@ -defmodule TestServer.Plug do +defmodule TestServer.HTTP.Plug do @moduledoc false alias Plug.Conn - alias TestServer.Instance + alias TestServer.HTTP.Instance def init({http_server, args, instance}), do: {http_server, args, instance} @@ -17,7 +17,7 @@ defmodule TestServer.Plug do {:error, {:not_found, conn}} -> message = - "#{Instance.format_instance(instance)} received an unexpected #{conn.method} request at #{conn.request_path}" + "#{TestServer.format_instance(TestServer.HTTP, instance)} received an unexpected #{conn.method} request at #{conn.request_path}" |> append_formatted_params(conn) |> append_formatted_routes(instance) diff --git a/lib/test_server/http_server.ex b/lib/test_server/http/server.ex similarity index 89% rename from lib/test_server/http_server.ex rename to lib/test_server/http/server.ex index c73c534..f37b34b 100644 --- a/lib/test_server/http_server.ex +++ b/lib/test_server/http/server.ex @@ -1,13 +1,13 @@ -defmodule TestServer.HTTPServer do +defmodule TestServer.HTTP.Server do @moduledoc """ - HTTP server adapter module. + HTTP server adapter behaviour. ## Usage defmodule MyApp.MyHTTPServer do - @behaviour TestServer.HTTPServer + @behaviour TestServer.HTTP.Server - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def start(instance, port, scheme, tls_options, server_options) do my_http_server_options = server_options @@ -20,10 +20,10 @@ defmodule TestServer.HTTPServer do end end - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def stop(instance, server_options), do: MyHTTPServer.stop() - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def get_socket_pid(%{adapter: {_, data}}), do: data.pid # or however your adapter provides the pid end """ @@ -125,14 +125,14 @@ defmodule TestServer.HTTPServer do defp default_http_server do cond do - Code.ensure_loaded?(TestServer.HTTPServer.Bandit) -> - {TestServer.HTTPServer.Bandit, []} + Code.ensure_loaded?(TestServer.HTTP.Server.Bandit) -> + {TestServer.HTTP.Server.Bandit, []} - Code.ensure_loaded?(TestServer.HTTPServer.Plug.Cowboy) -> - {TestServer.HTTPServer.Plug.Cowboy, []} + Code.ensure_loaded?(TestServer.HTTP.Server.Plug.Cowboy) -> + {TestServer.HTTP.Server.Plug.Cowboy, []} true -> - {TestServer.HTTPServer.Httpd, []} + {TestServer.HTTP.Server.Httpd, []} end end diff --git a/lib/test_server/http_server/bandit.ex b/lib/test_server/http/server/bandit.ex similarity index 81% rename from lib/test_server/http_server/bandit.ex rename to lib/test_server/http/server/bandit.ex index 09c5f18..be6a1a5 100644 --- a/lib/test_server/http_server/bandit.ex +++ b/lib/test_server/http/server/bandit.ex @@ -1,5 +1,5 @@ if Code.ensure_loaded?(Bandit) do - defmodule TestServer.HTTPServer.Bandit do + defmodule TestServer.HTTP.Server.Bandit do @moduledoc """ HTTP server adapter using `Bandit`. @@ -9,16 +9,16 @@ if Code.ensure_loaded?(Bandit) do ## Usage - TestServer.start( - http_server: {TestServer.HTTPServer.Bandit, bandit_options} + TestServer.HTTP.start( + http_server: {TestServer.HTTP.Server.Bandit, bandit_options} ) """ - @behaviour TestServer.HTTPServer + @behaviour TestServer.HTTP.Server @behaviour WebSock - alias TestServer.WebSocket + alias TestServer.HTTP.WebSocket - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def start(instance, port, scheme, options, bandit_options) do transport_options = bandit_options @@ -43,7 +43,7 @@ if Code.ensure_loaded?(Bandit) do bandit_options = bandit_options |> Keyword.put(:thousand_island_options, thousand_islands_options) - |> Keyword.put(:plug, {TestServer.HTTPServer.Bandit.Plug, {__MODULE__, [], instance}}) + |> Keyword.put(:plug, {TestServer.HTTP.Server.Bandit.Plug, {__MODULE__, [], instance}}) |> Keyword.put(:scheme, scheme) |> Keyword.put_new(:startup_log, false) @@ -59,13 +59,13 @@ if Code.ensure_loaded?(Bandit) do Keyword.merge(transport_options, Keyword.put_new(tls_options, :log_level, :warning)) end - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def stop(server_pid, _bandit_options) do ThousandIsland.stop(server_pid) end - @impl TestServer.HTTPServer - def get_socket_pid(%{adapter: {TestServer.HTTPServer.Bandit.Adapter, {_plug_pid, req}}}) do + @impl TestServer.HTTP.Server + def get_socket_pid(%{adapter: {TestServer.HTTP.Server.Bandit.Adapter, {_plug_pid, req}}}) do req.owner_pid end diff --git a/lib/test_server/http_server/bandit/adapter.ex b/lib/test_server/http/server/bandit/adapter.ex similarity index 98% rename from lib/test_server/http_server/bandit/adapter.ex rename to lib/test_server/http/server/bandit/adapter.ex index 9fcbccb..6231055 100644 --- a/lib/test_server/http_server/bandit/adapter.ex +++ b/lib/test_server/http/server/bandit/adapter.ex @@ -10,7 +10,7 @@ # https://github.com/mtrudel/bandit/issues/215 # https://github.com/mtrudel/bandit/issues/101 if Code.ensure_loaded?(Bandit) do - defmodule TestServer.HTTPServer.Bandit.Adapter do + defmodule TestServer.HTTP.Server.Bandit.Adapter do @moduledoc false @behaviour Plug.Conn.Adapter diff --git a/lib/test_server/http_server/bandit/plug.ex b/lib/test_server/http/server/bandit/plug.ex similarity index 67% rename from lib/test_server/http_server/bandit/plug.ex rename to lib/test_server/http/server/bandit/plug.ex index ab69a7a..bf90532 100644 --- a/lib/test_server/http_server/bandit/plug.ex +++ b/lib/test_server/http/server/bandit/plug.ex @@ -1,12 +1,12 @@ -# See TestServer.HTTPServer.Bandit.HTTP2Adapter for why this is required. -defmodule TestServer.HTTPServer.Bandit.Plug do +# See TestServer.HTTP.Server.Bandit.Adapter for why this is required. +defmodule TestServer.HTTP.Server.Bandit.Plug do @moduledoc false - defdelegate init(opts), to: TestServer.Plug + defdelegate init(opts), to: TestServer.HTTP.Plug def call(%{adapter: {Bandit.Adapter, req}} = conn, {http_server, args, instance}) do plug_pid = self() - conn = %{conn | adapter: {TestServer.HTTPServer.Bandit.Adapter, {plug_pid, req}}} + conn = %{conn | adapter: {TestServer.HTTP.Server.Bandit.Adapter, {plug_pid, req}}} loop( Task.async(fn -> @@ -21,7 +21,7 @@ defmodule TestServer.HTTPServer.Bandit.Plug do end def call(conn, {http_server, args, instance}) do - TestServer.Plug.call(conn, {http_server, args, instance}) + TestServer.HTTP.Plug.call(conn, {http_server, args, instance}) end defp loop(task) do diff --git a/lib/test_server/http_server/httpd.ex b/lib/test_server/http/server/httpd.ex similarity index 93% rename from lib/test_server/http_server/httpd.ex rename to lib/test_server/http/server/httpd.ex index 8c2e3cc..7353333 100644 --- a/lib/test_server/http_server/httpd.ex +++ b/lib/test_server/http/server/httpd.ex @@ -1,5 +1,5 @@ if Code.ensure_loaded?(:httpd) do - defmodule TestServer.HTTPServer.Httpd do + defmodule TestServer.HTTP.Server.Httpd do @moduledoc """ HTTP server adapter using `:httpd`. @@ -8,13 +8,13 @@ if Code.ensure_loaded?(:httpd) do ## Usage - TestServer.start( - http_server: {TestServer.HTTPServer.Httpd, httpd_options} + TestServer.HTTP.start( + http_server: {TestServer.HTTP.Server.Httpd, httpd_options} ) """ - @behaviour TestServer.HTTPServer + @behaviour TestServer.HTTP.Server - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def start(instance, port, scheme, options, httpd_options) do httpd_options = httpd_options @@ -23,7 +23,7 @@ if Code.ensure_loaded?(:httpd) do |> Keyword.put_new(:server_name, ~c"Httpd Test Server") |> Keyword.put_new(:document_root, ~c"/tmp") |> Keyword.put_new(:server_root, ~c"/tmp") - |> Keyword.put_new(:handler_plug, {TestServer.Plug, {__MODULE__, [], instance}}) + |> Keyword.put_new(:handler_plug, {TestServer.HTTP.Plug, {__MODULE__, [], instance}}) |> Keyword.put_new(:ipfamily, options[:ipfamily]) |> put_tls_options(scheme, options[:tls]) @@ -41,12 +41,12 @@ if Code.ensure_loaded?(:httpd) do Keyword.put(httpd_options, :socket_type, {:ssl, tls_options}) end - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def stop(pid, _httpd_options) do :inets.stop(:httpd, pid) end - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def get_socket_pid(%{adapter: {_, _data}}), do: self() # :httpd record handler diff --git a/lib/test_server/http_server/plug_cowboy.ex b/lib/test_server/http/server/plug_cowboy.ex similarity index 83% rename from lib/test_server/http_server/plug_cowboy.ex rename to lib/test_server/http/server/plug_cowboy.ex index 6fd353d..0e52d90 100644 --- a/lib/test_server/http_server/plug_cowboy.ex +++ b/lib/test_server/http/server/plug_cowboy.ex @@ -1,5 +1,5 @@ if Code.ensure_loaded?(Plug.Cowboy) do - defmodule TestServer.HTTPServer.Plug.Cowboy do + defmodule TestServer.HTTP.Server.Plug.Cowboy do @moduledoc """ HTTP server adapter using `Plug.Cowboy`. @@ -9,25 +9,25 @@ if Code.ensure_loaded?(Plug.Cowboy) do ## Usage - TestServer.start( - http_server: {TestServer.HTTPServer.Plug.Cowboy, cowboy_options} + TestServer.HTTP.start( + http_server: {TestServer.HTTP.Server.Plug.Cowboy, cowboy_options} ) """ # Server - @behaviour TestServer.HTTPServer + @behaviour TestServer.HTTP.Server @behaviour :cowboy_websocket alias Plug.{Cowboy, Cowboy.Handler} - alias TestServer.WebSocket + alias TestServer.HTTP.WebSocket @default_protocol_options [ idle_timeout: :timer.seconds(1), request_timeout: :timer.seconds(1) ] - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def start(instance, port, scheme, options, cowboy_options) do cowboy_options = cowboy_options @@ -41,14 +41,18 @@ if Code.ensure_loaded?(Plug.Cowboy) do |> Keyword.put(:net, options[:ipfamily]) |> put_tls_options(scheme, options[:tls]) - case apply(Cowboy, scheme, [TestServer.Plug, {__MODULE__, %{}, instance}, cowboy_options]) do + case apply(Cowboy, scheme, [ + TestServer.HTTP.Plug, + {__MODULE__, %{}, instance}, + cowboy_options + ]) do {:ok, pid} -> {:ok, pid, cowboy_options} {:error, error} -> {:error, error} end end defp dispatch(instance) do - dispatches = [{:_, __MODULE__, {TestServer.Plug, {__MODULE__, %{}, instance}}}] + dispatches = [{:_, __MODULE__, {TestServer.HTTP.Plug, {__MODULE__, %{}, instance}}}] [{:_, dispatches}] end @@ -59,7 +63,7 @@ if Code.ensure_loaded?(Plug.Cowboy) do Keyword.merge(cowboy_options, Keyword.put_new(tls_options, :log_level, :warning)) end - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def stop(_pid, cowboy_options) do port = Keyword.fetch!(cowboy_options, :port) @@ -70,7 +74,7 @@ if Code.ensure_loaded?(Plug.Cowboy) do {__MODULE__, port} end - @impl TestServer.HTTPServer + @impl TestServer.HTTP.Server def get_socket_pid(%{adapter: {_, req}}), do: req.pid # Dispatch handling diff --git a/lib/test_server/websocket.ex b/lib/test_server/http/websocket.ex similarity index 92% rename from lib/test_server/websocket.ex rename to lib/test_server/http/websocket.ex index ecdc0a3..e57d4fd 100644 --- a/lib/test_server/websocket.ex +++ b/lib/test_server/http/websocket.ex @@ -1,7 +1,7 @@ -defmodule TestServer.WebSocket do +defmodule TestServer.HTTP.WebSocket do @moduledoc false - alias TestServer.Instance + alias TestServer.HTTP.Instance def handle_frame(frame, {{instance, _route_ref} = socket, state}) do case Instance.dispatch(socket, {:websocket, {:handle, frame}, state}) do @@ -10,7 +10,7 @@ defmodule TestServer.WebSocket do {:error, :not_found} -> message = - "#{Instance.format_instance(instance)} received an unexpected WebSocket frame" + "#{TestServer.format_instance(TestServer.HTTP, instance)} received an unexpected WebSocket frame" |> append_formatted_frame(frame) |> append_formatted_websocket_handlers(socket) diff --git a/lib/test_server/instance_manager.ex b/lib/test_server/instance_manager.ex index a2b07ef..9d9bd7e 100644 --- a/lib/test_server/instance_manager.ex +++ b/lib/test_server/instance_manager.ex @@ -3,38 +3,69 @@ defmodule TestServer.InstanceManager do use GenServer - alias TestServer.{Instance, InstanceSupervisor} + alias TestServer.InstanceSupervisor def start_link(options) do GenServer.start_link(__MODULE__, options, name: __MODULE__) end - def start_instance(caller, stacktrace, options) do + @spec start_instance(pid(), module(), keyword()) :: {:ok, pid()} | {:error, term()} + def start_instance(caller, protocol_module, options) do + [_first | stacktrace] = TestServer.get_pruned_stacktrace(protocol_module) + + instance_module = Module.concat(protocol_module, Instance) + options = options |> Keyword.put_new(:caller, caller) |> Keyword.put_new(:stacktrace, stacktrace) - caller = Keyword.fetch!(options, :caller) + case DynamicSupervisor.start_child(InstanceSupervisor, instance_module.child_spec(options)) do + {:ok, instance} -> + GenServer.call(__MODULE__, {:register, {caller, protocol_module, instance}}) - case DynamicSupervisor.start_child(InstanceSupervisor, Instance.child_spec(options)) do - {:ok, instance} -> GenServer.call(__MODULE__, {:register, {caller, instance}}) - {:error, error} -> {:error, error} + {:error, error} -> + {:error, error} end end @spec stop_instance(pid()) :: :ok | {:error, :not_found} def stop_instance(instance) do - :ok = TestServer.HTTPServer.stop(Instance.get_options(instance)) res = DynamicSupervisor.terminate_child(InstanceSupervisor, instance) GenServer.call(__MODULE__, {:remove, instance}) res end - @spec get_by_caller(pid()) :: nil | [pid()] - def get_by_caller(caller) do - GenServer.call(__MODULE__, {:get_by_caller, caller}) + @spec fetch_instance(pid(), module()) :: {:ok, pid()} | :error + def fetch_instance(caller, protocol_module) do + case GenServer.call(__MODULE__, {:get_by_caller, caller, protocol_module}) do + [] -> :error + [instance] -> {:ok, instance} + instances -> raise_multiple_instances_error(instances, protocol_module) + end + end + + defp raise_multiple_instances_error(instances, protocol_module) do + [{m, f, a, _} | _] = TestServer.get_pruned_stacktrace(protocol_module) + + formatted_instances = + instances + |> Enum.with_index() + |> Enum.map_join("\n\n", fn {instance, index} -> + options = GenServer.call(instance, :options) + + """ + ##{index + 1}: #{inspect(instance)} + #{Enum.map_join(options[:stacktrace], "\n ", &Exception.format_stacktrace_entry/1)} + """ + end) + + raise """ + Multiple instances running, please pass instance to `#{inspect(m)}.#{f}/#{a}`. + + #{formatted_instances} + """ end @impl true @@ -43,20 +74,20 @@ defmodule TestServer.InstanceManager do end @impl true - def handle_call({:register, {caller, instance}}, _from, state) do - state = %{state | instances: state.instances ++ [%{caller: caller, instance: instance}]} + def handle_call({:register, {caller, protocol_module, instance}}, _from, state) do + entry = %{caller: caller, instance: instance, protocol_module: protocol_module} + state = %{state | instances: state.instances ++ [entry]} {:reply, {:ok, instance}, state} end - def handle_call({:get_by_caller, caller}, _from, state) do - instance = - case Enum.filter(state.instances, &(&1.caller == caller)) do - [] -> nil - instances -> Enum.map(instances, & &1.instance) - end + def handle_call({:get_by_caller, caller, protocol_module}, _from, state) do + instances = + state.instances + |> Enum.filter(&(&1.caller == caller and &1.protocol_module == protocol_module)) + |> Enum.map(& &1.instance) - {:reply, instance, state} + {:reply, instances, state} end def handle_call({:alive?, instance}, _from, state) do diff --git a/mix.exs b/mix.exs index 8db77a7..54e7533 100644 --- a/mix.exs +++ b/mix.exs @@ -77,11 +77,17 @@ defmodule TestServer.MixProject do "CHANGELOG.md" ], groups_for_modules: [ - HTTPServer: [ - TestServer.HTTPServer, - TestServer.HTTPServer.Httpd, - TestServer.HTTPServer.Bandit, - TestServer.HTTPServer.Plug.Cowboy + HTTP: [ + TestServer.HTTP, + TestServer.HTTP.Instance, + TestServer.HTTP.Plug, + TestServer.HTTP.WebSocket + ], + "HTTP.Server": [ + TestServer.HTTP.Server, + TestServer.HTTP.Server.Httpd, + TestServer.HTTP.Server.Bandit, + TestServer.HTTP.Server.Plug.Cowboy ] ] ] diff --git a/test/http_server/bandit/http2_adapter_test.exs b/test/http_server/bandit/adapter_test.exs similarity index 71% rename from test/http_server/bandit/http2_adapter_test.exs rename to test/http_server/bandit/adapter_test.exs index a7cbdb3..7be8795 100644 --- a/test/http_server/bandit/http2_adapter_test.exs +++ b/test/http_server/bandit/adapter_test.exs @@ -1,30 +1,29 @@ -defmodule TestServer.HTTPServer.Bandit.HTTP2AdapterTest do +defmodule TestServer.HTTP.Server.Bandit.HTTP2AdapterTest do use ExUnit.Case - doctest TestServer setup do Application.ensure_all_started(:bandit) {:ok, _instance} = - TestServer.start(scheme: :https, http_server: {TestServer.HTTPServer.Bandit, []}) + TestServer.HTTP.start(scheme: :https, http_server: {TestServer.HTTP.Server.Bandit, []}) :ok end test "Plug.Conn.send_resp/3" do assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> Plug.Conn.send_resp(conn, 200, "test") end ) - assert {:ok, "test"} = http2_request(TestServer.url()) + assert {:ok, "test"} = http2_request(TestServer.HTTP.url()) end test "Plug.Conn.send_file/1" do assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> Plug.Conn.send_file(conn, 200, __ENV__.file) end @@ -32,12 +31,12 @@ defmodule TestServer.HTTPServer.Bandit.HTTP2AdapterTest do expected = File.read!(__ENV__.file) - assert {:ok, ^expected} = http2_request(TestServer.url()) + assert {:ok, ^expected} = http2_request(TestServer.HTTP.url()) end test "Plug.Conn.send_chunked/1 and Plug.Conn.chunk/1" do assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> conn = Plug.Conn.send_chunked(conn, 200) {:ok, conn} = Plug.Conn.chunk(conn, "Hello\n") @@ -47,12 +46,12 @@ defmodule TestServer.HTTPServer.Bandit.HTTP2AdapterTest do end ) - assert {:ok, "Hello\nWorld"} = http2_request(TestServer.url()) + assert {:ok, "Hello\nWorld"} = http2_request(TestServer.HTTP.url()) end test "Plug.Conn.get_peer_data/1" do assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> assert %{address: {127, 0, 0, 1}} = Plug.Conn.get_peer_data(conn) @@ -60,38 +59,38 @@ defmodule TestServer.HTTPServer.Bandit.HTTP2AdapterTest do end ) - assert {:ok, "OK"} = http2_request(TestServer.url()) + assert {:ok, "OK"} = http2_request(TestServer.HTTP.url()) end test "Plug.Conn.get_http_protocol/1" do assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> assert Plug.Conn.get_http_protocol(conn) == :"HTTP/2" Plug.Conn.send_resp(conn, 200, "OK") end ) - assert {:ok, "OK"} = http2_request(TestServer.url()) + assert {:ok, "OK"} = http2_request(TestServer.HTTP.url()) end test "Plug.Conn.read_body/1" do assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> assert {:ok, body, _data} = Plug.Conn.read_body(conn) Plug.Conn.resp(conn, 200, body) end ) - assert {:ok, "test"} = http2_request(TestServer.url(), method: :post, body: "test") + assert {:ok, "test"} = http2_request(TestServer.HTTP.url(), method: :post, body: "test") end defp http2_request(url, opts \\ []) do pools = %{ default: [ protocols: [:http2], - conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]] + conn_opts: [transport_opts: [cacerts: TestServer.HTTP.x509_suite().cacerts]] ] } diff --git a/test/test_helper.exs b/test/test_helper.exs index e47b5b1..e21830d 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -6,6 +6,6 @@ http_server = System.get_env("HTTP_SERVER", "Bandit") # `Header timestamp couldn't be fetched from ETS cache` warnings if http_server == "Bandit", do: Application.ensure_all_started(:bandit) -http_server = Module.concat(TestServer.HTTPServer, http_server) +http_server = Module.concat(TestServer.HTTP.Server, http_server) Application.put_env(:test_server, :http_server, {http_server, []}) IO.puts("Testing with #{inspect(http_server)}") diff --git a/test/test_server_test.exs b/test/test_server/http_test.exs similarity index 58% rename from test/test_server_test.exs rename to test/test_server/http_test.exs index 2eb80bc..20cc2b5 100644 --- a/test/test_server_test.exs +++ b/test/test_server/http_test.exs @@ -1,6 +1,6 @@ -defmodule TestServerTest do +defmodule TestServer.HTTPTest do use ExUnit.Case - doctest TestServer + doctest TestServer.HTTP import ExUnit.CaptureIO @@ -9,41 +9,41 @@ defmodule TestServerTest do describe "start/1" do test "with invalid port" do assert_raise RuntimeError, ~r/Invalid port, got: :invalid/, fn -> - TestServer.start(port: :invalid) + TestServer.HTTP.start(port: :invalid) end assert_raise RuntimeError, ~r/Invalid port, got: 65536/, fn -> - TestServer.start(port: 65_536) + TestServer.HTTP.start(port: 65_536) end assert_raise RuntimeError, ~r/Could not listen to port 4444, because: :eaddrinuse/, fn -> - TestServer.start(port: 4444) - TestServer.start(port: 4444) + TestServer.HTTP.start(port: 4444) + TestServer.HTTP.start(port: 4444) end end test "with invalid scheme" do assert_raise RuntimeError, ~r/Invalid scheme, got: :invalid/, fn -> - TestServer.start(scheme: :invalid) + TestServer.HTTP.start(scheme: :invalid) end end test "starts with multiple ports" do - {:ok, instance_1} = TestServer.start() - {:ok, instance_2} = TestServer.start() + {:ok, instance_1} = TestServer.HTTP.start() + {:ok, instance_2} = TestServer.HTTP.start() refute instance_1 == instance_2 - options_1 = TestServer.Instance.get_options(instance_1) - options_2 = TestServer.Instance.get_options(instance_2) + options_1 = TestServer.HTTP.Instance.get_options(instance_1) + options_2 = TestServer.HTTP.Instance.get_options(instance_2) refute options_1[:port] == options_2[:port] end test "starts with self-signed SSL" do - {:ok, instance} = TestServer.start(scheme: :https) + {:ok, instance} = TestServer.HTTP.start(scheme: :https) - options = TestServer.Instance.get_options(instance) + options = TestServer.HTTP.Instance.get_options(instance) assert %X509.Test.Suite{} = options[:x509_suite] @@ -62,24 +62,26 @@ defmodule TestServerTest do ] end - valid_cacerts = TestServer.x509_suite().cacerts + valid_cacerts = TestServer.HTTP.x509_suite().cacerts invalid_cacerts = X509.Test.Suite.new().cacerts assert {:error, {:failed_connect, _}} = - http1_request(TestServer.url("/"), http_opts: http_opts.(invalid_cacerts)) + http1_request(TestServer.HTTP.url("/"), http_opts: http_opts.(invalid_cacerts)) - assert :ok = TestServer.add("/") - assert {:ok, _} = http1_request(TestServer.url("/"), http_opts: http_opts.(valid_cacerts)) + assert :ok = TestServer.HTTP.add("/") + + assert {:ok, _} = + http1_request(TestServer.HTTP.url("/"), http_opts: http_opts.(valid_cacerts)) end test "starts in IPv6-only mode`" do - {:ok, instance} = TestServer.start(ipfamily: :inet6) - options = TestServer.Instance.get_options(instance) + {:ok, instance} = TestServer.HTTP.start(ipfamily: :inet6) + options = TestServer.HTTP.Instance.get_options(instance) assert options[:ipfamily] == :inet6 assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} @@ -87,118 +89,101 @@ defmodule TestServerTest do end ) - assert %{host: hostname} = URI.parse(TestServer.url("/")) + assert %{host: hostname} = URI.parse(TestServer.HTTP.url("/")) assert {:ok, {0, 0, 0, 0, 0, 0, 0, 1}} == :inet.getaddr(String.to_charlist(hostname), :inet6) - assert {:ok, _} = http1_request(TestServer.url("/")) + assert {:ok, _} = http1_request(TestServer.HTTP.url("/")) end test "with invalid http server" do assert_raise RuntimeError, ~r/Invalid http_server, got: :invalid/, fn -> - TestServer.start(http_server: :invalid) + TestServer.HTTP.start(http_server: :invalid) end end end describe "stop/1" do test "when not running" do - assert_raise RuntimeError, "No current TestServer.Instance running", fn -> - TestServer.stop() + assert_raise RuntimeError, "No current TestServer.HTTP.Instance running", fn -> + TestServer.HTTP.stop() end - assert_raise RuntimeError, ~r/TestServer.Instance \#PID\<[0-9.]+\> is not running/, fn -> - {:ok, instance} = TestServer.start() + assert_raise RuntimeError, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, + fn -> + {:ok, instance} = TestServer.HTTP.start() - assert :ok = TestServer.stop() + assert :ok = TestServer.HTTP.stop() - TestServer.stop(instance) - end + TestServer.HTTP.stop(instance) + end end test "stops" do - assert {:ok, pid} = TestServer.start() - url = TestServer.url("/") + assert {:ok, pid} = TestServer.HTTP.start() + url = TestServer.HTTP.url("/") - assert :ok = TestServer.stop() + assert :ok = TestServer.HTTP.stop() refute Process.alive?(pid) assert {:error, {:failed_connect, _}} = http1_request(url) end test "with multiple instances" do - {:ok, instance_1} = TestServer.start() - {:ok, _instance_2} = TestServer.start() + {:ok, instance_1} = TestServer.HTTP.start() + {:ok, _instance_2} = TestServer.HTTP.start() assert_raise RuntimeError, - ~r/Multiple TestServer\.Instance's running, please pass instance to `TestServer\.stop\/0`/, + ~r/Multiple instances running, please pass instance to `TestServer\.HTTP\.stop\/0`/, fn -> - TestServer.stop() + TestServer.HTTP.stop() end - assert :ok = TestServer.stop(instance_1) - assert :ok = TestServer.stop() - end - end - - describe "get_instance/0" do - test "when not running" do - refute TestServer.get_instance() - end - - test "with multiple instances" do - {:ok, _instance_1} = TestServer.start() - {:ok, _instance_2} = TestServer.start() - - assert_raise RuntimeError, ~r/Multiple TestServer\.Instance's running./, fn -> - TestServer.get_instance() - end - end - - test "with instance" do - {:ok, instance} = TestServer.start() - - assert TestServer.get_instance() == instance + assert :ok = TestServer.HTTP.stop(instance_1) + assert :ok = TestServer.HTTP.stop() end end describe "url/3" do test "when instance not running" do - assert_raise RuntimeError, "No current TestServer.Instance running", fn -> - TestServer.url() + assert_raise RuntimeError, "No current TestServer.HTTP.Instance running", fn -> + TestServer.HTTP.url() end - assert_raise RuntimeError, ~r/TestServer.Instance \#PID\<[0-9.]+\> is not running/, fn -> - {:ok, instance} = TestServer.start() + assert_raise RuntimeError, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, + fn -> + {:ok, instance} = TestServer.HTTP.start() - assert :ok = TestServer.stop() + assert :ok = TestServer.HTTP.stop() - TestServer.url(instance) - end + TestServer.HTTP.url(instance) + end end test "with invalid `:host`" do - TestServer.start() + TestServer.HTTP.start() assert_raise RuntimeError, ~r/Invalid host, got: :invalid/, fn -> - TestServer.url("/", host: :invalid) + TestServer.HTTP.url("/", host: :invalid) end end test "produces routes" do - TestServer.start() + TestServer.HTTP.start() - assert TestServer.url("/") =~ ~r/^http\:\/\/localhost\:[0-9]+\/$/ - refute TestServer.url("/") == TestServer.url("/path") - refute TestServer.url("/") == TestServer.url("/", host: "bad-host") + assert TestServer.HTTP.url("/") =~ ~r/^http\:\/\/localhost\:[0-9]+\/$/ + refute TestServer.HTTP.url("/") == TestServer.HTTP.url("/path") + refute TestServer.HTTP.url("/") == TestServer.HTTP.url("/", host: "bad-host") end test "with `:host`" do - TestServer.start() + TestServer.HTTP.start() assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> assert conn.remote_ip == {127, 0, 0, 1} assert conn.host == "custom-host" @@ -207,14 +192,14 @@ defmodule TestServerTest do end ) - assert {:ok, _} = http1_request(TestServer.url("/", host: "custom-host")) + assert {:ok, _} = http1_request(TestServer.HTTP.url("/", host: "custom-host")) end test "with `:host` in IPv6-only mode" do - TestServer.start(ipfamily: :inet6, http_server: {TestServer.HTTPServer.Httpd, []}) + TestServer.HTTP.start(ipfamily: :inet6, http_server: {TestServer.HTTP.Server.Httpd, []}) assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1} assert conn.host == "custom-host" @@ -223,57 +208,59 @@ defmodule TestServerTest do end ) - assert {:ok, _} = http1_request(TestServer.url("/", host: "custom-host")) + assert {:ok, _} = http1_request(TestServer.HTTP.url("/", host: "custom-host")) end test "with multiple instances" do - {:ok, instance_1} = TestServer.start() - {:ok, instance_2} = TestServer.start() + {:ok, instance_1} = TestServer.HTTP.start() + {:ok, instance_2} = TestServer.HTTP.start() assert_raise RuntimeError, - ~r/Multiple TestServer\.Instance's running, please pass instance to `TestServer\.url\/2`/, + ~r/Multiple instances running, please pass instance to `TestServer\.HTTP\.url\/2`/, fn -> - TestServer.url() + TestServer.HTTP.url() end - refute TestServer.url(instance_1) == TestServer.url(instance_2) + refute TestServer.HTTP.url(instance_1) == TestServer.HTTP.url(instance_2) end end describe "add/3" do test "when instance not running" do - assert_raise RuntimeError, ~r/TestServer.Instance \#PID\<[0-9.]+\> is not running/, fn -> - {:ok, instance} = TestServer.start() + assert_raise RuntimeError, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, + fn -> + {:ok, instance} = TestServer.HTTP.start() - assert :ok = TestServer.stop() + assert :ok = TestServer.HTTP.stop() - TestServer.add(instance, "/") - end + TestServer.HTTP.add(instance, "/") + end end test "invalid options" do assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.add("/", match: :invalid) + TestServer.HTTP.add("/", match: :invalid) end assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.add("/", to: :invalid) + TestServer.HTTP.add("/", to: :invalid) end end test "with multiple instances" do - {:ok, instance_1} = TestServer.start() - {:ok, _instance_2} = TestServer.start() + {:ok, instance_1} = TestServer.HTTP.start() + {:ok, _instance_2} = TestServer.HTTP.start() assert_raise RuntimeError, - ~r/Multiple TestServer\.Instance's running, please pass instance to `TestServer\.add\/2`/, + ~r/Multiple instances running, please pass instance to `TestServer\.HTTP\.add\/2`/, fn -> - TestServer.add("/") + TestServer.HTTP.add("/") end - assert :ok = TestServer.add(instance_1, "/") + assert :ok = TestServer.HTTP.add(instance_1, "/") - TestServer.stop(instance_1) + TestServer.HTTP.stop(instance_1) end test "with mismatching URI" do @@ -281,9 +268,9 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) - assert :ok = TestServer.add("/") - assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.url("/path")) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) + assert :ok = TestServer.HTTP.add("/") + assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/path")) end end @@ -295,10 +282,10 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.add("/", via: :post) - assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.url("/")) + assert :ok = TestServer.HTTP.add("/", via: :post) + assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end end @@ -310,11 +297,11 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) - assert :ok = TestServer.add("/") + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) + assert :ok = TestServer.HTTP.add("/") - assert {:ok, _} = unquote(__MODULE__).http1_request(TestServer.url("/")) - assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.url("/?a=1")) + assert {:ok, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) + assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/?a=1")) end end @@ -328,7 +315,7 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - assert :ok = TestServer.add("/") + assert :ok = TestServer.HTTP.add("/") end end @@ -343,8 +330,8 @@ defmodule TestServerTest do def call(conn, _opts), do: Plug.Conn.resp(conn, 200, to_string(__MODULE__)) end - assert :ok = TestServer.add("/", to: ToPlug) - assert http1_request(TestServer.url("/")) == {:ok, to_string(ToPlug)} + assert :ok = TestServer.HTTP.add("/", to: ToPlug) + assert http1_request(TestServer.HTTP.url("/")) == {:ok, to_string(ToPlug)} end test "with callback function raising exception" do @@ -352,16 +339,16 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.add("/", to: fn _conn -> raise "boom" end) - assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.url("/")) + assert :ok = TestServer.HTTP.add("/", to: fn _conn -> raise "boom" end) + assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end end assert io = capture_io(fn -> ExUnit.run() end) assert io =~ "(RuntimeError) boom" - assert io =~ "anonymous fn/1 in TestServerTest.ToFunctionRaiseTest" + assert io =~ "anonymous fn/1 in TestServer.HTTPTest.ToFunctionRaiseTest" end test "with callback function halts" do @@ -369,10 +356,10 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.add("/", to: fn conn -> Plug.Conn.halt(conn) end) - assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.url("/")) + assert :ok = TestServer.HTTP.add("/", to: fn conn -> Plug.Conn.halt(conn) end) + assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end end @@ -382,46 +369,46 @@ defmodule TestServerTest do test "with callback function" do assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> Plug.Conn.resp(conn, 200, "function called") end ) - assert http1_request(TestServer.url("/")) == {:ok, "function called"} + assert http1_request(TestServer.HTTP.url("/")) == {:ok, "function called"} end test "with match function" do assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", match: fn %{params: %{"a" => "1"}} = _conn -> true _conn -> false end ) - assert {:ok, _} = http1_request(TestServer.url("/ignore") <> "?a=1") + assert {:ok, _} = http1_request(TestServer.HTTP.url("/ignore") <> "?a=1") end test "with :via method" do - assert :ok = TestServer.add("/", via: :get) - assert :ok = TestServer.add("/", via: :post) - assert {:ok, _} = http1_request(TestServer.url("/")) - assert {:ok, _} = http1_request(TestServer.url("/"), method: :post) + assert :ok = TestServer.HTTP.add("/", via: :get) + assert :ok = TestServer.HTTP.add("/", via: :post) + assert {:ok, _} = http1_request(TestServer.HTTP.url("/")) + assert {:ok, _} = http1_request(TestServer.HTTP.url("/"), method: :post) end # `:httpd` has no HTTP/2 support unless System.get_env("HTTP_SERVER") == "Httpd" do test "with HTTP/2" do - {:ok, _instance} = TestServer.start(scheme: :https) + {:ok, _instance} = TestServer.HTTP.start(scheme: :https) - assert :ok = TestServer.add("/") - assert {:ok, "HTTP/2"} = http2_request(TestServer.url()) + assert :ok = TestServer.HTTP.add("/") + assert {:ok, "HTTP/2"} = http2_request(TestServer.HTTP.url()) end test "with HTTP/2 with plug function" do - {:ok, _instance} = TestServer.start(scheme: :https) + {:ok, _instance} = TestServer.HTTP.start(scheme: :https) assert :ok = - TestServer.add("/", + TestServer.HTTP.add("/", to: fn conn -> assert Plug.Conn.get_http_protocol(conn) == :"HTTP/2" assert {:ok, body, _data} = Plug.Conn.read_body(conn) @@ -429,7 +416,7 @@ defmodule TestServerTest do end ) - assert {:ok, "test"} = http2_request(TestServer.url(), method: :post, body: "test") + assert {:ok, "test"} = http2_request(TestServer.HTTP.url(), method: :post, body: "test") end end end @@ -437,20 +424,21 @@ defmodule TestServerTest do describe "plug/2" do test "with invalid plug" do assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.plug(:invalid) + TestServer.HTTP.plug(:invalid) end end test "with plug function" do assert :ok = - TestServer.plug(fn conn -> + TestServer.HTTP.plug(fn conn -> assert {:ok, body, _data} = Plug.Conn.read_body(conn) %{conn | params: %{"plug" => "anonymous function", body: body}} end) - assert :ok = TestServer.add("/", to: &Plug.Conn.resp(&1, 200, URI.encode_query(&1.params))) + assert :ok = + TestServer.HTTP.add("/", to: &Plug.Conn.resp(&1, 200, URI.encode_query(&1.params))) - assert {:ok, query} = http1_request(TestServer.url("/")) + assert {:ok, query} = http1_request(TestServer.HTTP.url("/")) assert URI.decode_query(query) == %{"plug" => "anonymous function", "body" => ""} end @@ -461,9 +449,9 @@ defmodule TestServerTest do def call(conn, _opts), do: %{conn | params: %{"plug" => to_string(__MODULE__)}} end - assert :ok = TestServer.plug(ModulePlug) - assert :ok = TestServer.add("/", to: &Plug.Conn.resp(&1, 200, &1.params["plug"])) - assert http1_request(TestServer.url("/")) == {:ok, to_string(ModulePlug)} + assert :ok = TestServer.HTTP.plug(ModulePlug) + assert :ok = TestServer.HTTP.add("/", to: &Plug.Conn.resp(&1, 200, &1.params["plug"])) + assert http1_request(TestServer.HTTP.url("/")) == {:ok, to_string(ModulePlug)} end test "when plug errors" do @@ -471,16 +459,16 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.plug(fn _conn -> raise "boom" end) - assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.url("/")) + assert :ok = TestServer.HTTP.plug(fn _conn -> raise "boom" end) + assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end end assert io = capture_io(fn -> ExUnit.run() end) assert io =~ "(RuntimeError) boom" - assert io =~ "anonymous fn/1 in TestServerTest.PlugFunctionRaiseTest" + assert io =~ "anonymous fn/1 in TestServer.HTTPTest.PlugFunctionRaiseTest" end test "when plug function halts" do @@ -488,11 +476,11 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert :ok = TestServer.plug(fn conn -> Plug.Conn.halt(conn) end) - assert :ok = TestServer.add("/") - assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.url("/")) + assert :ok = TestServer.HTTP.plug(fn conn -> Plug.Conn.halt(conn) end) + assert :ok = TestServer.HTTP.add("/") + assert {:error, _} = unquote(__MODULE__).http1_request(TestServer.HTTP.url("/")) end end @@ -503,26 +491,28 @@ defmodule TestServerTest do describe "x509_suite/0" do test "when instance not running" do - assert_raise RuntimeError, "No current TestServer.Instance running", fn -> - TestServer.x509_suite() + assert_raise RuntimeError, "No current TestServer.HTTP.Instance running", fn -> + TestServer.HTTP.x509_suite() end - assert_raise RuntimeError, ~r/TestServer\.Instance \#PID\<[0-9.]+\> is not running/, fn -> - {:ok, instance} = TestServer.start() + assert_raise RuntimeError, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, + fn -> + {:ok, instance} = TestServer.HTTP.start() - assert :ok = TestServer.stop() + assert :ok = TestServer.HTTP.stop() - TestServer.x509_suite(instance) - end + TestServer.HTTP.x509_suite(instance) + end end test "when instance not running in http" do - TestServer.start() + TestServer.HTTP.start() assert_raise RuntimeError, - ~r/TestServer\.Instance \#PID\<[0-9.]+\> is not running with `\[scheme: :https\]` option/, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running with `\[scheme: :https\]` option/, fn -> - TestServer.x509_suite() + TestServer.HTTP.x509_suite() end end @@ -535,12 +525,12 @@ defmodule TestServerTest do cacerts: suite.chain ++ suite.cacerts ] - TestServer.start(scheme: :https, tls: tls_options) + TestServer.HTTP.start(scheme: :https, tls: tls_options) assert_raise RuntimeError, - ~r/TestServer\.Instance \#PID\<[0-9.]+\> is running with custom SSL/, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is running with custom SSL/, fn -> - TestServer.x509_suite() + TestServer.HTTP.x509_suite() end end end @@ -549,54 +539,56 @@ defmodule TestServerTest do unless System.get_env("HTTP_SERVER") == "Httpd" do describe "websocket_init/3" do test "when instance not running" do - {:ok, instance} = TestServer.start() - assert :ok = TestServer.stop() + {:ok, instance} = TestServer.HTTP.start() + assert :ok = TestServer.HTTP.stop() - assert_raise RuntimeError, ~r/TestServer\.Instance \#PID\<[0-9.]+\> is not running/, fn -> - TestServer.websocket_init(instance, "/ws") - end + assert_raise RuntimeError, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, + fn -> + TestServer.HTTP.websocket_init(instance, "/ws") + end end test "invalid options" do assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.websocket_init("/", to: :invalid) + TestServer.HTTP.websocket_init("/", to: :invalid) end assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.websocket_init("/", match: :invalid) + TestServer.HTTP.websocket_init("/", match: :invalid) end assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.websocket_init("/", match: :invalid) + TestServer.HTTP.websocket_init("/", match: :invalid) end end test "with multiple instances" do - {:ok, _instance_1} = TestServer.start() - {:ok, _instance_2} = TestServer.start() + {:ok, _instance_1} = TestServer.HTTP.start() + {:ok, _instance_2} = TestServer.HTTP.start() assert_raise RuntimeError, - ~r/Multiple TestServer\.Instance's running, please pass instance to `TestServer\.websocket_init\/2`/, + ~r/Multiple instances running, please pass instance to `TestServer\.HTTP\.websocket_init\/2`/, fn -> - TestServer.websocket_init("/ws") + TestServer.HTTP.websocket_init("/ws") end end test "with handshake callback function with set conn" do assert {:ok, _socket} = - TestServer.websocket_init("/ws", + TestServer.HTTP.websocket_init("/ws", to: fn conn -> Plug.Conn.resp(conn, 403, "Forbidden") end ) assert {:error, %WebSockex.RequestError{code: 403}} = - WebSocketClient.start_link(TestServer.url("/ws")) + WebSocketClient.start_link(TestServer.HTTP.url("/ws")) end test "with handshake callback function with unset conn" do assert {:ok, _socket} = - TestServer.websocket_init("/ws", + TestServer.HTTP.websocket_init("/ws", to: fn conn -> assert Plug.Conn.get_req_header(conn, "upgrade") == ["websocket"] @@ -604,33 +596,35 @@ defmodule TestServerTest do end ) - assert {:ok, _client} = WebSocketClient.start_link(TestServer.url("/ws")) + assert {:ok, _client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) end end describe "websocket_handle/3" do test "when instance not running" do - {:ok, instance} = TestServer.start() - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert :ok = TestServer.stop(instance) + {:ok, instance} = TestServer.HTTP.start() + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert :ok = TestServer.HTTP.stop(instance) - assert_raise RuntimeError, ~r/TestServer\.Instance \#PID\<[0-9.]+\> is not running/, fn -> - TestServer.websocket_handle(socket) - end + assert_raise RuntimeError, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, + fn -> + TestServer.HTTP.websocket_handle(socket) + end end test "invalid options" do - assert {:ok, socket} = TestServer.websocket_init("/ws") + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.websocket_handle(socket, to: :invalid) + TestServer.HTTP.websocket_handle(socket, to: :invalid) end assert_raise BadFunctionError, ~r/expected a function, got: :invalid/, fn -> - TestServer.websocket_handle(socket, match: :invalid) + TestServer.HTTP.websocket_handle(socket, match: :invalid) end - TestServer.stop() + TestServer.HTTP.stop() end test "with no message received" do @@ -638,9 +632,9 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, _client} = WebSocketClient.start_link(TestServer.url("/ws")) - assert :ok = TestServer.websocket_handle(socket) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, _client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) + assert :ok = TestServer.HTTP.websocket_handle(socket) end end @@ -653,10 +647,10 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert {:ok, _socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) + assert {:ok, _socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) assert {:ok, msg} = WebSocketClient.send_message(client, "ping") assert msg =~ "received an unexpected WebSocket frame" @@ -673,11 +667,11 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) - assert :ok = TestServer.websocket_handle(socket) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) + assert :ok = TestServer.HTTP.websocket_handle(socket) assert WebSocketClient.send_message(client, "ping") == {:ok, "ping"} @@ -697,12 +691,12 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) assert :ok = - TestServer.websocket_handle(socket, + TestServer.HTTP.websocket_handle(socket, to: fn _frame, _state -> raise "boom" end ) @@ -713,7 +707,7 @@ defmodule TestServerTest do assert io = capture_io(fn -> ExUnit.run() end) assert io =~ "(RuntimeError) boom" - assert io =~ "anonymous fn/2 in TestServerTest.WebSocketHandleToFunctionRaiseTest" + assert io =~ "anonymous fn/2 in TestServer.HTTPTest.WebSocketHandleToFunctionRaiseTest" end test "with callback function with invalid response" do @@ -721,13 +715,15 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) assert :ok = - TestServer.websocket_handle(socket, to: fn _frame, _state -> :invalid end) + TestServer.HTTP.websocket_handle(socket, + to: fn _frame, _state -> :invalid end + ) assert {:ok, msg} = WebSocketClient.send_message(client, "ping") assert msg =~ "(RuntimeError) Invalid callback response, got: :invalid." @@ -739,11 +735,11 @@ defmodule TestServerTest do end test "with callback function" do - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) assert :ok = - TestServer.websocket_handle(socket, + TestServer.HTTP.websocket_handle(socket, to: fn {:text, _any}, state -> {:reply, {:text, "function called"}, state} end ) @@ -751,11 +747,13 @@ defmodule TestServerTest do end test "with match function" do - assert {:ok, socket} = TestServer.websocket_init("/ws", init_state: %{custom: true}) - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) + assert {:ok, socket} = + TestServer.HTTP.websocket_init("/ws", init_state: %{custom: true}) + + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) assert :ok = - TestServer.websocket_handle(socket, + TestServer.HTTP.websocket_handle(socket, match: fn _frame, %{custom: true} -> true end @@ -767,13 +765,15 @@ defmodule TestServerTest do describe "websocket_info/2" do test "when instance not running" do - {:ok, instance} = TestServer.start() - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert :ok = TestServer.stop(instance) + {:ok, instance} = TestServer.HTTP.start() + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert :ok = TestServer.HTTP.stop(instance) - assert_raise RuntimeError, ~r/TestServer\.Instance \#PID\<[0-9.]+\> is not running/, fn -> - TestServer.websocket_info(socket) - end + assert_raise RuntimeError, + ~r/TestServer\.HTTP\.Instance \#PID\<[0-9.]+\> is not running/, + fn -> + TestServer.HTTP.websocket_info(socket) + end end test "with invalid callback response" do @@ -781,10 +781,10 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) - assert :ok = TestServer.websocket_info(socket, fn _state -> :invalid end) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) + 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, message} = WebSocketClient.receive_message(client) assert message =~ "(RuntimeError) Invalid callback response, got: :invalid." @@ -800,11 +800,12 @@ defmodule TestServerTest do use ExUnit.Case test "fails" do - {:ok, _instance} = TestServer.start(suppress_warning: true) - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) + {:ok, _instance} = TestServer.HTTP.start(suppress_warning: true) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) - assert :ok = TestServer.websocket_info(socket, fn _state -> raise "boom" end) + assert :ok = + TestServer.HTTP.websocket_info(socket, fn _state -> raise "boom" end) assert {:ok, message} = WebSocketClient.receive_message(client) assert message =~ "(RuntimeError) boom" @@ -813,15 +814,15 @@ defmodule TestServerTest do assert io = capture_io(fn -> ExUnit.run() end) assert io =~ "(RuntimeError) boom" - assert io =~ "anonymous fn/1 in TestServerTest.WebSocketInfoToFunctionRaiseTest" + assert io =~ "anonymous fn/1 in TestServer.HTTPTest.WebSocketInfoToFunctionRaiseTest" end test "with callback function" do - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) assert :ok = - TestServer.websocket_info(socket, fn state -> + TestServer.HTTP.websocket_info(socket, fn state -> {:reply, {:text, "pong"}, state} end) @@ -829,10 +830,10 @@ defmodule TestServerTest do end test "with default callback function" do - assert {:ok, socket} = TestServer.websocket_init("/ws") - assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws")) + assert {:ok, socket} = TestServer.HTTP.websocket_init("/ws") + assert {:ok, client} = WebSocketClient.start_link(TestServer.HTTP.url("/ws")) - assert :ok = TestServer.websocket_info(socket) + assert :ok = TestServer.HTTP.websocket_info(socket) assert {:ok, "ping"} = WebSocketClient.receive_message(client) end end @@ -897,7 +898,7 @@ defmodule TestServerTest do pools = %{ default: [ protocols: [:http2], - conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]] + conn_opts: [transport_opts: [cacerts: TestServer.HTTP.x509_suite().cacerts]] ] }