HTTPEx.Backend.Mock.Expectation (HTTPEx v0.2.3)

View Source

Defines a HTTP mock expectation. Consists of a matching part and an expectation part.

Summary

Functions

Finds the expectation that matches the given Request.t()

Gets expects field from given Expectation

Gets matcher field from given Expectation

Looks up which type the given matcher field is

Increases the calls counter in Expectation

Tests if the request mets all defined expectations.

Tests if the request matches all fields.

Builds an Expectation struct from the given Keyword list

Sets expects field for given Expectation

Sets matcher field for given Expectation

Generates a fake response from a map, an error tuple or a http driver struct. Replaces any vars if a response map has the option replace_body_vars set to true.

Validates if the field with the given value is allowed as an expects

Validates if the field with the given value is allowed as a matcher

Types

enum_matcher()

@type enum_matcher() :: atom()

exact_value_matcher()

@type exact_value_matcher() :: {:exact_value, any()}

expects_result()

@type expects_result() :: {boolean(), [atom()], [{atom(), any(), any()}]}

func_matcher()

@type func_matcher() ::
  (HTTPEx.Request.t() -> boolean()) | (HTTPEx.Request.t() -> {boolean(), map()})

http_response()

@type http_response() :: {:ok, struct()} | {:error, struct()}

int_matcher()

@type int_matcher() :: integer()

keyword_list_matcher()

@type keyword_list_matcher() :: [{String.t(), String.t() | Regex.t()}]

map_matcher()

@type map_matcher() :: map()

match_result()

@type match_result() :: {boolean(), [atom()], [atom()], map()}

matcher()

matcher_field_type()

@type matcher_field_type() ::
  :func
  | :string
  | :string_with_format
  | :regex
  | :wildcard
  | :keyword_list
  | :map
  | :enum
  | :int
  | :exact_value

regex_matcher()

@type regex_matcher() :: Regex.t()

response_error()

@type response_error() :: {:error, atom()}

response_func()

@type response_func() :: (HTTPEx.Request.t() -> response_map() | response_error())

response_map()

@type response_map() :: %{
  :status => Plug.Conn.status(),
  :body => String.t(),
  optional(:delay) => number(),
  optional(:headers) => list(),
  optional(:replace_body_vars) => boolean()
}

string_formats()

@type string_formats() :: :json | :xml | :form

string_matcher()

@type string_matcher() :: String.t()

string_with_format_matcher()

@type string_with_format_matcher() :: {String.t(), string_formats()}

t()

@type t() :: %HTTPEx.Backend.Mock.Expectation{
  calls: non_neg_integer(),
  description: String.t() | nil,
  expects: %{
    body:
      func_matcher()
      | string_matcher()
      | string_with_format_matcher()
      | regex_matcher()
      | wildcard_matcher()
      | exact_value_matcher(),
    headers: keyword_list_matcher() | wildcard_matcher(),
    path: string_matcher() | regex_matcher() | wildcard_matcher(),
    query: map_matcher() | wildcard_matcher()
  },
  global: boolean(),
  index: non_neg_integer(),
  matchers: %{
    body:
      func_matcher()
      | string_matcher()
      | string_with_format_matcher()
      | regex_matcher()
      | wildcard_matcher()
      | exact_value_matcher(),
    headers: func_matcher() | keyword_list_matcher() | wildcard_matcher(),
    host:
      func_matcher() | string_matcher() | regex_matcher() | wildcard_matcher(),
    method: enum_matcher() | wildcard_matcher(),
    path:
      func_matcher() | string_matcher() | regex_matcher() | wildcard_matcher(),
    port: int_matcher() | wildcard_matcher(),
    query: func_matcher() | map_matcher() | wildcard_matcher()
  },
  max_calls: non_neg_integer() | :infinity,
  min_calls: non_neg_integer(),
  priority: non_neg_integer(),
  response: response_func() | response_map() | response_error(),
  stacktrace: nil | tuple(),
  type: :assertion | :stub
}

wildcard_matcher()

@type wildcard_matcher() :: :any

Functions

find_matching_expectation(expectations, request)

@spec find_matching_expectation([t()], HTTPEx.Request.t()) ::
  {:ok, t(), map()} | {:error, atom()} | {:error, atom(), t()}

Finds the expectation that matches the given Request.t()

Example

iex> expectation_1 =
...>   %Expectation{index: 0}
...>   |> Expectation.set_match!(:host, "www.example.com")
...>   |> Expectation.set_match!(:port, 80)
...>
...> expectation_2 =
...>   %Expectation{index: 1}
...>   |> Expectation.set_match!(:host, "www.example.com")
...>   |> Expectation.set_match!(:path, Regex.compile!("api/(?<api_version>[^/]+)/path/*"))
...>   |> Expectation.set_match!(:port, 80)
...>
...> expectations = [expectation_1, expectation_2]
...>
...> {:ok, match, vars} =
...>   Expectation.find_matching_expectation(
...>     expectations,
...>     %Request{
...>       url: "http://www.example.com/api/v1/path/test?token=XYZ&user_id=1337",
...>       body: "Payload OK!",
...>       headers: [{"app", "test"}, {"secret", "123"}],
...>       method: :post
...>     }
...>   )
...>
...> match == expectation_1
true
iex> vars
%{}
iex> {:ok, match, vars} =
...>   Expectation.find_matching_expectation(
...>     expectations,
...>     %Request{
...>       url: "http://www.example.com/another-path",
...>       body: "Payload OK!",
...>       headers: [{"app", "test"}, {"secret", "123"}],
...>       method: :post
...>     }
...>   )
...>
...> match == expectation_1
true
iex> vars
%{}

get_expect(expectation, key)

@spec get_expect(t(), atom()) :: matcher()

Gets expects field from given Expectation

Examples

iex> expectation = Expectation.set_expect!(%Expectation{}, :body, "OK!")
iex> Expectation.get_expect(expectation, :body)
"OK!"
iex> Expectation.get_expect(expectation, :path)
:any

get_match(expectation, key)

@spec get_match(t(), atom()) :: matcher()

Gets matcher field from given Expectation

Examples

iex> expectation = Expectation.set_match!(%Expectation{}, :host, "localhost")
iex> Expectation.get_match(expectation, :host)
"localhost"
iex> Expectation.get_match(expectation, :headers)
:any

iex> expectation = Expectation.set_match!(%Expectation{}, :path, fn _request -> true end)
iex> is_function(Expectation.get_match(expectation, :path))
true

get_matcher_type(field, value)

@spec get_matcher_type(atom(), matcher()) :: matcher_field_type()

Looks up which type the given matcher field is

Examples

iex> Expectation.get_matcher_type(:host, "localhost")
:string

iex> Expectation.get_matcher_type(:body, {"{}", :json})
:string_with_format

iex> Expectation.get_matcher_type(:body, {"<a>b</a>", :xml})
:string_with_format

iex> Expectation.get_matcher_type(:body, {"foo=bar", :form})
:string_with_format

iex> Expectation.get_matcher_type(:body, {:exact_value, {:form, "data"}})
:exact_value

iex> Expectation.get_matcher_type(:host, fn _request -> true end)
:func

iex> Expectation.get_matcher_type(:query, %{"user_id" => "1234"})
:map

iex> Expectation.get_matcher_type(:headers, [{"Content-Type", "application/json"}])
:keyword_list

iex> Expectation.get_matcher_type(:path, :any)
:wildcard

iex> Expectation.get_matcher_type(:port, 1337)
:int

iex> Expectation.get_matcher_type(:path, ~r/api/)
:regex

iex> Expectation.get_matcher_type(:method, :post)
:enum

iex> Expectation.get_matcher_type(:method, :get)
:enum

increase_call(expectation)

@spec increase_call(t()) :: t()

Increases the calls counter in Expectation

Example

iex> expectation = %Expectation{}
...> expectation.calls
0
iex> expectation = Expectation.increase_call(expectation)
...> expectation.calls
1

match_expects(request, expectation)

@spec match_expects(HTTPEx.Request.t(), t()) :: expects_result()

Tests if the request mets all defined expectations.

The match_expects function will always return a tuple with a boolean and a list of fields that did not match.

Examples

A complex example, mixing different matchers:

iex> expectation =
...>   %Expectation{}
...>   |> Expectation.set_expect!(:body, fn %Request{} = request -> {String.contains?(request.body, "OK"), %{"var" => 1}} end)
...>   |> Expectation.set_expect!(:headers, [{"secret", "123"}])
...>   |> Expectation.set_expect!(:path, Regex.compile!("api/(?<api_version>[^/]+)/path"))
...>   |> Expectation.set_expect!(:query, %{"user_id" => "1337"})
iex> Expectation.match_expects(
...>   %Request{
...>     url: "http://www.example.com/api/v1/path/test?token=XYZ&user_id=1337",
...>     body: "Payload OK!",
...>     headers: [{"app", "test"}, {"secret", "123"}],
...>     method: :post
...>   },
...>   expectation
...> )
{true, [:path, :body, :query, :headers], []}
iex> Expectation.match_expects(
...>   %Request{
...>     url: "http://www.example.com/api/v1/path/test?token=XYZ&user_id=1339",
...>     body: "Some error",
...>     headers: [{"app", "test"}, {"geheim", "123"}],
...>     method: :post
...>   },
...>   expectation
...> )
{
  false,
  [:path],
  [
  {:body, true, false}, {:query, %{"user_id" => "1337"}, %{"token" => "XYZ", "user_id" => "1339"}}, {:headers, [{"secret", "123"}], [{"app", "test"}, {"geheim", "123"}]}]}

match_request(request, expectation)

@spec match_request(HTTPEx.Request.t(), t()) :: match_result()

Tests if the request matches all fields.

The match_request function will always return a tuple with a boolean, the fields that matched, and a map with collected variables from regexes or :func matchers that were executed.

These vars can be used in responses to replace placeholders in responses.

Examples

A complex example, mixing different matchers:

iex> expectation =
...>   %Expectation{}
...>   |> Expectation.set_match!(:host, "www.example.com")
...>   |> Expectation.set_match!(:port, 80)
...>   |> Expectation.set_match!(:body, fn %Request{} = request -> {String.contains?(request.body, "OK"), %{"var" => 1}} end)
...>   |> Expectation.set_match!(:headers, [{"secret", "123"}])
...>   |> Expectation.set_match!(:path, Regex.compile!("api/(?<api_version>[^/]+)/path"))
...>   |> Expectation.set_match!(:query, %{"user_id" => "1337"})
...>   |> Expectation.set_match!(:method, :post)
iex> Expectation.match_request(
...>   %Request{
...>     url: "http://www.example.com/api/v1/path/test?token=XYZ&user_id=1337",
...>     body: "Payload OK!",
...>     headers: [{"app", "test"}, {"secret", "123"}],
...>     method: :post
...>   },
...>   expectation
...> )
{true, [:method, :headers, :query, :body, :host, :path, :port], [], %{"api_version" => "v1", "var" => 1}}
iex> Expectation.match_request(
...>   %Request{
...>     url: "http://www.example.co/api/v2/path/test?token=XYZ",
...>     body: "Payload OK!",
...>     headers: [{"app", "test"}, {"secret", "123"}],
...>     method: :post
...>   },
...>   expectation
...> )
{false, [:method, :headers, :body, :path, :port], [:host, :query], %{"api_version" => "v2", "var" => 1}}

A couple of examples using the body formatters.

JSON:

iex> payload = JSON.encode!(%{username: "test"})
...> formatted_payload = JSON.encode!(%{username: "test"})
iex> expectation =
...>   %Expectation{}
...>   |> Expectation.set_match!(:host, "www.example.com")
...>   |> Expectation.set_match!(:port, 80)
...>   |> Expectation.set_match!(:body, {formatted_payload, :json})
...>   |> Expectation.set_match!(:method, :post)
iex> Expectation.match_request(
...>   %Request{
...>     url: "http://www.example.com/api/v1/path/test?token=XYZ&user_id=1337",
...>     body: payload,
...>     method: :post
...>   },
...>   expectation
...> )
{true, [:method, :headers, :query, :body, :host, :path, :port], [], %{}}

XML:

  iex> payload = "<test>data</test>"
  ...> formatted_payload = "<test>\n  data\n</test>"
  iex> expectation =
  ...>   %Expectation{}
  ...>   |> Expectation.set_match!(:host, "www.example.com")
  ...>   |> Expectation.set_match!(:port, 80)
  ...>   |> Expectation.set_match!(:body, {formatted_payload, :xml})
  ...>   |> Expectation.set_match!(:method, :post)
  iex> Expectation.match_request(
  ...>   %Request{
  ...>     url: "http://www.example.com/api/v1/path/test?token=XYZ&user_id=1337",
  ...>     body: payload,
  ...>     method: :post
  ...>   },
  ...>   expectation
  ...> )
  {true, [:method, :headers, :query, :body, :host, :path, :port], [], %{}}

new!(opts)

@spec new!(Keyword.t()) :: t()

Builds an Expectation struct from the given Keyword list

Options

  • description
  • method
  • endpoint
  • body
  • headers
  • host
  • path
  • port
  • query
  • expect_body
  • expect_headers
  • expect_path
  • expect_query
  • min_calls
  • max_calls
  • stacktrace

Examples

iex> Expectation.new!(method: :get, endpoint: "http://www.example.com", response: %{status: 200, body: "OK"}, type: :assert)
%Expectation{matchers: %{host: "www.example.com", method: :get, path: "/", port: 80, body: :any, headers: :any, query: :any}, type: :assertion}

set_expect!(expectation, field, value)

@spec set_expect!(t(), atom(), matcher()) :: t()

Sets expects field for given Expectation

Examples

iex> Expectation.set_expect!(%Expectation{}, :body, "ok!")
%Expectation{expects: %{body: "ok!", headers: :any, path: :any, query: :any}}

iex> Expectation.set_expect!(%Expectation{}, :body, nil)
** (ArgumentError) Invalid type used for field expectation `body`. Must be one of: `function()`, `String.t()`, `{String.t(), :json | :xml}`, `RegEx.t()`, `:any`, `{:exact_value, any()}`

iex> Expectation.set_expect!(%Expectation{}, :unknown, nil)
** (ArgumentError) Unknown field `unknown` for expectation

set_match!(expectation, field, value)

@spec set_match!(t(), atom(), matcher()) :: t()

Sets matcher field for given Expectation

Examples

iex> Expectation.set_match!(%Expectation{}, :host, "localhost")
%Expectation{matchers: %{body: :any, headers: :any, host: "localhost", method: :any, path: :any, port: :any, query: :any}}

iex> Expectation.set_match!(%Expectation{}, :body, "OK!")
%Expectation{matchers: %{body: "OK!", headers: :any, host: :any, method: :any, path: :any, port: :any, query: :any}}

iex> Expectation.set_match!(%Expectation{}, :body, {:exact_value, {:form, "OK!"}})
%Expectation{matchers: %{body: {:exact_value, {:form, "OK!"}}, headers: :any, host: :any, method: :any, path: :any, port: :any, query: :any}}

iex> Expectation.set_match!(%Expectation{}, :body, {"{}", :json})
%Expectation{matchers: %{body: {"{}", :json}, headers: :any, host: :any, method: :any, path: :any, port: :any, query: :any}}

iex> Expectation.set_match!(%Expectation{}, :body, {"<test>a</test>", :xml})
%Expectation{matchers: %{body: {"<test>a</test>", :xml}, headers: :any, host: :any, method: :any, path: :any, port: :any, query: :any}}

iex> Expectation.set_match!(%Expectation{}, :body, {"foo=bar", :form})
%Expectation{matchers: %{body: {"foo=bar", :form}, headers: :any, host: :any, method: :any, path: :any, port: :any, query: :any}}

iex> Expectation.set_match!(%Expectation{}, :host, nil)
** (ArgumentError) Invalid type used for field matcher `host`. Must be one of: `function()`, `String.t()`, `RegEx.t()`, `:any`

iex> Expectation.set_match!(%Expectation{}, :unknown, :any)
** (ArgumentError) Unknown field `unknown` for matcher

to_client_response(client, arg2, response)

to_response(request, expectation, vars)

Generates a fake response from a map, an error tuple or a http driver struct. Replaces any vars if a response map has the option replace_body_vars set to true.

The response can also be a function. This function receives a Request.t() and must return a valid response().

validate_expectations(request, expectation)

@spec validate_expectations(HTTPEx.Request.t(), t()) ::
  :ok | {:error, :expectations_not_met, [{atom(), any(), any()}]}

validate_expects_value(field, value)

@spec validate_expects_value(atom(), matcher()) :: :ok | {:error, atom()}

Validates if the field with the given value is allowed as an expects

validate_matcher_value(field, value)

@spec validate_matcher_value(atom(), matcher()) :: :ok | {:error, atom()}

Validates if the field with the given value is allowed as a matcher

Examples

An exact value matcher (note: uses ===):

iex> Expectation.validate_matcher_value(:body, {:exact_value, {:form, [a: "data"]}})
:ok

Note, this matcher cannot be used on all fields:

iex> Expectation.validate_matcher_value(:host, {:exact_value, "some-host"})
{:error, :invalid_field_type}

A string matcher:

iex> Expectation.validate_matcher_value(:host, "localhost")
:ok

You can also use a function and either return a true/false:

iex> Expectation.validate_matcher_value(:host, fn _request -> true end)
:ok

Note, that the function has to have an arity of one. Other function arity's will return an error:

iex> Expectation.validate_matcher_value(:host, fn -> true end)
{:error, :invalid_field_type}

You can also supply regexes:

iex> Expectation.validate_matcher_value(:path, Regex.compile!("http://localhost:5000/*"))
:ok

A wildcard:

iex> Expectation.validate_matcher_value(:path, :any)
:ok

Match keyword lists with String.t() values on both sides:

iex> Expectation.validate_matcher_value(:headers, [{"Content-Type", "application/json"}])
:ok

Or with a Regex.t():

iex> Expectation.validate_matcher_value(:headers, [{"Content-Type", ~r/application/}])
:ok

Invalid lists, tuples. types are not valid:

iex> Expectation.validate_matcher_value(:headers, [{"Content-Type", :value}])
{:error, :invalid_field_type}

iex> Expectation.validate_matcher_value(:headers, [])
{:error, :invalid_field_type}

iex> Expectation.validate_matcher_value(:headers, [{"Content-Type", "Value", "Other-Value"}])
{:error, :invalid_field_type}

iex> Expectation.validate_matcher_value(:headers, "Content-Type: application/json")
{:error, :invalid_field_type}

Some fields require an enum, like method:

iex> Expectation.validate_matcher_value(:method, :post)
:ok

iex> Expectation.validate_matcher_value(:method, :get)
:ok

iex> Expectation.validate_matcher_value(:method, "get")
{:error, :invalid_field_type}

You can also match a map. This is useful if you want to match query params:

iex> Expectation.validate_matcher_value(:query, %{"user_id" => "1234"})
:ok

iex> Expectation.validate_matcher_value(:query, %{user_id: "1234"})
{:error, :invalid_field_type}

iex> Expectation.validate_matcher_value(:query, %{})
{:error, :invalid_field_type}