HttpDouble (http_double v1.0.0)

Copy Markdown View Source

Public API for starting and controlling dummy HTTP servers for integration tests.

The main entry points are:

Documentation: README on GitHub and HexDocs.

Summary

Types

A recorded HTTP call, suitable for assertions in tests.

Endpoint information for pointing external systems (for example ejabberd) at a running HttpDouble instance.

Matcher used to select which HTTP requests should be affected by a stub/expectation.

Response specification used by stubs, expectations and static routes.

A server reference; can be a PID or a registered name.

Functions

Adds a single static route at runtime.

Adds multiple static routes at runtime.

Returns the list of all HTTP calls observed by a server.

Returns a child_spec/1 suitable for embedding the server under a supervisor.

Deletes a route, identified either by :id or by {method, path}.

Returns a convenient endpoint map %{host, port, base_url} for pointing external systems (for example ejabberd) at this HttpDouble instance.

Adds an expectation rule.

Returns the logical host the server advertises (for configuration injection).

Returns the TCP port the server is currently listening on.

Resets the server state

Replaces the full static route table at runtime.

Starts a new dummy HTTP server.

Stops the server gracefully.

Adds a stub rule using an options keyword list.

Convenience stub form: stub(server, matcher, response_spec).

Updates a single route, identified either by :id or by {method, path}.

Builds a full URL for a given path (string or list of segments) using the server's current endpoint.

Types

call_record()

@type call_record() :: %{
  conn_id: non_neg_integer(),
  request: HttpDouble.Request.t(),
  timestamp: integer(),
  rule_id: reference() | nil,
  route_id: reference() | nil,
  action: term()
}

A recorded HTTP call, suitable for assertions in tests.

endpoint()

@type endpoint() :: %{host: String.t(), port: non_neg_integer(), base_url: String.t()}

Endpoint information for pointing external systems (for example ejabberd) at a running HttpDouble instance.

matcher()

@type matcher() ::
  :any
  | {:method, atom() | String.t()}
  | {:path, String.t()}
  | {:prefix, String.t()}
  | {:path_regex, Regex.t()}
  | {:route, atom() | String.t(), String.t()}
  | {:fn, (HttpDouble.Request.t(), HttpDouble.MockRule.meta() -> boolean())}
  | map()

Matcher used to select which HTTP requests should be affected by a stub/expectation.

Supported shapes:

  • :any – match any request
  • {:method, method} – e.g. {:method, :get} or {:method, "POST"}
  • {:path, path} – exact path match, e.g. {:path, "/health"}
  • {:prefix, prefix} – prefix path match, e.g. {:prefix, "/api"}
  • {:path_regex, re} – regular expression on the request path
  • {:route, method, path} – method + path match
  • {:fn, fun} – predicate fun.(request, meta) :: boolean
  • a map with keys like :method, :path, :host, :query, :headers, :body that will be interpreted by HttpDouble.MockRule

response_spec()

@type response_spec() ::
  map()
  | HttpDouble.Response.t()
  | {:delay, non_neg_integer(), response_spec()}
  | :timeout
  | :no_reply
  | :close
  | {:close, response_spec()}
  | {:raw, iodata()}
  | {:partial, [iodata()]}
  | {:fun,
     (HttpDouble.Request.t(), HttpDouble.MockRule.meta() -> response_spec())}
  | [response_spec()]

Response specification used by stubs, expectations and static routes.

Supported forms:

  • simple maps:
    • %{status: 200, body: "ok"}
    • %{status: 201, json: %{result: "accepted"}}
    • %{status: 204} (empty body)
  • explicit response struct HttpDouble.Response.t()
  • {:delay, ms, inner} – delay sending inner by ms milliseconds
  • :timeout or :no_reply – do not send any reply
  • :close – immediately close the TCP connection
  • {:close, inner} – send inner then close the connection
  • {:raw, iodata} – send raw bytes as-is (can be invalid HTTP)
  • {:partial, [iodata()]} – send raw chunks one by one
  • {:fun, fun} – callback fun.(request, meta) :: response_spec
  • a non-empty list of response specs – used sequentially per matcher

server()

@type server() :: GenServer.server()

A server reference; can be a PID or a registered name.

Functions

add_route(server, route_spec)

@spec add_route(server(), map()) :: :ok

Adds a single static route at runtime.

add_routes(server, route_specs)

@spec add_routes(server(), [map()]) :: :ok

Adds multiple static routes at runtime.

calls(server)

@spec calls(server()) :: [call_record()]

Returns the list of all HTTP calls observed by a server.

child_spec(opts)

@spec child_spec(Keyword.t()) :: Supervisor.child_spec()

Returns a child_spec/1 suitable for embedding the server under a supervisor.

delete_route(server, route_spec)

@spec delete_route(server(), map()) :: :ok

Deletes a route, identified either by :id or by {method, path}.

endpoint(server)

@spec endpoint(server()) :: endpoint()

Returns a convenient endpoint map %{host, port, base_url} for pointing external systems (for example ejabberd) at this HttpDouble instance.

expect(server, matcher, response_spec)

@spec expect(server(), matcher(), response_spec()) :: {:ok, reference()}

Adds an expectation rule.

Semantics are the same as for stub/3, but expectations are intended to be asserted via calls/1 in your tests. The engine will keep track of how many times each expectation was used.

host(server)

@spec host(server()) :: String.t()

Returns the logical host the server advertises (for configuration injection).

port(server)

@spec port(server()) :: non_neg_integer()

Returns the TCP port the server is currently listening on.

reset!(server)

@spec reset!(server()) :: :ok

Resets the server state:

  • clears all routes
  • clears all mock rules and expectations
  • clears call history

set_routes(server, route_specs)

@spec set_routes(server(), [map()]) :: :ok

Replaces the full static route table at runtime.

start_link(opts \\ [])

@spec start_link(Keyword.t()) :: GenServer.on_start()

Starts a new dummy HTTP server.

Options:

  • :port – TCP port to listen on (defaults to 0, meaning a random free port)
  • :host – logical host name to report in endpoint/1 (default: "127.0.0.1")
  • :ip – IP tuple to bind to (default: {127, 0, 0, 1})
  • :routes – list of static route definitions (see README for examples)
  • :mode – one of:
    • :mock_first (default) – try mock rules first, fall back to static routes
    • :routes_only – ignore mock rules, use only static routes
    • :mock_only – only apply mock rules, unknown requests are 404
  • :name – optional registered name (via Registry.HttpDouble.ServerRegistry)

stop(server)

@spec stop(server()) :: :ok

Stops the server gracefully.

stub(server, opts)

@spec stub(server(), Keyword.t()) :: {:ok, reference()}

Adds a stub rule using an options keyword list.

This is the most flexible form and supports all fields of HttpDouble.MockRule.

stub(server, matcher, response_spec)

@spec stub(server(), matcher(), response_spec()) :: {:ok, reference()}

Convenience stub form: stub(server, matcher, response_spec).

Examples:

# Always return 200/"ok" for GET /health
{:ok, _rule} =
  HttpDouble.stub(server, %{method: "GET", path: "/health"}, %{status: 200, body: "ok"})

# Sequential responses for POST /api/messages
{:ok, _rule} =
  HttpDouble.stub(server, %{method: :post, path: "/api/messages"}, [
    %{status: 201, json: %{result: "accepted"}},
    %{status: 503, body: "unavailable"}
  ])

update_route(server, route_spec)

@spec update_route(server(), map()) :: :ok

Updates a single route, identified either by :id or by {method, path}.

url(server, path)

@spec url(server(), String.t() | [String.t()]) :: String.t()

Builds a full URL for a given path (string or list of segments) using the server's current endpoint.