pact_consumer/
lib.rs

1//! The `pact_consumer` crate provides tools for writing consumer [Pact
2//! tests][pact]. It implements the [V3 Pact specification][spec]. You can also
3//! use it as a simple HTTP mocking library for Rust.
4//!
5//! [pact]: https://docs.pact.io/ [spec]:
6//! https://github.com/pact-foundation/pact-specification
7//!
8//! ## What is Pact?
9//!
10//! [Pact][pact] is a [cross-language standard][spec] for testing the
11//! communication between the consumer of a REST API, and the code that provides
12//! that API. Test cases are written from the consumer's perspective, and they
13//! can then be exported testing the provider.
14//!
15//! The big advantages of Pact are:
16//!
17//! 1. The mocks you write to test the client can also be reused to verify that
18//!    the server would actually respond the way the client expects. This gives
19//!    the end-to-end assurance of integration tests (well, almost), but with
20//!    the speed and convenience of unit tests.
21//! 2. Pact has been implemented in many popular languages, so you can test
22//!    clients and servers in multiple languages.
23//!
24//! Whenever possible, we try to use vocabulary similar to the Ruby or
25//! JavaScript API for basic concepts, and we try to provide the same behavior.
26//! But we offer many handy builder methods to make tests cleaner.
27//!
28//! ## How to use it
29//!
30//! To use this crate, add it to your `[dev-dependencies]` in your `Cargo.toml`:
31//!
32//! ```toml
33//! [dev-dependencies]
34//! pact_consumer = "~1.4.0"
35//! ```
36//!
37//! Once this is done, you can then write the following inside a function marked
38//! with `#[tokio::test]`:
39//!
40//! ```no_run
41//! use pact_consumer::prelude::*;
42//!
43//! // Define the Pact for the test, specify the names of the consuming
44//! // application and the provider application.
45//! let provider_service = PactBuilder::new("Consumer", "Alice Service")
46//!     // Start a new interaction. We can add as many interactions as we want.
47//!     .interaction("a retrieve Mallory request", "", |mut i| {
48//!         // Defines a provider state. It is optional.
49//!         i.given("there is some good mallory");
50//!         // Define the request, a GET (default) request to '/mallory'.
51//!         i.request.path("/mallory");
52//!         // Define the response we want returned. We assume a 200 OK
53//!         // response by default.
54//!         i.response
55//!             .content_type("text/plain")
56//!             .body("That is some good Mallory.");
57//!         // Return the interaction builder back to the pact framework
58//!         i
59//!     })
60//!     .start_mock_server(None, None);
61//! ```
62//!
63//! You can than use an HTTP client like `reqwest` to make requests against your
64//! server.
65//!
66//! ```rust
67//! # tokio_test::block_on(async {
68//! # use pact_models::pact::Pact;
69//! # use std::io::Read;
70//! # use pact_consumer::prelude::*;
71//! # let provider_service = PactBuilder::new("Consumer", "Alice Service")
72//! #     // Start a new interaction. We can add as many interactions as we want.
73//! #     .interaction("a retrieve Mallory request", "", |mut i| {
74//! #         // Defines a provider state. It is optional.
75//! #         i.given("there is some good mallory");
76//! #         // Define the request, a GET (default) request to '/mallory'.
77//! #         i.request.path("/mallory");
78//! #         // Define the response we want returned. We assume a 200 OK
79//! #         // response by default.
80//! #         i.response
81//! #             .content_type("text/plain")
82//! #             .body("That is some good Mallory.");
83//! #         // Return the interaction builder back to the pact framework
84//! #         i
85//! #     }).start_mock_server(None, None);
86//!
87//! // You would use your actual client code here.
88//! let mallory_url = provider_service.path("/mallory");
89//! let mut response = reqwest::get(mallory_url).await.expect("could not fetch URL")
90//!   .text().await.expect("Could not read response body");
91//! assert_eq!(response, "That is some good Mallory.");
92//!
93//! // When `provider_service` goes out of scope, your pact will be validated,
94//! // and the test will fail if the mock server didn't receive matching
95//! // requests.
96//! # });
97//! ```
98//!
99//! ## Matching using patterns
100//!
101//! You can also use patterns like `like!`, `each_like!` or `term!` to allow
102//! more general matches, and you can build complex patterns using the
103//! `json_pattern!` macro:
104//!
105//! ```
106//! use pact_consumer::prelude::*;
107//!
108//! PactBuilder::new("quotes client", "quotes service")
109//!     .interaction("add a new quote to the database", "", |mut i| {
110//!         i.request
111//!             .post()
112//!             .path("/quotes")
113//!             .json_utf8()
114//!             .json_body(json_pattern!({
115//!                  // Allow the client to send any string as a quote.
116//!                  // When testing the server, use "Eureka!".
117//!                  "quote": like!("Eureka!"),
118//!                  // Allow the client to send any string as an author.
119//!                  // When testing the server, use "Archimedes".
120//!                  "by": like!("Archimedes"),
121//!                  // Allow the client to send an array of strings.
122//!                  // When testing the server, send a single-item array
123//!                  // containing the string "greek".
124//!                  "tags": each_like!("greek"),
125//!              }));
126//!
127//!         i.response
128//!             .created()
129//!             // Return a location of "/quotes/12" to the client. When
130//!             // testing the server, allow it to return any numeric ID.
131//!             .header("Location", term!("^/quotes/[0-9]+$", "/quotes/12"));
132//!         i
133//!     });
134//! ```
135//!
136//! The key insight here is this "pact" can be used to test both the client and
137//! the server:
138//!
139//! - When testing the **client**, we allow the request to be anything which
140//!   matches the patterns—so `"quote"` can be any string, not just `"Eureka!"`.
141//!   But we respond with the specified values, such as `"/quotes/12"`.
142//! - When testing the **server**, we send the specified values, such as
143//!   `"Eureka!"`. But we allow the server to respond with anything matching the
144//!   regular expression `^/quotes/[0-9]+$`, because we don't know what database
145//!   ID it will use.
146//!
147//! Also, when testing the server, we may need to set up particular database
148//! fixtures. This can be done using the string passed to `given` in the
149//! examples above.
150//!
151//! ## Testing using domain objects
152//!
153//! Normally, it's best to generate your JSON using your actual domain objects.
154//! This is easier, and it reduces duplication in your code.
155//!
156//! ```
157//! use pact_consumer::prelude::*;
158//! use serde::{Deserialize, Serialize};
159//!
160//! /// Our application's domain object representing a user.
161//! #[derive(Deserialize, Serialize)]
162//! struct User {
163//!     /// All users have this field.
164//!     name: String,
165//!
166//!     /// The server may omit this field when sending JSON, or it may send it
167//!     /// as `null`.
168//!     comment: Option<String>,
169//! }
170//!
171//! // Create our example user using our normal application objects.
172//! let example = User {
173//!     name: "J. Smith".to_owned(),
174//!     comment: None,
175//! };
176//!
177//! PactBuilder::new("consumer", "provider")
178//!     .interaction("get all users", "", |mut i| {
179//!         i.given("a list of users in the database");
180//!         i.request.path("/users");
181//!         i.response
182//!             .json_utf8()
183//!             .json_body(each_like!(
184//!                 // Here, `strip_null_fields` will remove `comment` from
185//!                 // the generated JSON, allowing our pattern to match
186//!                 // missing comments, null comments, and comments with
187//!                 // strings.
188//!                 strip_null_fields(serde_json::json!(example)),
189//!             ));
190//!         i
191//!     })
192//!     .build();
193//! ```
194//!
195//! ## Testing messages
196//!
197//! Testing message consumers is supported. There are two types: asynchronous messages and synchronous request/response.
198//!
199//! ### Asynchronous messages
200//!
201//! Asynchronous messages are you normal type of single shot or fire and forget type messages. They are typically sent to a
202//! message queue or topic as a notification or event. With Pact tests, we will be testing that our consumer of the messages
203//! works with the messages setup as the expectations in test. This should be the message handler code that processes the
204//! actual messages that come off the message queue in production.
205//!
206//! The generated Pact file from the test run can then be used to verify whatever created the messages adheres to the Pact
207//! file.
208//!
209//! ```rust
210//! use pact_consumer::prelude::*;
211//! use expectest::prelude::*;
212//! use serde_json::{Value, from_slice};
213//!
214//! // Define the Pact for the test (you can setup multiple interactions by chaining the given or message_interaction calls)
215//! // For messages we need to use the V4 Pact format.
216//! let mut pact_builder = PactBuilder::new_v4("message-consumer", "message-provider"); // Define the message consumer and provider by name
217//! pact_builder
218//!   // Adds an interaction given the message description and type.
219//!   .message_interaction("Mallory Message", |mut i| {
220//!     // defines a provider state. It is optional.
221//!     i.given("there is some good mallory".to_string());
222//!     // Can set the test name (optional)
223//!     i.test_name("a_message_consumer_side_of_a_pact_goes_a_little_something_like_this");
224//!     // Set the contents of the message. Here we use a JSON pattern, so that matching rules are applied
225//!     i.json_body(json_pattern!({
226//!       "mallory": like!("That is some good Mallory.")
227//!     }));
228//!     // Need to return the mutated interaction builder
229//!     i
230//!   });
231//!
232//! // This will return each message configured with the Pact builder. We need to process them
233//! // with out message handler (it should be the one used to actually process your messages).
234//! for message in pact_builder.messages() {
235//!   let bytes = message.contents.contents.value().unwrap();
236//!
237//!   // Process the message here as it would if it came off the queue
238//!   let message: Value = serde_json::from_slice(&bytes).unwrap();
239//!
240//!   // Make some assertions on the processed value
241//!   expect!(message.as_object().unwrap().get("mallory")).to(be_some().value("That is some good Mallory."));
242//! }
243//! ```
244//!
245//! ### Synchronous request/response messages
246//!
247//! Synchronous request/response messages are a form of message interchange were a request message is sent to another service and
248//! one or more response messages are returned. Examples of this would be things like Websockets and gRPC.
249//!
250//! ```rust
251//! # use bytes::Bytes;
252//! # struct MessageHandler {}
253//! # struct MockProvider { pub message: Bytes }
254//! # impl MessageHandler { fn process(bytes: Bytes, provider: &MockProvider) -> anyhow::Result<&str> { Ok("That is some good Mallory.") } }
255//! use pact_consumer::prelude::*;
256//! use pact_consumer::*;
257//! use expectest::prelude::*;
258//! use serde_json::{Value, from_slice};
259//!
260//! // Define the Pact for the test (you can setup multiple interactions by chaining the given or message_interaction calls)
261//! // For synchronous messages we also need to use the V4 Pact format.
262//! let mut pact_builder = PactBuilder::new_v4("message-consumer", "message-provider"); // Define the message consumer and provider by name
263//! pact_builder
264//!   // Adds an interaction given the message description and type.
265//!   .synchronous_message_interaction("Mallory Message", |mut i| {
266//!     // defines a provider state. It is optional.
267//!     i.given("there is some good mallory".to_string());
268//!     // Can set the test name (optional)
269//!     i.test_name("a_synchronous_message_consumer_side_of_a_pact_goes_a_little_something_like_this");
270//!     // Set the contents of the request message. Here we use a JSON pattern, so that matching rules are applied.
271//!     // This is the request message that is going to be forwarded to the provider
272//!     i.request_json_body(json_pattern!({
273//!       "requestFor": like!("Some good Mallory, please.")
274//!     }));
275//!     // Add a response message we expect the provider to return. You can call this multiple times to add multiple messages.
276//!     i.response_json_body(json_pattern!({
277//!       "mallory": like!("That is some good Mallory.")
278//!     }));
279//!     // Need to return the mutated interaction builder
280//!     i
281//!   });
282//!
283//! // For our test we want to invoke our message handling code that is going to initialise the request
284//! // to the provider with the request message. But we need some mechanism to mock the response
285//! // with the resulting response message so we can confirm our message handler works with it.
286//! for message in pact_builder.synchronous_messages() {
287//!   // the request message we must make
288//!   let request_message_bytes = message.request.contents.value().unwrap();
289//!   // the response message we expect to receive from the provider
290//!   let response_message_bytes = message.response.first().unwrap().contents.value().unwrap();
291//!
292//!   // We use a mock here, assuming there is a Trait that controls the response message that our
293//!   // mock can implement.
294//!   let mock_provider = MockProvider { message: response_message_bytes };
295//!   // Invoke our message handler to send the request message from the Pact interaction and then
296//!   // wait for the response message. In this case it will be the response via the mock provider.
297//!   let response = MessageHandler::process(request_message_bytes, &mock_provider);
298//!
299//!   // Make some assertions on the processed value
300//!   expect!(response).to(be_ok().value("That is some good Mallory."));
301//! }
302//! ```
303//!
304//! ## Using Pact plugins
305//!
306//! The consumer test builders support using Pact plugins. Plugins are defined in the [Pact plugins project](https://github.com/pact-foundation/pact-plugins).
307//! To use plugins requires the use of Pact specification V4 Pacts.
308//!
309//! To use a plugin, first you need to let the builder know to load the plugin and then configure the interaction based on
310//! the requirements for the plugin. Each plugin may have different requirements, so you will have to consult the plugin
311//! docs on what is required. The plugins will be loaded from the plugin directory. By default, this is `~/.pact/plugins` or
312//! the value of the `PACT_PLUGIN_DIR` environment variable.
313//!
314//! There are generic functions that take JSON data structures and pass these on to the plugin to
315//! setup the interaction. For request/response HTTP interactions, there is the `contents` function on the request and
316//! response builders. For message interactions, the function is called `contents_from`.
317//!
318//! For example, if we use the CSV plugin from the plugins project, our test would look like:
319//!
320//! ```no_run
321//! use expectest::prelude::*;
322//! use regex::Regex;
323//! use pact_consumer::prelude::*;
324//! #[tokio::test]
325//! async fn test_csv_client() {
326//!     // Create a new V4 Pact
327//!     let csv_service = PactBuilder::new_v4("CsvClient", "CsvServer")
328//!     // Tell the builder we are using the CSV plugin
329//!     .using_plugin("csv", None).await
330//!     // Add the interaction for the CSV request
331//!     .interaction("request for a CSV report", "core/interaction/http", |mut i| async move {
332//!         // Path to the request we are going to make
333//!         i.request.path("/reports/report001.csv");
334//!         // Response we expect back
335//!         i.response
336//!           .ok()
337//!           // We use the generic "contents" function to send the expected response data to the plugin in JSON format
338//!           .contents(ContentType::from("text/csv"), json!({
339//!             "csvHeaders": false,
340//!             "column:1": "matching(type,'Name')",
341//!             "column:2": "matching(number,100)",
342//!             "column:3": "matching(datetime, 'yyyy-MM-dd','2000-01-01')"
343//!           })).await;
344//!         i.clone()
345//!     })
346//!     .await
347//!     // Now start the mock server
348//!     .start_mock_server_async(None, None)
349//!     .await;
350//!
351//!     // Now we can make our actual request for the CSV file and validate the response
352//!     let client = CsvClient::new(csv_service.url().clone());
353//!     let data = client.fetch("report001.csv").await.unwrap();
354//!
355//!     let columns: Vec<&str> = data.trim().split(",").collect();
356//!     expect!(columns.get(0)).to(be_some().value(&"Name"));
357//!     expect!(columns.get(1)).to(be_some().value(&"100"));
358//!     let date = columns.get(2).unwrap();
359//!     let re = Regex::new("\\d{4}-\\d{2}-\\d{2}").unwrap();
360//!     expect!(re.is_match(date)).to(be_true());
361//! }
362//! ```
363//!
364//! ## More Info
365//!
366//! For more advice on writing good pacts, see [Best Practices][].
367//!
368//! [Best Practices]: https://docs.pact.io/best_practices/consumer.html
369#![warn(missing_docs)]
370#[doc = include_str!("../README.md")]
371
372// Child modules which define macros (must be first because macros are resolved)
373// in source inclusion order).
374#[macro_use]
375pub mod patterns;
376#[cfg(test)]
377#[macro_use]
378mod test_support;
379
380// Other child modules.
381pub mod builders;
382pub mod mock_server;
383pub mod util;
384
385/// A "prelude" or a default list of import types to include. This includes
386/// the basic DSL, but it avoids including rarely-used types.
387///
388/// ```
389/// use pact_consumer::prelude::*;
390/// ```
391pub mod prelude {
392    pub use crate::{
393        like,
394        each_like,
395        each_like_helper,
396        term,
397        json_pattern,
398        json_pattern_internal
399    };
400    pub use crate::builders::{HttpPartBuilder, PactBuilder, PactBuilderAsync};
401    #[cfg(feature = "plugins")] pub use crate::builders::plugin_builder::PluginInteractionBuilder;
402    pub use crate::mock_server::{StartMockServer, ValidatingMockServer};
403    pub use crate::patterns::{
404        EachLike,
405        Like,
406        Term,
407        ObjectMatching,
408        EachKey,
409        EachValue,
410        JsonPattern,
411        Pattern,
412        StringPattern,
413        each_key,
414        each_value
415    };
416    #[cfg(feature = "datetime")] pub use crate::patterns::{DateTime};
417    pub use crate::util::strip_null_fields;
418    pub use pact_mock_server::mock_server::MockServerConfig;
419}
420
421/// Consumer version
422pub const PACT_CONSUMER_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");