|
| 1 | +defmodule TestServer do |
| 2 | + @external_resource "README.md" |
| 3 | + @moduledoc "README.md" |
| 4 | + |> File.read!() |
| 5 | + |> String.split("<!-- MDOC !-->") |
| 6 | + |> Enum.fetch!(1) |
| 7 | + |
| 8 | + alias Plug.Conn |
| 9 | + alias TestServer.{Instance, InstanceManager} |
| 10 | + |
| 11 | + @doc """ |
| 12 | + Start a test server instance. |
| 13 | +
|
| 14 | + The instance will be terminated when the test case finishes. |
| 15 | +
|
| 16 | + ## Options |
| 17 | +
|
| 18 | + * `:port` - integer of port number, defaults to random port that can be opened; |
| 19 | + * `:scheme` - an atom for the http scheme. Defaults to `:http`; |
| 20 | + * `:cowboy_options` - See [Cowboy docs](https://ninenines.eu/docs/en/cowboy/2.5/manual/cowboy_http/) |
| 21 | + """ |
| 22 | + @spec start(keyword()) :: {:ok, pid()} |
| 23 | + def start(options \\ []) do |
| 24 | + case ExUnit.fetch_test_supervisor() do |
| 25 | + {:ok, sup} -> |
| 26 | + start_with_ex_unit(options, sup) |
| 27 | + |
| 28 | + :error -> |
| 29 | + raise ArgumentError, "can only be called in a test process" |
| 30 | + end |
| 31 | + end |
| 32 | + |
| 33 | + defp start_with_ex_unit(options, _sup) do |
| 34 | + case InstanceManager.start_instance(options) do |
| 35 | + {:ok, instance} -> |
| 36 | + ExUnit.Callbacks.on_exit(fn -> stop(instance) end) |
| 37 | + |
| 38 | + {:ok, instance} |
| 39 | + |
| 40 | + {:error, error} -> |
| 41 | + raise_start_failure({:error, error}) |
| 42 | + end |
| 43 | + end |
| 44 | + |
| 45 | + defp raise_start_failure({:error, {{:EXIT, reason}, _spec}}) do |
| 46 | + raise_start_failure({:error, reason}) |
| 47 | + end |
| 48 | + |
| 49 | + defp raise_start_failure({:error, error}) do |
| 50 | + raise """ |
| 51 | + EXIT when starting #{__MODULE__.Instance}: |
| 52 | +
|
| 53 | + #{Exception.format_exit(error)} |
| 54 | + """ |
| 55 | + end |
| 56 | + |
| 57 | + @doc """ |
| 58 | + Shuts down the current test server instance |
| 59 | + """ |
| 60 | + @spec stop() :: :ok | {:error, term()} |
| 61 | + def stop do |
| 62 | + case InstanceManager.get_by_caller(self()) do |
| 63 | + nil -> :ok |
| 64 | + instance -> stop(instance) |
| 65 | + end |
| 66 | + end |
| 67 | + |
| 68 | + @doc """ |
| 69 | + Shuts down a test server instance |
| 70 | + """ |
| 71 | + @spec stop(pid()) :: :ok | {:error, term()} |
| 72 | + def stop(instance) do |
| 73 | + case Process.alive?(instance) do |
| 74 | + true -> |
| 75 | + verify_routes!(instance) |
| 76 | + InstanceManager.stop_instance(instance) |
| 77 | + |
| 78 | + false -> |
| 79 | + :ok |
| 80 | + end |
| 81 | + end |
| 82 | + |
| 83 | + defp verify_routes!(instance) do |
| 84 | + case Instance.active_routes(instance) do |
| 85 | + [] -> |
| 86 | + :ok |
| 87 | + |
| 88 | + routes -> |
| 89 | + raise """ |
| 90 | + The test ended before the following #{inspect(__MODULE__)} route(s) received a request: |
| 91 | +
|
| 92 | + #{Instance.routes_info(routes)} |
| 93 | + """ |
| 94 | + end |
| 95 | + end |
| 96 | + |
| 97 | + @spec url() :: binary() |
| 98 | + def url, do: url("") |
| 99 | + |
| 100 | + @spec url(binary() | keyword()) :: binary() |
| 101 | + def url(uri) when is_binary(uri), do: url(uri, []) |
| 102 | + def url(opts) when is_list(opts), do: url("", opts) |
| 103 | + |
| 104 | + @doc """ |
| 105 | + Produces a URL for the test server instance. |
| 106 | +
|
| 107 | + ## Options |
| 108 | + * `:host` - binary host value, it'll be added to inet for IP 127.0.0.1, defaults to `"localhost"`; |
| 109 | + """ |
| 110 | + @spec url(binary(), keyword()) :: binary() |
| 111 | + def url(uri, opts) do |
| 112 | + unless is_nil(opts[:host]) or is_binary(opts[:host]), |
| 113 | + do: raise("Invalid host, got: #{inspect(opts[:host])}") |
| 114 | + |
| 115 | + {:ok, instance} = autostart() |
| 116 | + |
| 117 | + domain = maybe_enable_host(opts[:host]) |
| 118 | + options = Instance.get_options(instance) |
| 119 | + |
| 120 | + "#{Keyword.fetch!(options, :scheme)}://#{domain}:#{Keyword.fetch!(options, :port)}#{uri}" |
| 121 | + end |
| 122 | + |
| 123 | + defp autostart do |
| 124 | + case InstanceManager.get_by_caller(self()) do |
| 125 | + nil -> start([]) |
| 126 | + instance -> {:ok, instance} |
| 127 | + end |
| 128 | + end |
| 129 | + |
| 130 | + defp maybe_enable_host(nil), do: "localhost" |
| 131 | + |
| 132 | + defp maybe_enable_host(host) do |
| 133 | + :inet_db.set_lookup([:file, :dns]) |
| 134 | + :inet_db.add_host({127, 0, 0, 1}, [String.to_charlist(host)]) |
| 135 | + |
| 136 | + host |
| 137 | + end |
| 138 | + |
| 139 | + @doc """ |
| 140 | + Adds a route to the test server. |
| 141 | +
|
| 142 | + ## Options |
| 143 | +
|
| 144 | + * `:via` - matches the route against some specific HTTP method(s) specified as an atom, like `:get` or `:put`, or a list, like `[:get, :post]`. |
| 145 | + * `:match` - an anonymous function that will be called to see if a route matches, defaults to matching with arguments of uri and `:via` option. |
| 146 | + * `:to` - a Plug or anonymous function that will be called when the route matches. |
| 147 | + """ |
| 148 | + @spec add(binary(), keyword()) :: :ok | {:error, term()} |
| 149 | + def add(uri, options \\ []) when is_binary(uri) and is_list(options) do |
| 150 | + {:ok, instance} = autostart() |
| 151 | + |
| 152 | + {:current_stacktrace, [_process, _test_server | stacktrace]} = |
| 153 | + Process.info(self(), :current_stacktrace) |
| 154 | + |
| 155 | + options = Keyword.put_new(options, :to, &default_response_handler/1) |
| 156 | + |
| 157 | + Instance.register(instance, {uri, options, stacktrace}) |
| 158 | + end |
| 159 | + |
| 160 | + defp default_response_handler(conn) do |
| 161 | + Conn.send_resp(conn, 200, to_string(Conn.get_http_protocol(conn))) |
| 162 | + end |
| 163 | + |
| 164 | + @doc """ |
| 165 | + Fetches the generated x509 suite for the current test server instance. |
| 166 | + """ |
| 167 | + @spec x509_suite() :: term() |
| 168 | + def x509_suite do |
| 169 | + case InstanceManager.get_by_caller(self()) do |
| 170 | + nil -> raise "#{inspect(Instance)} is not running, did you start it?" |
| 171 | + instance -> x509_suite(instance) |
| 172 | + end |
| 173 | + end |
| 174 | + |
| 175 | + @doc """ |
| 176 | + Fetches the generated x509 suite for a test server instance. |
| 177 | + """ |
| 178 | + @spec x509_suite(pid()) :: term() |
| 179 | + def x509_suite(instance) do |
| 180 | + options = Instance.get_options(instance) |
| 181 | + |
| 182 | + cond do |
| 183 | + not (options[:scheme] == :https) -> |
| 184 | + raise "The #{inspect(Instance)} is not running with `[scheme: :https]` option" |
| 185 | + |
| 186 | + not Keyword.has_key?(options, :x509_suite) -> |
| 187 | + raise "The #{inspect(Instance)} is running with custom SSL" |
| 188 | + |
| 189 | + true -> |
| 190 | + options[:x509_suite] |
| 191 | + end |
| 192 | + end |
| 193 | +end |
0 commit comments