PactEx.Consumer (pact_ex v0.1.0)

Copy Markdown View Source

Runs consumer-side contract tests against a Pact mock server.

pact = PactEx.new("web", "users-api")

interaction =
  PactEx.HTTP.interaction("get user")
  |> PactEx.HTTP.with_request(:get, "/users/1")
  |> PactEx.HTTP.will_respond_with(200, body: %{"id" => 1})

PactEx.Consumer.run!(pact, [interaction], fn mock ->
  Req.get!(mock.url <> "/users/1")
end)

run/4 starts a mock server, calls your function, checks that every expected request arrived and that no other did, then writes the pact file. Nothing is written when something did not match.

How interactions are encoded

  • Paths, query values and header values are strings or matcher maps (see PactEx.Matchers). Query and header values may also be integers, floats or booleans, which become strings. A list gives a multi-valued parameter. nil, an empty list and other types are rejected: an empty list would drop the parameter without a word.
  • Query and header names are strings or atoms. An atom header name is written as Req writes it: underscores become dashes and ASCII letters lower case (:x_request_id is "x-request-id"). Query names and string names are not changed.
  • Each request and response has one body content type. The candidates are the :content_type option, the Content-Type header, and the default of the body type. Each is trimmed and the first one that pact_ffi can parse is used, so a type such as "application/json; charset" is skipped. That one string is passed with the body, and when it came from a string header the header is written with it. The header name can be in any case and is written as Content-Type. Its value can be a string, a matcher (its "value" is used) or a list (its first element is used). Two headers that differ only in case are an error. A header that was not the source of the type is sent as you wrote it. A Content-Type value that pact_ffi would turn into an empty list (a map or JSON string with "value" => [], a matcher too) returns {:error, %PactEx.Error{code: -101}}, with or without a body. That holds for any element of a list. As the whole header or the last element, pact_ffi aborts the VM on it. Before a later element, it records an empty string. An empty or whitespace-only string (as the value, an element or a matcher's "value") is rejected too. On a request no request of the interaction can match such a header, and on a response no provider can produce it.
  • The default is application/json for a map or list, text/plain for a string that is valid UTF-8 without NUL bytes, and application/octet-stream for any other binary.
  • A map or list body is sent as JSON. With an XML content type (application/xml, text/xml, or application/ or text/ with a +xml suffix) or application/x-www-form-urlencoded, pact_ffi reads JSON as a description of the body in its own format. That also applies to a string body that pact_ffi's sniffing takes for JSON. The sniffing reads only the first characters, after optional whitespace: true, false, null, an ASCII digit, a quote, [, or { followed by optional whitespace and then } or a quote and a word character. The rest of the text is not looked at (1=a&2=b counts). pact_ffi panics on some descriptions, which aborts the VM, so PactEx checks them first and returns {:error, %PactEx.Error{code: -101}} naming the interaction and the path of the problem. An XML description is %{"root" => element}, where an element is %{"name" => ..., "attributes" => ..., "children" => [...]}, a text node is %{"content" => ...}, and a matcher element is %{"pact:matcher:type" => ..., "value" => element}. A form description is a map of strings, numbers, matchers with such a value and lists of them (nested maps, booleans, nil and keys that start with "pact:" are errors, because pact_ffi would drop them). A type that is both JSON and XML (application/xml+json) is JSON.
  • Every JSON PactEx writes is decoded again and an object that repeats a key is an error at any depth, for every content type. That covers %{a: 1, "a" => 2}, structs that encode to a repeated key, and matcher maps in headers and provider state params.
  • A string body is sent byte for byte, except under an XML or form type when pact_ffi's sniffing takes it for JSON. Then it must be a valid JSON description (for example 1=a&2=b under a form type is an error). Any other body type is an error.

The pact file is <pact_dir>/<consumer>-<provider>.json. It is merged with an existing file, so several runs can add interactions to the same pact.

Summary

Functions

Runs fun against a mock server for interactions and writes the pact if everything matched.

Starts a mock server for interactions.

Functions

run(pact, interactions, fun, opts \\ [])

Runs fun against a mock server for interactions and writes the pact if everything matched.

fun receives the PactEx.MockServer.t/0. Its return value is ignored. The server is stopped when fun returns, raises, throws or exits. If fun raises, throws or exits, nothing is written and the exception propagates. A brutal kill of the calling process (an ExUnit timeout or a linked crash) skips the cleanup, and the server lives until the OS process exits.

Takes the same options as start_mock_server/3.

Returns:

  • :ok - everything matched and the pact was written.
  • {:error, %PactEx.MismatchError{}} - an expected request never arrived, or an unexpected one did. Nothing is written.
  • {:error, %PactEx.Error{}} - the server could not start, an interaction is invalid, or the pact file could not be written. fun does not run when the server cannot start.

run!(pact, interactions, fun, opts \\ [])

@spec run!(
  PactEx.Pact.t(),
  [PactEx.HTTP.Interaction.t()],
  (PactEx.MockServer.t() -> any()),
  keyword()
) ::
  :ok

Like run/4, but returns :ok or raises the PactEx.MismatchError or PactEx.Error.

start_mock_server(pact, interactions, opts \\ [])

@spec start_mock_server(PactEx.Pact.t(), [PactEx.HTTP.Interaction.t()], keyword()) ::
  {:ok, PactEx.MockServer.t()} | {:error, PactEx.Error.t()}

Starts a mock server for interactions.

Use this when run/4 is too rigid. You must call PactEx.MockServer.stop/1 when done, and PactEx.MockServer.write_pact/2 if you want the pact file.

Options

  • :host - address to bind. Defaults to "127.0.0.1".
  • :port - port to bind, from 0 to 65535. Defaults to 0, which lets the OS pick a free port.

Returns {:error, %PactEx.Error{}} if an interaction is invalid (for example, it has no request or a value of an unsupported type), if an argument contains a NUL byte, or if the server cannot bind. A missing response defaults to status 200.

Raises ArgumentError on unknown options, a port out of range, or a NUL byte in the consumer, provider or an interaction description.