1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
#![deny(missing_docs)]

//! Black-box integration test for REST APIs in Rust.
//!
//! `restest` provides primitives that allow to write REST API in a declarative
//! manner. It leverages the Rust test framework and uses macro-assisted pattern
//! tho assert for a pattern and add specified variables to scope.
//!
//! # Adding to the `Cargo.toml`
//!
//! `restest` provides test-only code. As such, it can be added as a
//! dev-dependency:
//!
#![doc = dep_doc::dev_dep_doc!()]
//!
//! # Example
//!
//! ```no_run
//! use restest::{assert_body_matches, path, Context, Request};
//!
//! use serde::{Deserialize, Serialize};
//! use http::StatusCode;
//!
//! const CONTEXT: Context = Context::new().with_port(8080);
//!
//! # #[tokio::main]
//! # async fn main() {
//! let request = Request::post(path!["user/ghopper"]).with_body(PostUser {
//!     year_of_birth: 1943,
//! });
//!
//! let body = CONTEXT
//!     .run(request)
//!     .await
//!     .expect_status(StatusCode::OK)
//!     .await;
//!
//! assert_body_matches! {
//!     body,
//!     User {
//!         year_of_birth: 1943,
//!         ..
//!     }
//! }
//! # }
//!
//! #[derive(Debug, Serialize)]
//! struct PostUser {
//!     year_of_birth: usize,
//! }
//!
//! #[derive(Debug, Deserialize)]
//! struct User {
//!     year_of_birth: usize,
//!     id: Uuid
//! }
//! # #[derive(Debug, Deserialize)]
//! # struct Uuid;
//! ```
//!
//! # Writing tests
//!
//! Writing tests using `restest` always follow the same patterns. They are
//! described below.
//!
//! ## Specifying the context
//!
//! The [`Context`] object handles all the server-specific configuration:
//!   - which base URL should be used,
//!   - which port should be used.
//!
//! It can be created with [`Context::new`]. All its setters are `const`, so
//! it can be initialized once for all the tests of a module:
//!
//! ```rust
//! use restest::Context;
//!
//! const CONTEXT: Context = Context::new().with_port(8080);
//!
//! #[tokio::test]
//! async fn test_first_route() {
//!     // Test code that use `CONTEXT` for a specific route
//! }
//!
//! #[tokio::test]
//! async fn test_second_route() {
//!     // Test code that use `CONTEXT` again for another route
//! }
//! ```
//!
//! As we're running `async` code under the hood, all the tests must be `async`,
//! hence the use of `tokio::test`
//!
//! # Creating a request
//!
//! Let's focus on the test function itself.
//!
//! The first thing to do is to create a [`Request`] object. This object allows
//! to specify characteristics about a specific request that is performed later.
//!
//! Running [`Request::get`] allows to construct a GET request to a specific
//! URL. Header keys can be specified by calling the
//! [`with_header`](request::RequestBuilder::with_header) method. The
//! final [`Request`] is built by calling the
//! [`with_body`](request::RequestBuilder::with_body) method, which
//! allows to add a body.
//!
//! ```rust
//! use restest::{path, Request};
//!
//! let request = Request::get(path!["users", "scrabsha"])
//!     .with_header("token", "mom-said-yes")
//!     .with_body(());
//! ```
//!
//! Similarly, POST requests can be creating by using [`Request::post`] instead
//! of [`Request::get`].
//!
//! # Performing the request
//!
//! Once that the [`Request`] object has been created, we can run the request
//! by passing the [`Request`] to the [`Context`] when calling [`Context::run`].
//! Once `await`-ed, the [`expect_status`](request::RequestResult::expect_status) method
//! checks for the request status code and converts the response body to the
//! expected output type.
//!
//! ```rust,no_run
//! use http::StatusCode;
//! use uuid::Uuid;
//! use serde::Deserialize;
//!
//! # use restest::{Context, Request};
//! # const CONTEXT: Context = Context::new();
//! # #[tokio::main]
//! # async fn main() {
//! # let request = Request::get("foo").with_body(());
//! let user: User = CONTEXT
//!     .run(request)
//!     .await
//!     .expect_status(StatusCode::OK)
//!     .await;
//! # }
//!
//! #[derive(Deserialize)]
//! struct User {
//!     name: String,
//!     age: u8,
//!     id: Uuid,
//! }
//! ```
//!
//! # Checking the response body
//!
//! Properties about the response body can be asserted with
//! [`assert_body_matches`]. The macro supports the full rust pattern syntax,
//! making it easy to check for expected values and variants. It also provides
//! bindings, allowing you to bring data from the body in scope:
//!
//! ```rust
//! use restest::assert_body_matches;
//! # use uuid::Uuid;
//!
//! # let user = User {
//! #     name: "Grace Hopper".to_string(),
//! #     age: 85,
//! #     id: Uuid::new_v4(),
//! # };
//! #
//! assert_body_matches! {
//!     user,
//!     User {
//!         name: "Grace Hopper",
//!         age: 85,
//!         id,
//!     },
//! }
//!
//! // id is now a variable that can be used:
//! println!("Grace Hopper has id `{}`", id);
//! #
//! # #[derive(serde::Deserialize)]
//! # struct User {
//! #     name: String,
//! #     age: u8,
//! #     id: Uuid,
//! # }
//! ```
//!
//! The extracted variable can be used for next requests or more complex
//! testing.
//!
//! *And that's it!*

/// Asserts that a response body matches a given pattern, adds
/// bindings to the current scope.
///
/// This pattern supports all the Rust pattern syntax, with a few additions:
///   - matching on [`String`] can be done with string literals,
///   - matching on [`Vec`] can be done using slice patterns,
///   - values that are bound to variables are available in the whole scope,
///     allowing for later use.
///
/// # Panics
///
/// This macro will panic if the body does not match the provided pattern.
///
/// # Example
///
/// The following code demonstrate basic matching:
///
/// ```rust,no_run
/// use restest::assert_body_matches;
///
/// struct User {
///     name: String,
///     age: u8,
/// }
///
/// let user = get_user();
///
/// assert_body_matches! {
///     user,
///     User {
///         name: "John Doe",
///         age: 48,
///     },
/// }
///
/// fn get_user() -> User {
///     /* Obscure code */
/// # User {
/// #     name: "John Doe".to_string(),
/// #     age: 48,
/// # }
/// }
/// ```
///
/// Values can be brought to scope:
///
/// ```rust
/// use restest::assert_body_matches;
/// use uuid::Uuid;
///
/// struct User {
///     id: Uuid,
///     name: String,
/// }
///
/// let user = get_user();
///
/// assert_body_matches! {
///     user,
///     User {
///         id,
///         name: "John Doe",
///     },
/// }
///
/// // id is now available:
/// println!("John Doe has id `{}`", id);
///
/// fn get_user() -> User {
///     /* Obscure code */
/// #    User {
/// #        id: Uuid::new_v4(),
/// #        name: "John Doe".to_string(),
/// #    }
/// }
/// ```
///
/// Bringing values to scope may allow to extract information that are required
/// to perform a next request.
pub use restest_macros::assert_body_matches;

pub mod context;
pub mod request;
mod url;

pub use context::Context;
pub use request::Request;

/// Creates a path from multiple segments.
///
/// All the segments don't need to have the same type. They all need to
/// implement [`ToString`].
///
/// # Example
///
/// ```rust
/// use restest::{path, Request};
/// use uuid::Uuid;
///
/// let my_user_id = Uuid::new_v4();
///
/// Request::get(path!["users", my_user_id])
///     .with_body(())
///     // the rest of the request
/// #   ;
/// ```
#[macro_export]
macro_rules! path {
    ( $( $segment:expr ),* $(,)? ) => {
        vec![ $( Box::new($segment) as Box<dyn ToString>, )* ]
    };
}