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_idis"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_typeoption, theContent-Typeheader, 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 asContent-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. AContent-Typevalue 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/jsonfor a map or list,text/plainfor a string that is valid UTF-8 without NUL bytes, andapplication/octet-streamfor any other binary. - A map or list body is sent as JSON. With an XML content type
(
application/xml,text/xml, orapplication/ortext/with a+xmlsuffix) orapplication/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=bcounts). 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,niland 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=bunder 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.
Like run/4, but returns :ok or raises the PactEx.MismatchError or
PactEx.Error.
Starts a mock server for interactions.
Functions
@spec run( PactEx.Pact.t(), [PactEx.HTTP.Interaction.t()], (PactEx.MockServer.t() -> any()), keyword() ) :: :ok | {:error, PactEx.MismatchError.t() | PactEx.Error.t()}
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.fundoes not run when the server cannot start.
@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.
@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 to0, 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.