# `TestServer`
[🔗](https://github.com/danschultzer/test_server/blob/v0.1.22/lib/test_server.ex#L1)

No fuzz ExUnit test server to mock third party services.

Features:

- HTTP/1
- HTTP/2
- WebSocket
- Built-in TLS with self-signed certificates
- Plug route matching

## Usage

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
          )
```

# `instance`

```elixir
@type instance() :: pid()
```

# `route`

```elixir
@type route() :: reference()
```

# `stacktrace`

```elixir
@type stacktrace() :: list()
```

# `websocket_frame`

```elixir
@type websocket_frame() :: {atom(), any()}
```

# `websocket_reply`

```elixir
@type websocket_reply() ::
  {:reply, websocket_frame(), websocket_state()} | {:ok, websocket_state()}
```

# `websocket_socket`

```elixir
@type websocket_socket() :: {instance(), route()}
```

# `websocket_state`

```elixir
@type websocket_state() :: any()
```

# `add`

```elixir
@spec add(binary()) :: :ok
```

# `add`

```elixir
@spec add(
  binary(),
  keyword()
) :: :ok
@spec add(pid(), binary()) :: :ok
```

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"))

# `add`

```elixir
@spec add(pid(), binary(), keyword()) :: :ok
```

Adds a route to a test server instance.

See `add/2` for options.

# `get_instance`

```elixir
@spec get_instance() :: pid() | nil
```

Gets current test server instance if running.

## Examples

    refute TestServer.get_instance()

    {:ok, instance} = TestServer.start()

    assert TestServer.get_instance() == instance

# `plug`

```elixir
@spec plug(module() | function()) :: :ok
```

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)

# `plug`

```elixir
@spec plug(pid(), module() | function()) :: :ok
```

Adds a route to a test server instance.

See `plug/1` for more.

# `start`

```elixir
@spec start(keyword()) :: {:ok, pid()}
```

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.start(
      scheme: :https,
      ipfamily: :inet6,
      http_server: {TestServer.HTTPServer.Bandit, [ip: :any]}
    )

    TestServer.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.x509_suite().cacerts],
        protocols: [:http2]
      ]
    ]

    assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} =
            Req.get(TestServer.url(), req_opts)

# `stop`

```elixir
@spec stop() :: :ok | {:error, term()}
```

Shuts down the current test server.

## Examples

    TestServer.start()
    url = TestServer.url()
    TestServer.stop()

    assert {:error, %Req.TransportError{}} = Req.get(url, retry: false)

# `stop`

```elixir
@spec stop(pid()) :: :ok | {:error, term()}
```

Shuts down a test server instance.

# `url`

```elixir
@spec url() :: binary()
```

# `url`

```elixir
@spec url(binary() | keyword() | pid()) :: binary()
```

# `url`

```elixir
@spec url(
  binary(),
  keyword()
) :: binary()
@spec url(pid(), binary()) :: binary()
```

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"

# `url`

```elixir
@spec url(pid(), binary(), keyword()) :: binary()
```

Produces a URL for a test server instance.

See `url/2` for options.

# `websocket_handle`

```elixir
@spec websocket_handle(websocket_socket()) :: :ok | {:error, term()}
```

# `websocket_handle`

```elixir
@spec websocket_handle(
  websocket_socket(),
  keyword()
) :: :ok
```

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"}

# `websocket_info`

```elixir
@spec websocket_info(websocket_socket(), function() | nil) :: :ok
```

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"}

# `websocket_init`

```elixir
@spec websocket_init(binary()) :: {:ok, websocket_socket()} | {:error, term()}
```

# `websocket_init`

```elixir
@spec websocket_init(
  binary(),
  keyword()
) :: {:ok, websocket_socket()}
@spec websocket_init(pid(), binary()) :: {:ok, websocket_socket()}
```

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"))

# `websocket_init`

```elixir
@spec websocket_init(pid(), binary(), keyword()) :: {:ok, websocket_socket()}
```

Adds a websocket route to a test server.

See `websocket_init/2` for options.

# `x509_suite`

```elixir
@spec x509_suite() :: term()
```

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)

# `x509_suite`

```elixir
@spec x509_suite(pid()) :: term()
```

Fetches the generated x509 suite for a test server instance.

See `x509_suite/0` for more.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
