diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..8969614
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,2 @@
+[alias]
+clippy-mockito = "clippy --lib --tests --all-features"
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
new file mode 100644
index 0000000..29abeef
--- /dev/null
+++ b/.github/workflows/linters.yml
@@ -0,0 +1,37 @@
+name: Linters
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - "*"
+
+jobs:
+ rustfmt:
+ name: Run rustfmt on the minimum supported toolchain
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: 1.65.0
+ profile: minimal
+ components: clippy, rustfmt
+ override: true
+ - name: Run rustfmt
+ run: cargo fmt -- --check
+ clippy:
+ name: Run clippy on the minimum supported toolchain
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: 1.65.0
+ profile: minimal
+ components: clippy, rustfmt
+ override: true
+ - name: Run clippy
+ run: cargo clippy-mockito
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..93295ee
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,56 @@
+name: Tests
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - "*"
+
+jobs:
+ test-default:
+ name: Test the minimum supported toolchain
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: 1.65.0
+ profile: minimal
+ override: true
+ - name: Check
+ run: cargo check
+ - name: Test
+ run: cargo test --no-default-features
+
+ test-latest:
+ name: Test on latest stable
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ profile: minimal
+ override: true
+ - name: Check
+ run: cargo check
+ - name: Test
+ run: cargo test --no-default-features
+
+ test-nightly:
+ name: Test on nightly
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: nightly
+ profile: minimal
+ override: true
+ - name: Check
+ run: cargo check
+ - name: Test
+ run: cargo test --no-default-features
+
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 09525f7..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-language: rust
-rust:
- - 1.42.0 # minimum supported toolchain
- - stable
- - beta
- - nightly
-os: linux
-
-jobs:
- fast_finish: true
- allow_failures:
- - rust: nightly
- include:
- - name: Lints
- rust: 1.42.0
- install:
- - rustup component add clippy
- - rustup component add rustfmt
- script:
- - cargo clippy --lib --tests --all-features -- -D clippy::complexity
- - cargo fmt -- --check
-
-branches:
- only:
- - master
-
-script:
- - cargo test
-
-notifications:
- email:
- on_success: never
diff --git a/Cargo.toml b/Cargo.toml
index e9d89ce..3f7b5f7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,27 +10,30 @@ description = "HTTP mocking for Rust."
keywords = ["mock", "mocks", "http", "webmock", "webmocks"]
categories = ["development-tools::testing", "web-programming"]
exclude = ["/.appveyor.yml", "/.travis.yml", "/benchmarks.txt", "/docs/", "/slides.pdf"]
-edition = "2018"
+edition = "2021"
[badges]
travis-ci = { repository = "lipanski/mockito", branch = "master" }
appveyor = { repository = "lipanski/mockito", branch = "master", service = "github" }
[dependencies]
+assert-json-diff = "2.0"
+async-trait = "0.1"
+colored = { version = "2.0", optional = true }
+deadpool = "0.9"
+futures = "0.3"
+hyper = { version = "0.14", features = ["full"] }
+lazy_static = "1.4"
+log = "0.4"
rand = "0.8"
-httparse = "1.3.3"
-regex = "1.0.5"
-lazy_static = "1.1.0"
-serde_json = "1.0.17"
-similar = "2.1"
-
-colored = { version = "2.0.0", optional = true }
-log = "0.4.6"
-assert-json-diff = "2.0.0"
-serde_urlencoded = "0.7.0"
+regex = "1.7"
+serde_json = "1.0"
+serde_urlencoded = "0.7"
+similar = "2.2"
+tokio = { version = "1.25", features = ["full"] }
[dev-dependencies]
-env_logger = "0.8.2"
+env_logger = "0.8"
testing_logger = "0.1"
[features]
diff --git a/README.md b/README.md
index 4a7e25f..b022705 100644
--- a/README.md
+++ b/README.md
@@ -3,20 +3,117 @@
-
+
-
+
HTTP mocking for Rust!
-Get it on [crates.io](https://crates.io/crates/mockito/).
+Mockito is a library for **generating and delivering HTTP mocks** in Rust. You can use it for integration testing
+or offline work. Mockito runs a local pool of HTTP servers which create, deliver and remove the mocks.
-Documentation is available at .
+## Features
+
+- Support for HTTP1/2
+- Multi-threaded
+- Various request matchers (Regex, JSON etc.)
+- Mock multiple hosts at the same time
+- Sync and async interface
+- Simple, intuitive API
+- An awesome logo
+
+The full documentation is available at .
Before upgrading, make sure to check out the [changelog](https://github.com/lipanski/mockito/releases).
+## Getting Started
+
+Add `mockito` to your `Cargo.toml` and start mocking:
+
+```rust
+#[test]
+fn test_something() {
+ // Request a new server from the pool
+ let mut server = mockito::Server::new();
+
+ // Use one of these addresses to configure your client
+ let host = server.host_with_port();
+ let url = server.url();
+
+ // Create a mock
+ let m = server.mock("GET", "/hello")
+ .with_status(201)
+ .with_header("content-type", "text/plain")
+ .with_header("x-api-key", "1234")
+ .with_body("world")
+ .create();
+
+ // Any calls to GET /hello beyond this line will respond with 201, the
+ // `content-type: text/plain` header and the body "world".
+
+ // You can use `Mock::assert` to verify that your mock was called
+ m.assert();
+}
+```
+
+Use **matchers** to handle requests to the same endpoint in a different way:
+
+```rust
+#[test]
+fn test_something() {
+ let mut server = mockito::Server::new();
+
+ let m1 = server.mock("GET", "/greetings")
+ .match_header("content-type", "application/json")
+ .match_body(mockito::Matcher::PartialJsonString(
+ "{\"greeting\": \"hello\"}".to_string(),
+ ))
+ .with_body("hello json")
+ .create();
+
+ let m2 = server.mock("GET", "/greetings")
+ .match_header("content-type", "application/text")
+ .match_body(mockito::Matcher::Regex("greeting=hello".to_string()))
+ .with_body("hello text")
+ .create();
+}
+```
+
+Start **multiple servers** to simulate requests to different hosts:
+
+```rust
+#[test]
+fn test_something() {
+ let mut twitter = mockito::Server::new();
+ let mut github = mockito::Server::new();
+
+ // These mocks will be available at `twitter.url()`
+ let twitter_mock = twitter.mock("GET", "/api").create();
+
+ // These mocks will be available at `github.url()`
+ let github_mock = github.mock("GET", "/api").create();
+}
+```
+
+Write **async** tests:
+
+```rust
+#[tokio::test(flavor = "multi_thread")]
+async fn test_simple_route_mock_async() {
+ let mut server = Server::new_async().await;
+ let _m1 = server.mock("GET", "/a").with_body("aaa").create_async();
+ let _m2 = server.mock("GET", "/b").with_body("bbb").create_async();
+
+ let (_m1, _m2) = futures::join!(_m1, _m2);
+}
+```
+
+## Minimum supported Rust toolchain
+
+The current minimum support Rust toolchain is **1.65.0**
+
## Contribution Guidelines
1. Check the existing issues and pull requests.
@@ -37,7 +134,7 @@ cargo test
...or run tests using a different toolchain:
```sh
-rustup run --install 1.42.0 cargo test
+rustup run --install 1.65.0 cargo test
```
...or run tests while disabling the default features (e.g. the colors):
@@ -71,13 +168,13 @@ Mockito uses [clippy](https://github.com/rust-lang/rust-clippy) and it should be
Install `clippy`:
```sh
-rustup component add clippy-preview
+rustup component add clippy
```
-Run the linter on the minimum supported Rust version:
+The linter is always run on the minimum supported Rust version:
```sh
-rustup run --install 1.42.0 cargo clippy --lib --tests --all-features -- -D clippy::complexity
+rustup run --install 1.65.0 cargo clippy-mockito
```
### Release
@@ -101,7 +198,3 @@ Run benchmarks:
```sh
rustup run nightly cargo bench
```
-
----
-
-Logo courtesy to [http://niastudio.net](http://niastudio.net) :ok_hand:
diff --git a/benches/lib.rs b/benches/lib.rs
index 4e62f5e..1225b37 100644
--- a/benches/lib.rs
+++ b/benches/lib.rs
@@ -2,14 +2,15 @@
extern crate test;
-use mockito::{mock, reset, server_address};
+use mockito::Server;
+use std::fmt::Display;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use std::str::FromStr;
use test::Bencher;
-fn request_stream(route: &str, headers: &str) -> TcpStream {
- let mut stream = TcpStream::connect(server_address()).unwrap();
+fn request_stream(host: impl Display, route: &str, headers: &str) -> TcpStream {
+ let mut stream = TcpStream::connect(host.to_string()).unwrap();
let message = [route, " HTTP/1.1\r\n", headers, "\r\n"].join("");
stream.write_all(message.as_bytes()).unwrap();
@@ -49,27 +50,27 @@ fn parse_stream(stream: TcpStream) -> (String, Vec, String) {
(status_line, headers, body)
}
-fn request(route: &str, headers: &str) -> (String, Vec, String) {
- parse_stream(request_stream(route, headers))
+fn request(host: impl Display, route: &str, headers: &str) -> (String, Vec, String) {
+ parse_stream(request_stream(host, route, headers))
}
#[bench]
fn bench_create_simple_mock(b: &mut Bencher) {
- reset();
+ let mut s = Server::new();
b.iter(|| {
- let _m = mock("GET", "/").with_body("test").create();
+ let _m = s.mock("GET", "/").with_body("test").create();
})
}
#[bench]
fn bench_match_simple_mock(b: &mut Bencher) {
- reset();
+ let mut s = Server::new();
- let _m = mock("GET", "/").with_body("test").create();
+ let _m = s.mock("GET", "/").with_body("test").create();
b.iter(|| {
- let (status_line, _, _) = request("GET /", "");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "");
assert!(status_line.starts_with("HTTP/1.1 200"));
})
}
diff --git a/examples/mockito-server.rs b/examples/mockito-server.rs
index 857f34b..0dc5361 100644
--- a/examples/mockito-server.rs
+++ b/examples/mockito-server.rs
@@ -3,7 +3,9 @@ use mockito;
use std::time::Duration;
fn main() {
- mockito::start();
+ let mut s = mockito::Server::new();
+
+ s.mock("GET", "/").with_body("hello world");
loop {
std::thread::sleep(Duration::from_secs(1))
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..f42c8b3
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,2 @@
+edition = "2021"
+max_width = 100
diff --git a/src/command.rs b/src/command.rs
new file mode 100644
index 0000000..f6d6a32
--- /dev/null
+++ b/src/command.rs
@@ -0,0 +1,119 @@
+use crate::server::{RemoteMock, State};
+use tokio::sync::mpsc::Sender;
+use tokio::sync::oneshot;
+use tokio::sync::MutexGuard;
+
+#[derive(Debug)]
+pub(crate) enum Command {
+ CreateMock {
+ remote_mock: RemoteMock,
+ response_sender: oneshot::Sender,
+ },
+ GetMockHits {
+ mock_id: String,
+ response_sender: oneshot::Sender>,
+ },
+ RemoveMock {
+ mock_id: String,
+ response_sender: oneshot::Sender,
+ },
+ GetLastUnmatchedRequest {
+ response_sender: oneshot::Sender>,
+ },
+}
+
+impl Command {
+ pub(crate) async fn create_mock(sender: &Sender, remote_mock: RemoteMock) -> bool {
+ let (response_sender, response_receiver) = oneshot::channel();
+
+ let cmd = Command::CreateMock {
+ remote_mock,
+ response_sender,
+ };
+
+ let _ = sender.send(cmd).await;
+ response_receiver.await.unwrap_or(false)
+ }
+
+ pub(crate) async fn get_mock_hits(sender: &Sender, mock_id: String) -> Option {
+ let (response_sender, response_receiver) = oneshot::channel();
+
+ let cmd = Command::GetMockHits {
+ mock_id,
+ response_sender,
+ };
+
+ let _ = sender.send(cmd).await;
+ response_receiver.await.unwrap()
+ }
+
+ pub(crate) async fn remove_mock(sender: &Sender, mock_id: String) -> bool {
+ let (response_sender, response_receiver) = oneshot::channel();
+
+ let cmd = Command::RemoveMock {
+ mock_id,
+ response_sender,
+ };
+
+ let _ = sender.send(cmd).await;
+ response_receiver.await.unwrap_or(false)
+ }
+
+ pub(crate) async fn get_last_unmatched_request(sender: &Sender) -> Option {
+ let (response_sender, response_receiver) = oneshot::channel();
+
+ let cmd = Command::GetLastUnmatchedRequest { response_sender };
+
+ let _ = sender.send(cmd).await;
+ response_receiver.await.unwrap_or(None)
+ }
+
+ pub async fn handle(cmd: Command, mut state: MutexGuard<'_, State>) {
+ match cmd {
+ Command::CreateMock {
+ remote_mock,
+ response_sender,
+ } => {
+ state.mocks.push(remote_mock);
+
+ let _ = response_sender.send(true);
+ }
+ Command::GetMockHits {
+ mock_id,
+ response_sender,
+ } => {
+ let hits: Option = state
+ .mocks
+ .iter()
+ .find(|remote_mock| remote_mock.inner.id == mock_id)
+ .map(|remote_mock| remote_mock.inner.hits);
+
+ let _ = response_sender.send(hits);
+ }
+ Command::RemoveMock {
+ mock_id,
+ response_sender,
+ } => {
+ if let Some(pos) = state
+ .mocks
+ .iter()
+ .position(|remote_mock| remote_mock.inner.id == mock_id)
+ {
+ state.mocks.remove(pos);
+ }
+
+ let _ = response_sender.send(true);
+ }
+ Command::GetLastUnmatchedRequest { response_sender } => {
+ let last_unmatched_request = state.unmatched_requests.last_mut();
+
+ let label = match last_unmatched_request {
+ Some(req) => Some(req.to_string().await),
+ None => None,
+ };
+
+ let _ = response_sender.send(label);
+ }
+ }
+ }
+}
diff --git a/src/diff.rs b/src/diff.rs
index b1eab1d..540e81e 100644
--- a/src/diff.rs
+++ b/src/diff.rs
@@ -22,6 +22,7 @@ pub fn compare(expected: &str, actual: &str) -> String {
ChangeTag::Equal => {
let z = change.value();
#[cfg(feature = "color")]
+ #[allow(clippy::unnecessary_to_owned)]
result.push_str(&z.green().to_string());
#[cfg(not(feature = "color"))]
result.push_str(z);
@@ -29,6 +30,7 @@ pub fn compare(expected: &str, actual: &str) -> String {
ChangeTag::Insert => {
let z = change.value();
#[cfg(feature = "color")]
+ #[allow(clippy::unnecessary_to_owned)]
result.push_str(&z.white().on_green().to_string());
#[cfg(not(feature = "color"))]
result.push_str(z);
@@ -38,6 +40,7 @@ pub fn compare(expected: &str, actual: &str) -> String {
}
} else {
#[cfg(feature = "color")]
+ #[allow(clippy::unnecessary_to_owned)]
result.push_str(&x.bright_green().to_string());
#[cfg(not(feature = "color"))]
result.push_str(x);
@@ -45,6 +48,7 @@ pub fn compare(expected: &str, actual: &str) -> String {
}
ChangeTag::Delete => {
#[cfg(feature = "color")]
+ #[allow(clippy::unnecessary_to_owned)]
result.push_str(&x.red().to_string());
#[cfg(not(feature = "color"))]
result.push_str(x);
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..049090c
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,74 @@
+use std::error::Error as ErrorTrait;
+use std::fmt::Display;
+
+///
+/// Contains information about an error occurence
+///
+#[derive(Debug)]
+pub struct Error {
+ /// The type of this error
+ pub kind: ErrorKind,
+ /// Some errors come with more context
+ pub context: Option,
+}
+
+impl Error {
+ pub(crate) fn new(kind: ErrorKind) -> Error {
+ Error {
+ kind,
+ context: None,
+ }
+ }
+
+ pub(crate) fn new_with_context(kind: ErrorKind, context: impl Display) -> Error {
+ Error {
+ kind,
+ context: Some(context.to_string()),
+ }
+ }
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{} (context: {})",
+ self.kind.description(),
+ self.context.as_ref().unwrap_or(&"none".to_string())
+ )
+ }
+}
+
+impl ErrorTrait for Error {}
+
+///
+/// The type of an error
+///
+#[derive(Debug)]
+pub enum ErrorKind {
+ /// The server is not running
+ ServerFailure,
+ /// Could not deliver a response
+ ResponseFailure,
+ /// The status code is invalid or out of range
+ InvalidStatusCode,
+ /// Failed to read the request body
+ RequestBodyFailure,
+ /// Failed to write the response body
+ ResponseBodyFailure,
+ /// File not found
+ FileNotFound,
+}
+
+impl ErrorKind {
+ fn description(&self) -> &'static str {
+ match self {
+ ErrorKind::ServerFailure => "the server is not running",
+ ErrorKind::ResponseFailure => "could not deliver a response",
+ ErrorKind::InvalidStatusCode => "invalid status code",
+ ErrorKind::RequestBodyFailure => "failed to read the request body",
+ ErrorKind::ResponseBodyFailure => "failed to write the response body",
+ ErrorKind::FileNotFound => "file not found",
+ }
+ }
+}
diff --git a/src/legacy.rs b/src/legacy.rs
new file mode 100644
index 0000000..3887a78
--- /dev/null
+++ b/src/legacy.rs
@@ -0,0 +1,95 @@
+use crate::{Matcher, Mock, Server};
+use lazy_static::lazy_static;
+use std::cell::RefCell;
+use std::sync::LockResult;
+use std::sync::{Mutex, MutexGuard};
+lazy_static! {
+ // Legacy mode.
+ // A global lock that ensure all Mockito tests are run on a single thread.
+ static ref TEST_MUTEX: Mutex<()> = Mutex::new(());
+
+ // Legacy mode.
+ static ref DEFAULT_SERVER: Mutex = Mutex::new(Server::new_with_port(0));
+}
+thread_local!(
+ // Legacy mode.
+ // A thread-local reference to the global lock. This is acquired within `mock()`.
+ pub(crate) static LOCAL_TEST_MUTEX: RefCell>> =
+ RefCell::new(TEST_MUTEX.lock());
+);
+
+///
+/// **DEPRECATED:** This method is part of the legacy interface an will be removed
+/// in future versions. You should replace it with `Server::mock`:
+///
+/// ```
+/// let mut s = mockito::Server::new();
+/// let _m1 = s.mock("GET", "/");
+/// ```
+///
+/// Initializes a mock with the given HTTP `method` and `path`.
+///
+/// The mock is registered to the server only after the `create()` method has been called.
+///
+#[deprecated(since = "0.32.0", note = "Use `Server::mock` instead")]
+pub fn mock>(method: &str, path: P) -> Mock {
+ // Legacy mode.
+ // Ensures Mockito tests are run sequentially.
+ LOCAL_TEST_MUTEX.with(|_| {});
+
+ let mut server = DEFAULT_SERVER.lock().unwrap();
+
+ server.mock(method, path)
+}
+
+///
+/// **DEPRECATED:** This method is part of the legacy interface an will be removed
+/// in future versions. You should replace it with `Server::host_with_port`:
+///
+/// ```
+/// let mut s = mockito::Server::new();
+/// let server_address = s.host_with_port();
+/// ```
+///
+/// The host and port of the local server.
+/// Can be used with `std::net::TcpStream`.
+///
+#[deprecated(since = "0.32.0", note = "Use `Server::host_with_port` instead")]
+pub fn server_address() -> String {
+ let server = DEFAULT_SERVER.lock().unwrap();
+ server.host_with_port()
+}
+
+///
+/// **DEPRECATED:** This method is part of the legacy interface an will be removed
+/// in future versions. You should replace it with `Server::url`:
+///
+/// ```
+/// let mut s = mockito::Server::new();
+/// let server_url = s.url();
+/// ```
+///
+/// The local `http://...` URL of the server.
+///
+#[deprecated(since = "0.32.0", note = "Use `Server::url` instead")]
+pub fn server_url() -> String {
+ let server = DEFAULT_SERVER.lock().unwrap();
+ server.url()
+}
+
+///
+/// **DEPRECATED:** This method is part of the legacy interface an will be removed
+/// in future versions. You should replace it with `Server::reset`:
+///
+/// ```
+/// let mut s = mockito::Server::new();
+/// s.reset();
+/// ```
+///
+/// Removes all the mocks stored on the server.
+///
+#[deprecated(since = "0.32.0", note = "Use `Server::reset` instead")]
+pub fn reset() {
+ let mut server = DEFAULT_SERVER.lock().unwrap();
+ server.reset();
+}
diff --git a/src/lib.rs b/src/lib.rs
index 4939865..06ee921 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,46 +4,37 @@
)]
//!
-//! Mockito is a library for creating HTTP mocks to be used in integration tests or for offline work.
-//! It runs an HTTP server on a local port which delivers, creates and remove the mocks.
+//! Mockito is a library for **generating and delivering HTTP mocks** in Rust. You can use it for integration testing
+//! or offline work. Mockito runs a local pool of HTTP servers which create, deliver and remove the mocks.
//!
-//! The server is run on a separate thread within the same process and will be removed
-//! at the end of the run.
+//! # Features
//!
-//! # Getting Started
-//!
-//! Use `mockito::server_url()` or `mockito::server_address()` as the base URL for any mocked
-//! client in your tests. One way to do this is by using compiler flags:
-//!
-//! ## Example
-//!
-//! ```
-//! #[cfg(test)]
-//! use mockito;
-//!
-//! fn main() {
-//! #[cfg(not(test))]
-//! let url = "https://api.twitter.com";
+//! - Support for HTTP1/2
+//! - Multi-threaded
+//! - Various request matchers (Regex, JSON etc.)
+//! - Mock multiple hosts at the same time
+//! - Sync and async interface
+//! - Simple, intuitive API
+//! - An awesome logo
//!
-//! #[cfg(test)]
-//! let url = &mockito::server_url();
-//!
-//! // Use url as the base URL for your client
-//! }
-//! ```
-//!
-//! Then start mocking:
+//! # Getting Started
//!
-//! ## Example
+//! Add `mockito` to your `Cargo.toml` and start mocking:
//!
//! ```
//! #[cfg(test)]
//! mod tests {
-//! use mockito::mock;
-//!
//! #[test]
//! fn test_something() {
-//! let _m = mock("GET", "/hello")
+//! // Request a new server from the pool
+//! let mut server = mockito::Server::new();
+//!
+//! // Use one of these addresses to configure your client
+//! let host = server.host_with_port();
+//! let url = server.url();
+//!
+//! // Create a mock
+//! let _m = server.mock("GET", "/hello")
//! .with_status(201)
//! .with_header("content-type", "text/plain")
//! .with_header("x-api-key", "1234")
@@ -52,215 +43,105 @@
//!
//! // Any calls to GET /hello beyond this line will respond with 201, the
//! // `content-type: text/plain` header and the body "world".
+//!
+//! // You can use `Mock::assert` to verify that your mock was called
+//! // m.assert();
//! }
//! }
//! ```
//!
-//! # Lifetime
-//!
-//! Just like any Rust object, a mock is available only through its lifetime. You'll want to assign
-//! the mocks to variables in order to extend and control their lifetime.
-//!
-//! Avoid using the underscore matcher when creating your mocks, as in `let _ = mock("GET", "/")`.
-//! This will end your mock's lifetime immediately. You can still use the underscore to prefix your variable
-//! names in an assignment, but don't limit it to just this one character.
-//!
-//! ## Example
+//! Use **matchers** to handle requests to the same endpoint in a different way:
//!
//! ```
-//! use mockito::mock;
-//!
-//! let _m1 = mock("GET", "/long").with_body("hello").create();
-//!
-//! {
-//! let _m2 = mock("GET", "/short").with_body("hi").create();
-//!
-//! // Requests to GET /short will be mocked til here
-//! }
-//!
-//! // Requests to GET /long will be mocked til here
-//! ```
-//!
-//! # Limitations
-//!
-//! Creating mocks from threads is currently not possible. Please use the main (test) thread for that.
-//! See the note on threads at the end for more details.
-//!
-//! # Asserts
-//!
-//! You can use the `Mock::assert` method to **assert that a mock was called**. In other words, the
-//! `Mock#assert` method can validate that your code performed the expected HTTP requests.
-//!
-//! By default, the method expects that only one request to your mock was triggered.
-//!
-//! ## Example
-//!
-//! ```no_run
-//! use std::net::TcpStream;
-//! use std::io::{Read, Write};
-//! use mockito::{mock, server_address};
-//!
-//! let mock = mock("GET", "/hello").create();
+//! #[cfg(test)]
+//! mod tests {
+//! #[test]
+//! fn test_something() {
+//! let mut server = mockito::Server::new();
+//!
+//! let m1 = server.mock("GET", "/greetings")
+//! .match_header("content-type", "application/json")
+//! .match_body(mockito::Matcher::PartialJsonString(
+//! "{\"greeting\": \"hello\"}".to_string(),
+//! ))
+//! .with_body("hello json")
+//! .create();
//!
-//! {
-//! // Place a request
-//! let mut stream = TcpStream::connect(server_address()).unwrap();
-//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
-//! let mut response = String::new();
-//! stream.read_to_string(&mut response).unwrap();
-//! stream.flush().unwrap();
+//! let m2 = server.mock("GET", "/greetings")
+//! .match_header("content-type", "application/text")
+//! .match_body(mockito::Matcher::Regex("greeting=hello".to_string()))
+//! .with_body("hello text")
+//! .create();
+//! }
//! }
-//!
-//! mock.assert();
//! ```
//!
-//! When several mocks can match a request, Mockito applies the first one that still expects requests.
-//! You can use this behaviour to provide **different responses for subsequent requests to the same endpoint**.
-//!
-//! ## Example
+//! Start **multiple servers** to simulate requests to different hosts:
//!
//! ```
-//! use std::net::TcpStream;
-//! use std::io::{Read, Write};
-//! use mockito::{mock, server_address};
-//!
-//! let english_hello_mock = mock("GET", "/hello").with_body("good bye").create();
-//! let french_hello_mock = mock("GET", "/hello").with_body("au revoir").create();
-//!
-//! {
-//! // Place a request to GET /hello
-//! let mut stream = TcpStream::connect(server_address()).unwrap();
-//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
-//! let mut response = String::new();
-//! stream.read_to_string(&mut response).unwrap();
-//! stream.flush().unwrap();
-//! }
+//! #[cfg(test)]
+//! mod tests {
+//! #[test]
+//! fn test_something() {
+//! let mut twitter = mockito::Server::new();
+//! let mut github = mockito::Server::new();
//!
-//! english_hello_mock.assert();
+//! // These mocks will be available at `twitter.url()`
+//! let twitter_mock = twitter.mock("GET", "/api").create();
//!
-//! {
-//! // Place another request to GET /hello
-//! let mut stream = TcpStream::connect(server_address()).unwrap();
-//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
-//! let mut response = String::new();
-//! stream.read_to_string(&mut response).unwrap();
-//! stream.flush().unwrap();
+//! // These mocks will be available at `github.url()`
+//! let github_mock = github.mock("GET", "/api").create();
+//! }
//! }
-//!
-//! french_hello_mock.assert();
//! ```
//!
-//! If you're expecting more than 1 request, you can use the `Mock::expect` method to specify the exact amount of requests:
-//!
-//! ## Example
-//!
-//! ```no_run
-//! use std::net::TcpStream;
-//! use std::io::{Read, Write};
-//! use mockito::{mock, server_address};
-//!
-//! let mock = mockito::mock("GET", "/hello").expect(3).create();
-//!
-//! for _ in 0..3 {
-//! // Place a request
-//! let mut stream = TcpStream::connect(server_address()).unwrap();
-//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
-//! let mut response = String::new();
-//! stream.read_to_string(&mut response).unwrap();
-//! stream.flush().unwrap();
-//! }
+//! Write **async** tests:
//!
-//! mock.assert();
//! ```
+//! #[cfg(test)]
+//! mod tests {
+//! #[tokio::test(flavor = "multi_thread")]
+//! async fn test_something() {
+//! let mut server = Server::new_async().await;
+//! let _m1 = server.mock("GET", "/a").with_body("aaa").create_async();
+//! let _m2 = server.mock("GET", "/b").with_body("bbb").create_async();
//!
-//! You can also work with ranges, by using the `Mock::expect_at_least` and `Mock::expect_at_most` methods:
-//!
-//! ## Example
-//!
-//! ```no_run
-//! use std::net::TcpStream;
-//! use std::io::{Read, Write};
-//! use mockito::{mock, server_address};
-//!
-//! let mock = mockito::mock("GET", "/hello").expect_at_least(2).expect_at_most(4).create();
-//!
-//! for _ in 0..3 {
-//! // Place a request
-//! let mut stream = TcpStream::connect(server_address()).unwrap();
-//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
-//! let mut response = String::new();
-//! stream.read_to_string(&mut response).unwrap();
-//! stream.flush().unwrap();
+//! let (_m1, _m2) = futures::join!(_m1, _m2);
+//! }
//! }
-//!
-//! mock.assert();
//! ```
//!
-//! The errors produced by the `assert` method contain information about the tested mock, but also about the
-//! **last unmatched request**, which can be very useful to track down an error in your implementation or
-//! a missing or incomplete mock. A colored diff is also displayed.
-//!
-//! Color output is enabled by default, but can be toggled with the `color` feature flag.
-//!
-//! Here's an example of how a `Mock#assert` error looks like:
-//!
-//! ```text
-//! > Expected 1 request(s) to:
-//!
-//! POST /users?number=one
-//! bob
-//!
-//! ...but received 0
-//!
-//! > The last unmatched request was:
-//!
-//! POST /users?number=two
-//! content-length: 5
-//! alice
-//!
-//! > Difference:
-//!
-//! # A colored diff
+//! # Lifetime
//!
-//! ```
+//! Just like any Rust object, a mock is available only through its lifetime. You'll want to assign
+//! the mocks to variables in order to extend and control their lifetime.
//!
-//! You can also use the `matched` method to return a boolean for whether the mock was called the
-//! correct number of times without panicking
+//! Avoid using the underscore matcher when creating your mocks, as in `let _ = mock("GET", "/")`.
+//! This will end your mock's lifetime immediately. You can still use the underscore to prefix your variable
+//! names in an assignment, but don't limit it to just this one character.
//!
//! ## Example
//!
//! ```
-//! use std::net::TcpStream;
-//! use std::io::{Read, Write};
-//! use mockito::{mock, server_address};
-//!
-//! let mock = mock("GET", "/").create();
+//! let mut s = mockito::Server::new();
+//! let _m1 = s.mock("GET", "/long").with_body("hello").create();
//!
//! {
-//! let mut stream = TcpStream::connect(server_address()).unwrap();
-//! stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
-//! let mut response = String::new();
-//! stream.read_to_string(&mut response).unwrap();
-//! stream.flush().unwrap();
-//! }
+//! let _m2 = s.mock("GET", "/short").with_body("hi").create();
//!
-//! assert!(mock.matched());
-//!
-//! {
-//! let mut stream = TcpStream::connect(server_address()).unwrap();
-//! stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
-//! let mut response = String::new();
-//! stream.read_to_string(&mut response).unwrap();
-//! stream.flush().unwrap();
+//! // Requests to GET /short will be mocked til here
//! }
-//! assert!(!mock.matched());
+//!
+//! // Requests to GET /long will be mocked til here
//! ```
//!
//! # Matchers
//!
//! Mockito can match your request by method, path, query, headers or body.
//!
-//! Various matchers are provided by the `Matcher` type: exact, partial (regular expressions), any or missing.
+//! Various matchers are provided by the `Matcher` type: exact (string, binary, JSON), partial (regular expressions,
+//! JSON), any or missing. The following guide will walk you through the most common matchers. Check the
+//! `Matcher` documentation for all the rest.
//!
//! # Matching by path
//!
@@ -269,10 +150,10 @@
//! ## Example
//!
//! ```
-//! use mockito::mock;
+//! let mut s = mockito::Server::new();
//!
//! // Matched only calls to GET /hello
-//! let _m = mock("GET", "/hello").create();
+//! let _m = s.mock("GET", "/hello").create();
//! ```
//!
//! You can also match the path partially, by using a regular expression:
@@ -280,10 +161,12 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
//! // Will match calls to GET /hello/1 and GET /hello/2
-//! let _m = mock("GET", Matcher::Regex(r"^/hello/(1|2)$".to_string())).create();
+//! let _m = s.mock("GET",
+//! mockito::Matcher::Regex(r"^/hello/(1|2)$".to_string())
+//! ).create();
//! ```
//!
//! Or you can catch all requests, by using the `Matcher::Any` variant:
@@ -291,10 +174,10 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
//! // Will match any GET request
-//! let _m = mock("GET", Matcher::Any).create();
+//! let _m = s.mock("GET", mockito::Matcher::Any).create();
//! ```
//!
//! # Matching by query
@@ -305,26 +188,26 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
//! // This will match requests containing the URL-encoded
//! // query parameter `greeting=good%20day`
-//! let _m1 = mock("GET", "/test")
-//! .match_query(Matcher::UrlEncoded("greeting".into(), "good day".into()))
+//! let _m1 = s.mock("GET", "/test")
+//! .match_query(mockito::Matcher::UrlEncoded("greeting".into(), "good day".into()))
//! .create();
//!
//! // This will match requests containing the URL-encoded
//! // query parameters `hello=world` and `greeting=good%20day`
-//! let _m2 = mock("GET", "/test")
-//! .match_query(Matcher::AllOf(vec![
-//! Matcher::UrlEncoded("hello".into(), "world".into()),
-//! Matcher::UrlEncoded("greeting".into(), "good day".into())
+//! let _m2 = s.mock("GET", "/test")
+//! .match_query(mockito::Matcher::AllOf(vec![
+//! mockito::Matcher::UrlEncoded("hello".into(), "world".into()),
+//! mockito::Matcher::UrlEncoded("greeting".into(), "good day".into())
//! ]))
//! .create();
//!
//! // You can achieve similar results with the regex matcher
-//! let _m3 = mock("GET", "/test")
-//! .match_query(Matcher::Regex("hello=world".into()))
+//! let _m3 = s.mock("GET", "/test")
+//! .match_query(mockito::Matcher::Regex("hello=world".into()))
//! .create();
//! ```
//!
@@ -349,14 +232,14 @@
//! ## Example
//!
//! ```
-//! use mockito::mock;
+//! let mut s = mockito::Server::new();
//!
-//! let _m1 = mock("GET", "/hello")
+//! let _m1 = s.mock("GET", "/hello")
//! .match_header("content-type", "application/json")
//! .with_body(r#"{"hello": "world"}"#)
//! .create();
//!
-//! let _m2 = mock("GET", "/hello")
+//! let _m2 = s.mock("GET", "/hello")
//! .match_header("content-type", "text/plain")
//! .with_body("world")
//! .create();
@@ -370,10 +253,10 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
-//! let _m = mock("GET", "/hello")
-//! .match_header("content-type", Matcher::Regex(r".*json.*".to_string()))
+//! let _m = s.mock("GET", "/hello")
+//! .match_header("content-type", mockito::Matcher::Regex(r".*json.*".to_string()))
//! .with_body(r#"{"hello": "world"}"#)
//! .create();
//! ```
@@ -383,10 +266,10 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
-//! let _m = mock("GET", "/hello")
-//! .match_header("content-type", Matcher::Any)
+//! let _m = s.mock("GET", "/hello")
+//! .match_header("content-type", mockito::Matcher::Any)
//! .with_body("something")
//! .create();
//!
@@ -400,10 +283,10 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
-//! let _m = mock("GET", "/hello")
-//! .match_header("authorization", Matcher::Missing)
+//! let _m = s.mock("GET", "/hello")
+//! .match_header("authorization", mockito::Matcher::Missing)
//! .with_body("no authorization header")
//! .create();
//!
@@ -421,10 +304,10 @@
//! ## Example
//!
//! ```
-//! use mockito::mock;
+//! let mut s = mockito::Server::new();
//!
//! // Will match requests to POST / whenever the request body is "hello"
-//! let _m = mock("POST", "/").match_body("hello").create();
+//! let _m = s.mock("POST", "/").match_body("hello").create();
//! ```
//!
//! Or you can match the body by using a regular expression:
@@ -432,10 +315,12 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
//! // Will match requests to POST / whenever the request body *contains* the word "hello" (e.g. "hello world")
-//! let _m = mock("POST", "/").match_body(Matcher::Regex("hello".to_string())).create();
+//! let _m = s.mock("POST", "/").match_body(
+//! mockito::Matcher::Regex("hello".to_string())
+//! ).create();
//! ```
//!
//! Or you can match the body using a JSON object:
@@ -446,11 +331,11 @@
//! # extern crate mockito;
//! #[macro_use]
//! extern crate serde_json;
-//! use mockito::{mock, Matcher};
//!
//! # fn main() {
+//! let mut s = mockito::Server::new();
//! // Will match requests to POST / whenever the request body matches the json object
-//! let _m = mock("POST", "/").match_body(Matcher::Json(json!({"hello": "world"}))).create();
+//! let _m = s.mock("POST", "/").match_body(mockito::Matcher::Json(json!({"hello": "world"}))).create();
//! # }
//! ```
//!
@@ -458,12 +343,12 @@
//! but by passing a `String` to the matcher:
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
//! // Will match requests to POST / whenever the request body matches the json object
-//! let _m = mock("POST", "/")
+//! let _m = s.mock("POST", "/")
//! .match_body(
-//! Matcher::JsonString(r#"{"hello": "world"}"#.to_string())
+//! mockito::Matcher::JsonString(r#"{"hello": "world"}"#.to_string())
//! )
//! .create();
//! ```
@@ -476,18 +361,18 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
//! // Will match requests to POST / whenever the request body is either `hello=world` or `{"hello":"world"}`
-//! let _m = mock("POST", "/")
+//! let _m = s.mock("POST", "/")
//! .match_body(
-//! Matcher::AnyOf(vec![
-//! Matcher::Exact("hello=world".to_string()),
-//! Matcher::JsonString(r#"{"hello": "world"}"#.to_string()),
+//! mockito::Matcher::AnyOf(vec![
+//! mockito::Matcher::Exact("hello=world".to_string()),
+//! mockito::Matcher::JsonString(r#"{"hello": "world"}"#.to_string()),
//! ])
//! )
//! .create();
-//!```
+//! ```
//!
//! # The `AllOf` matcher
//!
@@ -497,18 +382,191 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, Matcher};
+//! let mut s = mockito::Server::new();
//!
//! // Will match requests to POST / whenever the request body contains both `hello` and `world`
-//! let _m = mock("POST", "/")
+//! let _m = s.mock("POST", "/")
//! .match_body(
-//! Matcher::AllOf(vec![
-//! Matcher::Regex("hello".to_string()),
-//! Matcher::Regex("world".to_string()),
+//! mockito::Matcher::AllOf(vec![
+//! mockito::Matcher::Regex("hello".to_string()),
+//! mockito::Matcher::Regex("world".to_string()),
//! ])
//! )
//! .create();
-//!```
+//! ```
+//!
+//! # Asserts
+//!
+//! You can use the `Mock::assert` method to **assert that a mock was called**. In other words,
+//! `Mock#assert` can validate that your code performed the expected HTTP request.
+//!
+//! By default, the method expects only **one** request to your mock.
+//!
+//! ## Example
+//!
+//! ```no_run
+//! use std::net::TcpStream;
+//! use std::io::{Read, Write};
+//!
+//! let mut s = mockito::Server::new();
+//! let mock = s.mock("GET", "/hello").create();
+//!
+//! {
+//! // Place a request
+//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
+//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
+//! let mut response = String::new();
+//! stream.read_to_string(&mut response).unwrap();
+//! stream.flush().unwrap();
+//! }
+//!
+//! mock.assert();
+//! ```
+//!
+//! When several mocks can match a request, Mockito applies the first one that still expects requests.
+//! You can use this behaviour to provide **different responses for subsequent requests to the same endpoint**.
+//!
+//! ## Example
+//!
+//! ```
+//! use std::net::TcpStream;
+//! use std::io::{Read, Write};
+//!
+//! let mut s = mockito::Server::new();
+//! let english_hello_mock = s.mock("GET", "/hello").with_body("good bye").create();
+//! let french_hello_mock = s.mock("GET", "/hello").with_body("au revoir").create();
+//!
+//! {
+//! // Place a request to GET /hello
+//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
+//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
+//! let mut response = String::new();
+//! stream.read_to_string(&mut response).unwrap();
+//! stream.flush().unwrap();
+//! }
+//!
+//! english_hello_mock.assert();
+//!
+//! {
+//! // Place another request to GET /hello
+//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
+//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
+//! let mut response = String::new();
+//! stream.read_to_string(&mut response).unwrap();
+//! stream.flush().unwrap();
+//! }
+//!
+//! french_hello_mock.assert();
+//! ```
+//!
+//! If you're expecting more than 1 request, you can use the `Mock::expect` method to specify the exact amount of requests:
+//!
+//! ## Example
+//!
+//! ```no_run
+//! use std::net::TcpStream;
+//! use std::io::{Read, Write};
+//!
+//! let mut s = mockito::Server::new();
+//!
+//! let mock = s.mock("GET", "/hello").expect(3).create();
+//!
+//! for _ in 0..3 {
+//! // Place a request
+//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
+//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
+//! let mut response = String::new();
+//! stream.read_to_string(&mut response).unwrap();
+//! stream.flush().unwrap();
+//! }
+//!
+//! mock.assert();
+//! ```
+//!
+//! You can also work with ranges, by using the `Mock::expect_at_least` and `Mock::expect_at_most` methods:
+//!
+//! ## Example
+//!
+//! ```no_run
+//! use std::net::TcpStream;
+//! use std::io::{Read, Write};
+//!
+//! let mut s = mockito::Server::new();
+//!
+//! let mock = s.mock("GET", "/hello").expect_at_least(2).expect_at_most(4).create();
+//!
+//! for _ in 0..3 {
+//! // Place a request
+//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
+//! stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
+//! let mut response = String::new();
+//! stream.read_to_string(&mut response).unwrap();
+//! stream.flush().unwrap();
+//! }
+//!
+//! mock.assert();
+//! ```
+//!
+//! The errors produced by the `assert` method contain information about the tested mock, but also about the
+//! **last unmatched request**, which can be very useful to track down an error in your implementation or
+//! a missing or incomplete mock. A colored diff is also displayed.
+//!
+//! Color output is enabled by default, but can be toggled with the `color` feature flag.
+//!
+//! Here's an example of how a `Mock#assert` error looks like:
+//!
+//! ```text
+//! > Expected 1 request(s) to:
+//!
+//! POST /users?number=one
+//! bob
+//!
+//! ...but received 0
+//!
+//! > The last unmatched request was:
+//!
+//! POST /users?number=two
+//! content-length: 5
+//! alice
+//!
+//! > Difference:
+//!
+//! # A colored diff
+//!
+//! ```
+//!
+//! You can also use the `matched` method to return a boolean for whether the mock was called the
+//! correct number of times without panicking
+//!
+//! ## Example
+//!
+//! ```
+//! use std::net::TcpStream;
+//! use std::io::{Read, Write};
+//!
+//! let mut s = mockito::Server::new();
+//!
+//! let mock = s.mock("GET", "/").create();
+//!
+//! {
+//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
+//! stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
+//! let mut response = String::new();
+//! stream.read_to_string(&mut response).unwrap();
+//! stream.flush().unwrap();
+//! }
+//!
+//! assert!(mock.matched());
+//!
+//! {
+//! let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
+//! stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
+//! let mut response = String::new();
+//! stream.read_to_string(&mut response).unwrap();
+//! stream.flush().unwrap();
+//! }
+//! assert!(!mock.matched());
+//! ```
//!
//! # Non-matching calls
//!
@@ -524,13 +582,13 @@
//! ## Example
//!
//! ```
-//! use mockito::{mock, reset};
+//! let mut s = mockito::Server::new();
//!
-//! let _m1 = mock("GET", "/1").create();
-//! let _m2 = mock("GET", "/2").create();
-//! let _m3 = mock("GET", "/3").create();
+//! let _m1 = s.mock("GET", "/1").create();
+//! let _m2 = s.mock("GET", "/2").create();
+//! let _m3 = s.mock("GET", "/3").create();
//!
-//! reset();
+//! s.reset();
//!
//! // Nothing is mocked at this point
//! ```
@@ -540,10 +598,10 @@
//! ## Example
//!
//! ```
-//! use mockito::mock;
//! use std::mem;
//!
-//! let m = mock("GET", "/hello").create();
+//! let mut s = mockito::Server::new();
+//! let m = s.mock("GET", "/hello").create();
//!
//! // Requests to GET /hello are mocked
//!
@@ -557,7 +615,7 @@
//! Mockito uses the `env_logger` crate under the hood to provide useful debugging information.
//!
//! If you'd like to activate the debug output, introduce the [env_logger](https://crates.rs/crates/env_logger) crate
-//! within your project and initialize it before each test that needs debugging:
+//! to your project and initialize it before each test that needs debugging:
//!
//! ```
//! #[test]
@@ -573,841 +631,54 @@
//! RUST_LOG=mockito=debug cargo test
//! ```
//!
-//! # Threads
+//! # Sharing the server with other threads
//!
-//! Mockito records all your mocks on the same server running in the background. For this
-//! reason, Mockito tests are run sequentially. This is handled internally via a thread-local
-//! mutex lock acquired **whenever you create a mock**. Tests that don't create mocks will
-//! still be run in parallel.
+//! If you ever need to share the mock server with another thread, make sure to wrap it inside
+//! a Mutex or it might not get cleaned properly:
//!
+//! ## Example
+//!
+//! ```
+//! use std::sync::{Arc, Mutex};
+//! use std::thread;
+//!
+//! let mut s = mockito::Server::new();
+//! let host = s.host_with_port();
+//! let _mock_outside_thread = s.mock("GET", "/").with_body("outside").create();
+//!
+//! let server_mutex = Arc::new(Mutex::new(s));
+//! let server_clone = server_mutex.clone();
+//! let process = thread::spawn(move || {
+//! let mut s = server_clone.lock().unwrap();
+//! let _mock_inside_thread = s.mock("GET", "/").with_body("inside").create();
+//! });
+//!
+//! process.join().unwrap();
+//! ```
+//!
+pub use error::{Error, ErrorKind};
+use lazy_static::lazy_static;
+#[allow(deprecated)]
+pub use legacy::{mock, reset, server_address, server_url};
+pub use matcher::Matcher;
+pub use mock::Mock;
+pub use server::Server;
+use tokio::runtime::Runtime;
-#[macro_use]
-extern crate lazy_static;
-#[macro_use]
-extern crate log;
-
+mod command;
mod diff;
+mod error;
+mod legacy;
+mod matcher;
+mod mock;
mod request;
mod response;
mod server;
-
-type Request = request::Request;
-type Response = response::Response;
-
-use rand::distributions::Alphanumeric;
-use rand::{thread_rng, Rng};
-use regex::Regex;
-use std::cell::RefCell;
-use std::collections::HashMap;
-use std::convert::{From, Into};
-use std::fmt;
-use std::fs::File;
-use std::io;
-use std::io::Read;
-use std::ops::Drop;
-use std::path::Path;
-use std::string::ToString;
-use std::sync::Arc;
-use std::sync::{LockResult, Mutex, MutexGuard};
+mod server_pool;
lazy_static! {
- // A global lock that ensure all Mockito tests are run on a single thread.
- static ref TEST_MUTEX: Mutex<()> = Mutex::new(());
-}
-
-thread_local!(
- // A thread-local reference to the global lock. This is acquired within `Mock#create()`.
- static LOCAL_TEST_MUTEX: RefCell>> =
- RefCell::new(TEST_MUTEX.lock());
-);
-
-///
-/// Points to the address the mock server is running at.
-/// Can be used with `std::net::TcpStream`.
-///
-#[deprecated(note = "Call server_address() instead")]
-pub const SERVER_ADDRESS: &str = SERVER_ADDRESS_INTERNAL;
-const SERVER_ADDRESS_INTERNAL: &str = "127.0.0.1:1234";
-
-///
-/// Points to the URL the mock server is running at.
-///
-#[deprecated(note = "Call server_url() instead")]
-pub const SERVER_URL: &str = "http://127.0.0.1:1234";
-
-pub use crate::server::address as server_address;
-pub use crate::server::url as server_url;
-use assert_json_diff::{assert_json_matches_no_panic, CompareMode};
-
-///
-/// Initializes a mock for the provided `method` and `path`.
-///
-/// The mock is registered to the server only after the `create()` method has been called.
-///
-/// ## Example
-///
-/// ```
-/// use mockito::mock;
-///
-/// let _m1 = mock("GET", "/");
-/// let _m2 = mock("POST", "/users");
-/// let _m3 = mock("DELETE", "/users?id=1");
-/// ```
-///
-pub fn mock>(method: &str, path: P) -> Mock {
- Mock::new(method, path)
-}
-
-///
-/// Removes all the mocks stored on the server.
-///
-pub fn reset() {
- server::try_start();
-
- let mut state = server::STATE.lock().unwrap();
- state.mocks.clear();
-}
-
-#[allow(missing_docs)]
-pub fn start() {
- server::try_start();
-}
-
-///
-/// Allows matching the request path or headers in multiple ways: matching the exact value, matching any value (as
-/// long as it is present), matching by regular expression or checking that a particular header is missing.
-///
-/// These matchers are used within the `mock` and `Mock::match_header` calls.
-///
-#[derive(Clone, PartialEq, Debug)]
-#[allow(deprecated)] // Rust bug #38832
-pub enum Matcher {
- /// Matches the exact path or header value. There's also an implementation of `From<&str>`
- /// to keep things simple and backwards compatible.
- Exact(String),
- /// Matches the body content as a binary file
- Binary(BinaryBody),
- /// Matches a path or header value by a regular expression.
- Regex(String),
- /// Matches a specified JSON body from a `serde_json::Value`
- Json(serde_json::Value),
- /// Matches a specified JSON body from a `String`
- JsonString(String),
- /// Matches a partial JSON body from a `serde_json::Value`
- PartialJson(serde_json::Value),
- /// Matches a specified partial JSON body from a `String`
- PartialJsonString(String),
- /// Matches a URL-encoded key/value pair, where both key and value should be specified
- /// in plain (unencoded) format
- UrlEncoded(String, String),
- /// At least one matcher must match
- AnyOf(Vec),
- /// All matchers must match
- AllOf(Vec),
- /// Matches any path or any header value.
- Any,
- /// Checks that a header is not present in the request.
- Missing,
-}
-
-impl<'a> From<&'a str> for Matcher {
- fn from(value: &str) -> Self {
- Matcher::Exact(value.to_string())
- }
-}
-
-#[allow(clippy::fallible_impl_from)]
-impl From<&Path> for Matcher {
- fn from(value: &Path) -> Self {
- // We want the code to panic if the path is not readable.
- Matcher::Binary(BinaryBody::from_path(value).unwrap())
- }
-}
-
-impl From<&mut File> for Matcher {
- fn from(value: &mut File) -> Self {
- Matcher::Binary(BinaryBody::from_file(value))
- }
-}
-
-impl From> for Matcher {
- fn from(value: Vec) -> Self {
- Matcher::Binary(BinaryBody::from_bytes(value))
- }
-}
-
-impl fmt::Display for Matcher {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let join_matches = |matches: &[Self]| {
- matches
- .iter()
- .map(Self::to_string)
- .fold(String::new(), |acc, matcher| {
- if acc.is_empty() {
- matcher
- } else {
- format!("{}, {}", acc, matcher)
- }
- })
- };
-
- let result = match self {
- Matcher::Exact(ref value) => value.to_string(),
- Matcher::Binary(ref file) => format!("{} (binary)", file),
- Matcher::Regex(ref value) => format!("{} (regex)", value),
- Matcher::Json(ref json_obj) => format!("{} (json)", json_obj),
- Matcher::JsonString(ref value) => format!("{} (json)", value),
- Matcher::PartialJson(ref json_obj) => format!("{} (partial json)", json_obj),
- Matcher::PartialJsonString(ref value) => format!("{} (partial json)", value),
- Matcher::UrlEncoded(ref field, ref value) => {
- format!("{}={} (urlencoded)", field, value)
- }
- Matcher::Any => "(any)".to_string(),
- Matcher::AnyOf(x) => format!("({}) (any of)", join_matches(x)),
- Matcher::AllOf(x) => format!("({}) (all of)", join_matches(x)),
- Matcher::Missing => "(missing)".to_string(),
- };
- write!(f, "{}", result)
- }
-}
-
-impl Matcher {
- fn matches_values(&self, header_values: &[&str]) -> bool {
- match self {
- Matcher::Missing => header_values.is_empty(),
- // AnyOf([…Missing…]) is handled here, but
- // AnyOf([Something]) is handled in the last block.
- // That's because Missing matches against all values at once,
- // but other matchers match against individual values.
- Matcher::AnyOf(ref matchers) if header_values.is_empty() => {
- matchers.iter().any(|m| m.matches_values(header_values))
- }
- Matcher::AllOf(ref matchers) if header_values.is_empty() => {
- matchers.iter().all(|m| m.matches_values(header_values))
- }
- _ => {
- !header_values.is_empty() && header_values.iter().all(|val| self.matches_value(val))
- }
- }
- }
-
- fn matches_binary_value(&self, binary: &[u8]) -> bool {
- match self {
- Matcher::Binary(ref file) => binary == &*file.content,
- _ => false,
- }
- }
-
- #[allow(deprecated)]
- fn matches_value(&self, other: &str) -> bool {
- let compare_json_config = assert_json_diff::Config::new(CompareMode::Inclusive);
- match self {
- Matcher::Exact(ref value) => value == other,
- Matcher::Binary(_) => false,
- Matcher::Regex(ref regex) => Regex::new(regex).unwrap().is_match(other),
- Matcher::Json(ref json_obj) => {
- let other: serde_json::Value = serde_json::from_str(other).unwrap();
- *json_obj == other
- }
- Matcher::JsonString(ref value) => {
- let value: serde_json::Value = serde_json::from_str(value).unwrap();
- let other: serde_json::Value = serde_json::from_str(other).unwrap();
- value == other
- }
- Matcher::PartialJson(ref json_obj) => {
- let actual: serde_json::Value = serde_json::from_str(other).unwrap();
- let expected = json_obj.clone();
- assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok()
- }
- Matcher::PartialJsonString(ref value) => {
- let expected: serde_json::Value = serde_json::from_str(value).unwrap();
- let actual: serde_json::Value = serde_json::from_str(other).unwrap();
- assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok()
- }
- Matcher::UrlEncoded(ref expected_field, ref expected_value) => {
- serde_urlencoded::from_str::>(other)
- .map(|params: HashMap<_, _>| {
- params.into_iter().any(|(ref field, ref value)| {
- field == expected_field && value == expected_value
- })
- })
- .unwrap_or(false)
- }
- Matcher::Any => true,
- Matcher::AnyOf(ref matchers) => matchers.iter().any(|m| m.matches_value(other)),
- Matcher::AllOf(ref matchers) => matchers.iter().all(|m| m.matches_value(other)),
- Matcher::Missing => other.is_empty(),
- }
- }
-}
-
-#[derive(Clone, PartialEq, Debug)]
-enum PathAndQueryMatcher {
- Unified(Matcher),
- Split(Box, Box),
-}
-
-impl PathAndQueryMatcher {
- fn matches_value(&self, other: &str) -> bool {
- match self {
- PathAndQueryMatcher::Unified(matcher) => matcher.matches_value(other),
- PathAndQueryMatcher::Split(ref path_matcher, ref query_matcher) => {
- let mut parts = other.splitn(2, '?');
- let path = parts.next().unwrap();
- let query = parts.next().unwrap_or("");
-
- path_matcher.matches_value(path) && query_matcher.matches_value(query)
- }
- }
- }
-}
-
-///
-/// Represents a binary object the body should be matched against
-///
-#[derive(Debug, Clone)]
-pub struct BinaryBody {
- path: Option,
- content: Vec,
-}
-
-impl BinaryBody {
- /// Read the content from path and initialize a `BinaryBody`
- ///
- /// # Errors
- ///
- /// The same resulting from a failed `std::fs::read`.
- pub fn from_path(path: &Path) -> Result {
- Ok(Self {
- path: path.to_str().map(ToString::to_string),
- content: std::fs::read(path)?,
- })
- }
-
- /// Read the content from a &mut File and initialize a `BinaryBody`
- pub fn from_file(file: &mut File) -> Self {
- Self {
- path: None,
- content: get_content_from(file),
- }
- }
-
- /// Instantiate the matcher directly passing the content
- #[allow(clippy::missing_const_for_fn)]
- pub fn from_bytes(content: Vec) -> Self {
- Self {
- path: None,
- content,
- }
- }
-}
-
-fn get_content_from(file: &mut File) -> Vec {
- let mut filecontent: Vec = Vec::new();
- file.read_to_end(&mut filecontent).unwrap();
- filecontent
-}
-
-impl PartialEq for BinaryBody {
- fn eq(&self, other: &Self) -> bool {
- match (self.path.as_ref(), other.path.as_ref()) {
- (Some(p), Some(o)) => p == o,
- _ => self.content == other.content,
- }
- }
-}
-
-impl fmt::Display for BinaryBody {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- if let Some(filepath) = self.path.as_ref() {
- write!(f, "filepath: {}", filepath)
- } else {
- let len: usize = std::cmp::min(self.content.len(), 8);
- let first_bytes: Vec = self.content.iter().copied().take(len).collect();
- write!(f, "filecontent: {:?}", first_bytes)
- }
- }
-}
-
-///
-/// Stores information about a mocked request. Should be initialized via `mockito::mock()`.
-///
-#[derive(Clone, PartialEq, Debug)]
-pub struct Mock {
- id: String,
- method: String,
- path: PathAndQueryMatcher,
- headers: Vec<(String, Matcher)>,
- body: Matcher,
- response: Response,
- hits: usize,
- expected_hits_at_least: Option,
- expected_hits_at_most: Option,
- is_remote: bool,
-
- /// Used to warn of mocks missing a `.create()` call. See issue #112
- created: bool,
-}
-
-impl Mock {
- fn new>(method: &str, path: P) -> Self {
- Self {
- id: thread_rng()
- .sample_iter(&Alphanumeric)
- .map(char::from)
- .take(24)
- .collect(),
- method: method.to_owned().to_uppercase(),
- path: PathAndQueryMatcher::Unified(path.into()),
- headers: Vec::new(),
- body: Matcher::Any,
- response: Response::default(),
- hits: 0,
- expected_hits_at_least: None,
- expected_hits_at_most: None,
- is_remote: false,
- created: false,
- }
- }
-
- ///
- /// Allows matching against the query part when responding with a mock.
- ///
- /// Note that you can also specify the query as part of the path argument
- /// in a `mock` call, in which case an exact match will be performed.
- /// Any future calls of `Mock#match_query` will override the query matcher.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::{mock, Matcher};
- ///
- /// // This will match requests containing the URL-encoded
- /// // query parameter `greeting=good%20day`
- /// let _m1 = mock("GET", "/test")
- /// .match_query(Matcher::UrlEncoded("greeting".into(), "good day".into()))
- /// .create();
- ///
- /// // This will match requests containing the URL-encoded
- /// // query parameters `hello=world` and `greeting=good%20day`
- /// let _m2 = mock("GET", "/test")
- /// .match_query(Matcher::AllOf(vec![
- /// Matcher::UrlEncoded("hello".into(), "world".into()),
- /// Matcher::UrlEncoded("greeting".into(), "good day".into())
- /// ]))
- /// .create();
- ///
- /// // You can achieve similar results with the regex matcher
- /// let _m3 = mock("GET", "/test")
- /// .match_query(Matcher::Regex("hello=world".into()))
- /// .create();
- /// ```
- ///
- pub fn match_query>(mut self, query: M) -> Self {
- let new_path = match &self.path {
- PathAndQueryMatcher::Unified(matcher) => {
- PathAndQueryMatcher::Split(Box::new(matcher.clone()), Box::new(query.into()))
- }
- PathAndQueryMatcher::Split(path, _) => {
- PathAndQueryMatcher::Split(path.clone(), Box::new(query.into()))
- }
- };
-
- self.path = new_path;
-
- self
- }
-
- ///
- /// Allows matching a particular request header when responding with a mock.
- ///
- /// When matching a request, the field letter case is ignored.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m = mock("GET", "/").match_header("content-type", "application/json");
- /// ```
- ///
- /// Like most other `Mock` methods, it allows chanining:
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m = mock("GET", "/")
- /// .match_header("content-type", "application/json")
- /// .match_header("authorization", "password");
- /// ```
- ///
- pub fn match_header>(mut self, field: &str, value: M) -> Self {
- self.headers
- .push((field.to_owned().to_lowercase(), value.into()));
-
- self
- }
-
- ///
- /// Allows matching a particular request body when responding with a mock.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m1 = mock("POST", "/").match_body(r#"{"hello": "world"}"#).with_body("json").create();
- /// let _m2 = mock("POST", "/").match_body("hello=world").with_body("form").create();
- ///
- /// // Requests passing `{"hello": "world"}` inside the body will be responded with "json".
- /// // Requests passing `hello=world` inside the body will be responded with "form".
- ///
- /// // Create a temporary file
- /// use std::env;
- /// use std::fs::File;
- /// use std::io::Write;
- /// use std::path::Path;
- /// use rand;
- /// use rand::Rng;
- ///
- /// let random_bytes: Vec = (0..1024).map(|_| rand::random::()).collect();
- ///
- /// let mut tmp_file = env::temp_dir();
- /// tmp_file.push("test_file.txt");
- /// let mut f_write = File::create(tmp_file.clone()).unwrap();
- /// f_write.write_all(random_bytes.as_slice()).unwrap();
- /// let mut f_read = File::open(tmp_file.clone()).unwrap();
- ///
- ///
- /// // the following are equivalent ways of defining a mock matching
- /// // a binary payload
- /// let _b1 = mock("POST", "/").match_body(tmp_file.as_path()).create();
- /// let _b3 = mock("POST", "/").match_body(random_bytes).create();
- /// let _b2 = mock("POST", "/").match_body(&mut f_read).create();
- /// ```
- ///
- pub fn match_body>(mut self, body: M) -> Self {
- self.body = body.into();
-
- self
- }
-
- ///
- /// Sets the status code of the mock response. The default status code is 200.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m = mock("GET", "/").with_status(201);
- /// ```
- ///
- pub fn with_status(mut self, status: usize) -> Self {
- self.response.status = status.into();
-
- self
- }
-
- ///
- /// Sets a header of the mock response.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m = mock("GET", "/").with_header("content-type", "application/json");
- /// ```
- ///
- pub fn with_header(mut self, field: &str, value: &str) -> Self {
- self.response
- .headers
- .push((field.to_owned(), value.to_owned()));
-
- self
- }
-
- ///
- /// Sets the body of the mock response. Its `Content-Length` is handled automatically.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m = mock("GET", "/").with_body("hello world");
- /// ```
- ///
- pub fn with_body>(mut self, body: StrOrBytes) -> Self {
- self.response.body = response::Body::Bytes(body.as_ref().to_owned());
- self
- }
-
- ///
- /// Sets the body of the mock response dynamically. The response will use chunked transfer encoding.
- ///
- /// The function must be thread-safe. If it's a closure, it can't be borrowing its context.
- /// Use `move` closures and `Arc` to share any data.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m = mock("GET", "/").with_body_from_fn(|w| w.write_all(b"hello world"));
- /// ```
- ///
- pub fn with_body_from_fn(
- mut self,
- cb: impl Fn(&mut dyn io::Write) -> io::Result<()> + Send + Sync + 'static,
- ) -> Self {
- self.response.body = response::Body::Fn(Arc::new(cb));
- self
- }
-
- ///
- /// Sets the body of the mock response from the contents of a file stored under `path`.
- /// Its `Content-Length` is handled automatically.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m = mock("GET", "/").with_body_from_file("tests/files/simple.http");
- /// ```
- ///
- pub fn with_body_from_file(mut self, path: impl AsRef) -> Self {
- self.response.body = response::Body::Bytes(std::fs::read(path).unwrap());
- self
- }
-
- ///
- /// Sets the expected amount of requests that this mock is supposed to receive.
- /// This is only enforced when calling the `assert` method.
- /// Defaults to 1 request.
- ///
- #[allow(clippy::missing_const_for_fn)]
- pub fn expect(mut self, hits: usize) -> Self {
- self.expected_hits_at_least = Some(hits);
- self.expected_hits_at_most = Some(hits);
- self
- }
-
- ///
- /// Sets the minimum amount of requests that this mock is supposed to receive.
- /// This is only enforced when calling the `assert` method.
- ///
- pub fn expect_at_least(mut self, hits: usize) -> Self {
- self.expected_hits_at_least = Some(hits);
- if self.expected_hits_at_most.is_some()
- && self.expected_hits_at_most < self.expected_hits_at_least
- {
- self.expected_hits_at_most = None;
- }
- self
- }
-
- ///
- /// Sets the maximum amount of requests that this mock is supposed to receive.
- /// This is only enforced when calling the `assert` method.
- ///
- pub fn expect_at_most(mut self, hits: usize) -> Self {
- self.expected_hits_at_most = Some(hits);
- if self.expected_hits_at_least.is_some()
- && self.expected_hits_at_least > self.expected_hits_at_most
- {
- self.expected_hits_at_least = None;
- }
- self
- }
-
- ///
- /// Asserts that the expected amount of requests (defaults to 1 request) were performed.
- ///
- pub fn assert(&self) {
- let mut opt_message = None;
-
- {
- let state = server::STATE.lock().unwrap();
-
- if let Some(remote_mock) = state.mocks.iter().find(|mock| mock.id == self.id) {
- let mut message = match (self.expected_hits_at_least, self.expected_hits_at_most) {
- (Some(min), Some(max)) if min == max => format!(
- "\n> Expected {} request(s) to:\n{}\n...but received {}\n\n",
- min, self, remote_mock.hits
- ),
- (Some(min), Some(max)) => format!(
- "\n> Expected between {} and {} request(s) to:\n{}\n...but received {}\n\n",
- min, max, self, remote_mock.hits
- ),
- (Some(min), None) => format!(
- "\n> Expected at least {} request(s) to:\n{}\n...but received {}\n\n",
- min, self, remote_mock.hits
- ),
- (None, Some(max)) => format!(
- "\n> Expected at most {} request(s) to:\n{}\n...but received {}\n\n",
- max, self, remote_mock.hits
- ),
- (None, None) => format!(
- "\n> Expected 1 request(s) to:\n{}\n...but received {}\n\n",
- self, remote_mock.hits
- ),
- };
-
- if let Some(last_request) = state.unmatched_requests.last() {
- message.push_str(&format!(
- "> The last unmatched request was:\n{}\n",
- last_request
- ));
-
- let difference = diff::compare(&self.to_string(), &last_request.to_string());
- message.push_str(&format!("> Difference:\n{}\n", difference));
- }
-
- opt_message = Some(message);
- }
- }
-
- if let Some(message) = opt_message {
- assert!(self.matched(), "{}", message)
- } else {
- panic!("Could not retrieve enough information about the remote mock.")
- }
- }
-
- ///
- /// Returns whether the expected amount of requests (defaults to 1) were performed.
- ///
- pub fn matched(&self) -> bool {
- let state = server::STATE.lock().unwrap();
-
- state
- .mocks
- .iter()
- .find(|mock| mock.id == self.id)
- .map_or(false, |remote_mock| {
- let hits = remote_mock.hits;
-
- match (self.expected_hits_at_least, self.expected_hits_at_most) {
- (Some(min), Some(max)) => hits >= min && hits <= max,
- (Some(min), None) => hits >= min,
- (None, Some(max)) => hits <= max,
- (None, None) => hits == 1,
- }
- })
- }
-
- ///
- /// Registers the mock to the server - your mock will be served only after calling this method.
- ///
- /// ## Example
- ///
- /// ```
- /// use mockito::mock;
- ///
- /// let _m = mock("GET", "/").with_body("hello world").create();
- /// ```
- ///
- #[must_use]
- pub fn create(mut self) -> Self {
- server::try_start();
-
- // Ensures Mockito tests are run sequentially.
- LOCAL_TEST_MUTEX.with(|_| {});
-
- let mut state = server::STATE.lock().unwrap();
-
- self.created = true;
-
- let mut remote_mock = self.clone();
- remote_mock.is_remote = true;
- state.mocks.push(remote_mock);
-
- self
- }
-
- #[allow(clippy::missing_const_for_fn)]
- fn is_local(&self) -> bool {
- !self.is_remote
- }
-}
-
-impl Drop for Mock {
- fn drop(&mut self) {
- if self.is_local() {
- let mut state = server::STATE.lock().unwrap();
-
- if let Some(pos) = state.mocks.iter().position(|mock| mock.id == self.id) {
- state.mocks.remove(pos);
- }
-
- debug!("Mock::drop() called for {}", self);
-
- if !self.created {
- warn!("Missing .create() call on mock {}", self);
- }
- }
- }
-}
-
-impl fmt::Display for PathAndQueryMatcher {
- #[allow(deprecated)]
- #[allow(clippy::write_with_newline)]
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- PathAndQueryMatcher::Unified(matcher) => write!(f, "{}\r\n", &matcher),
- PathAndQueryMatcher::Split(path, query) => write!(f, "{}?{}\r\n", &path, &query),
- }
- }
-}
-
-impl fmt::Display for Mock {
- #[allow(deprecated)]
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let mut formatted = String::new();
-
- formatted.push_str("\r\n");
- formatted.push_str(&self.method);
- formatted.push(' ');
- formatted.push_str(&self.path.to_string());
-
- for &(ref key, ref value) in &self.headers {
- formatted.push_str(key);
- formatted.push_str(": ");
- formatted.push_str(&value.to_string());
- formatted.push_str("\r\n");
- }
-
- match self.body {
- Matcher::Exact(ref value)
- | Matcher::JsonString(ref value)
- | Matcher::PartialJsonString(ref value)
- | Matcher::Regex(ref value) => {
- formatted.push_str(value);
- formatted.push_str("\r\n");
- }
- Matcher::Binary(_) => {
- formatted.push_str("(binary)\r\n");
- }
- Matcher::Json(ref json_obj) | Matcher::PartialJson(ref json_obj) => {
- formatted.push_str(&json_obj.to_string());
- formatted.push_str("\r\n")
- }
- Matcher::UrlEncoded(ref field, ref value) => {
- formatted.push_str(field);
- formatted.push('=');
- formatted.push_str(value);
- }
- Matcher::Missing => formatted.push_str("(missing)\r\n"),
- Matcher::AnyOf(..) => formatted.push_str("(any of)\r\n"),
- Matcher::AllOf(..) => formatted.push_str("(all of)\r\n"),
- Matcher::Any => {}
- }
-
- f.write_str(&formatted)
- }
+ pub(crate) static ref RUNTIME: Runtime = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("couldn't start tokio runtime");
}
diff --git a/src/matcher.rs b/src/matcher.rs
new file mode 100644
index 0000000..1b4b65a
--- /dev/null
+++ b/src/matcher.rs
@@ -0,0 +1,277 @@
+use assert_json_diff::{assert_json_matches_no_panic, CompareMode};
+use regex::Regex;
+use std::collections::HashMap;
+use std::convert::From;
+use std::fmt;
+use std::fs::File;
+use std::io;
+use std::io::Read;
+use std::path::Path;
+use std::string::ToString;
+
+///
+/// Allows matching the request path, headers or body in multiple ways: by the exact value, by any value (as
+/// long as it is present), by regular expression or by checking that a particular header is missing.
+///
+/// These matchers can be used within the `Server::mock`, `Mock::match_header` or `Mock::match_body` calls.
+///
+#[derive(Clone, PartialEq, Debug)]
+#[allow(deprecated)] // Rust bug #38832
+pub enum Matcher {
+ /// Matches the exact path or header value. There's also an implementation of `From<&str>`
+ /// to keep things simple and backwards compatible.
+ Exact(String),
+ /// Matches the body content as a binary file
+ Binary(BinaryBody),
+ /// Matches a path or header value by a regular expression.
+ Regex(String),
+ /// Matches a specified JSON body from a `serde_json::Value`
+ Json(serde_json::Value),
+ /// Matches a specified JSON body from a `String`
+ JsonString(String),
+ /// Matches a partial JSON body from a `serde_json::Value`
+ PartialJson(serde_json::Value),
+ /// Matches a specified partial JSON body from a `String`
+ PartialJsonString(String),
+ /// Matches a URL-encoded key/value pair, where both key and value should be specified
+ /// in plain (unencoded) format
+ UrlEncoded(String, String),
+ /// At least one matcher must match
+ AnyOf(Vec),
+ /// All matchers must match
+ AllOf(Vec),
+ /// Matches any path or any header value.
+ Any,
+ /// Checks that a header is not present in the request.
+ Missing,
+}
+
+impl<'a> From<&'a str> for Matcher {
+ fn from(value: &str) -> Self {
+ Matcher::Exact(value.to_string())
+ }
+}
+
+#[allow(clippy::fallible_impl_from)]
+impl From<&Path> for Matcher {
+ fn from(value: &Path) -> Self {
+ // We want the code to panic if the path is not readable.
+ Matcher::Binary(BinaryBody::from_path(value).unwrap())
+ }
+}
+
+impl From<&mut File> for Matcher {
+ fn from(value: &mut File) -> Self {
+ Matcher::Binary(BinaryBody::from_file(value))
+ }
+}
+
+impl From> for Matcher {
+ fn from(value: Vec) -> Self {
+ Matcher::Binary(BinaryBody::from_bytes(value))
+ }
+}
+
+impl fmt::Display for Matcher {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let join_matches = |matches: &[Self]| {
+ matches
+ .iter()
+ .map(Self::to_string)
+ .fold(String::new(), |acc, matcher| {
+ if acc.is_empty() {
+ matcher
+ } else {
+ format!("{}, {}", acc, matcher)
+ }
+ })
+ };
+
+ let result = match self {
+ Matcher::Exact(ref value) => value.to_string(),
+ Matcher::Binary(ref file) => format!("{} (binary)", file),
+ Matcher::Regex(ref value) => format!("{} (regex)", value),
+ Matcher::Json(ref json_obj) => format!("{} (json)", json_obj),
+ Matcher::JsonString(ref value) => format!("{} (json)", value),
+ Matcher::PartialJson(ref json_obj) => format!("{} (partial json)", json_obj),
+ Matcher::PartialJsonString(ref value) => format!("{} (partial json)", value),
+ Matcher::UrlEncoded(ref field, ref value) => {
+ format!("{}={} (urlencoded)", field, value)
+ }
+ Matcher::Any => "(any)".to_string(),
+ Matcher::AnyOf(x) => format!("({}) (any of)", join_matches(x)),
+ Matcher::AllOf(x) => format!("({}) (all of)", join_matches(x)),
+ Matcher::Missing => "(missing)".to_string(),
+ };
+ write!(f, "{}", result)
+ }
+}
+
+impl Matcher {
+ pub(crate) fn matches_values(&self, header_values: &[&str]) -> bool {
+ match self {
+ Matcher::Missing => header_values.is_empty(),
+ // AnyOf([…Missing…]) is handled here, but
+ // AnyOf([Something]) is handled in the last block.
+ // That's because Missing matches against all values at once,
+ // but other matchers match against individual values.
+ Matcher::AnyOf(ref matchers) if header_values.is_empty() => {
+ matchers.iter().any(|m| m.matches_values(header_values))
+ }
+ Matcher::AllOf(ref matchers) if header_values.is_empty() => {
+ matchers.iter().all(|m| m.matches_values(header_values))
+ }
+ _ => {
+ !header_values.is_empty() && header_values.iter().all(|val| self.matches_value(val))
+ }
+ }
+ }
+
+ pub(crate) fn matches_binary_value(&self, binary: &[u8]) -> bool {
+ match self {
+ Matcher::Binary(ref file) => binary == &*file.content,
+ _ => false,
+ }
+ }
+
+ #[allow(deprecated)]
+ pub(crate) fn matches_value(&self, other: &str) -> bool {
+ let compare_json_config = assert_json_diff::Config::new(CompareMode::Inclusive);
+ match self {
+ Matcher::Exact(ref value) => value == other,
+ Matcher::Binary(_) => false,
+ Matcher::Regex(ref regex) => Regex::new(regex).unwrap().is_match(other),
+ Matcher::Json(ref json_obj) => {
+ let other: serde_json::Value = serde_json::from_str(other).unwrap();
+ *json_obj == other
+ }
+ Matcher::JsonString(ref value) => {
+ let value: serde_json::Value = serde_json::from_str(value).unwrap();
+ let other: serde_json::Value = serde_json::from_str(other).unwrap();
+ value == other
+ }
+ Matcher::PartialJson(ref json_obj) => {
+ let actual: serde_json::Value = serde_json::from_str(other).unwrap();
+ let expected = json_obj.clone();
+ assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok()
+ }
+ Matcher::PartialJsonString(ref value) => {
+ let expected: serde_json::Value = serde_json::from_str(value).unwrap();
+ let actual: serde_json::Value = serde_json::from_str(other).unwrap();
+ assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok()
+ }
+ Matcher::UrlEncoded(ref expected_field, ref expected_value) => {
+ serde_urlencoded::from_str::>(other)
+ .map(|params: HashMap<_, _>| {
+ params.into_iter().any(|(ref field, ref value)| {
+ field == expected_field && value == expected_value
+ })
+ })
+ .unwrap_or(false)
+ }
+ Matcher::Any => true,
+ Matcher::AnyOf(ref matchers) => matchers.iter().any(|m| m.matches_value(other)),
+ Matcher::AllOf(ref matchers) => matchers.iter().all(|m| m.matches_value(other)),
+ Matcher::Missing => other.is_empty(),
+ }
+ }
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub(crate) enum PathAndQueryMatcher {
+ Unified(Matcher),
+ Split(Box, Box),
+}
+
+impl PathAndQueryMatcher {
+ pub(crate) fn matches_value(&self, other: &str) -> bool {
+ match self {
+ PathAndQueryMatcher::Unified(matcher) => matcher.matches_value(other),
+ PathAndQueryMatcher::Split(ref path_matcher, ref query_matcher) => {
+ let mut parts = other.splitn(2, '?');
+ let path = parts.next().unwrap();
+ let query = parts.next().unwrap_or("");
+
+ path_matcher.matches_value(path) && query_matcher.matches_value(query)
+ }
+ }
+ }
+}
+
+impl fmt::Display for PathAndQueryMatcher {
+ #[allow(deprecated)]
+ #[allow(clippy::write_with_newline)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ PathAndQueryMatcher::Unified(matcher) => write!(f, "{}\r\n", &matcher),
+ PathAndQueryMatcher::Split(path, query) => write!(f, "{}?{}\r\n", &path, &query),
+ }
+ }
+}
+
+///
+/// Represents a binary object the body should be matched against
+///
+#[derive(Debug, Clone)]
+pub struct BinaryBody {
+ path: Option,
+ content: Vec,
+}
+
+impl BinaryBody {
+ /// Read the content from path and initialize a `BinaryBody`
+ ///
+ /// # Errors
+ ///
+ /// The same resulting from a failed `std::fs::read`.
+ pub fn from_path(path: &Path) -> Result {
+ Ok(Self {
+ path: path.to_str().map(ToString::to_string),
+ content: std::fs::read(path)?,
+ })
+ }
+
+ /// Read the content from a &mut File and initialize a `BinaryBody`
+ pub fn from_file(file: &mut File) -> Self {
+ Self {
+ path: None,
+ content: get_content_from(file),
+ }
+ }
+
+ /// Instantiate the matcher directly passing the content
+ #[allow(clippy::missing_const_for_fn)]
+ pub fn from_bytes(content: Vec) -> Self {
+ Self {
+ path: None,
+ content,
+ }
+ }
+}
+
+fn get_content_from(file: &mut File) -> Vec {
+ let mut filecontent: Vec = Vec::new();
+ file.read_to_end(&mut filecontent).unwrap();
+ filecontent
+}
+
+impl PartialEq for BinaryBody {
+ fn eq(&self, other: &Self) -> bool {
+ match (self.path.as_ref(), other.path.as_ref()) {
+ (Some(p), Some(o)) => p == o,
+ _ => self.content == other.content,
+ }
+ }
+}
+
+impl fmt::Display for BinaryBody {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if let Some(filepath) = self.path.as_ref() {
+ write!(f, "filepath: {}", filepath)
+ } else {
+ let len: usize = std::cmp::min(self.content.len(), 8);
+ let first_bytes: Vec = self.content.iter().copied().take(len).collect();
+ write!(f, "filecontent: {:?}", first_bytes)
+ }
+ }
+}
diff --git a/src/mock.rs b/src/mock.rs
new file mode 100644
index 0000000..fb262b7
--- /dev/null
+++ b/src/mock.rs
@@ -0,0 +1,544 @@
+use crate::command::Command;
+use crate::diff;
+use crate::matcher::{Matcher, PathAndQueryMatcher};
+use crate::response::{Body, Response};
+use crate::server::RemoteMock;
+use crate::{Error, ErrorKind};
+use hyper::StatusCode;
+use rand::distributions::Alphanumeric;
+use rand::{thread_rng, Rng};
+use std::convert::Into;
+use std::fmt;
+use std::io;
+use std::ops::Drop;
+use std::path::Path;
+use std::string::ToString;
+use std::sync::Arc;
+use tokio::sync::mpsc::Sender;
+
+#[derive(Clone, Debug)]
+pub struct InnerMock {
+ pub(crate) id: String,
+ pub(crate) method: String,
+ pub(crate) path: PathAndQueryMatcher,
+ pub(crate) headers: Vec<(String, Matcher)>,
+ pub(crate) body: Matcher,
+ pub(crate) response: Response,
+ pub(crate) hits: usize,
+ pub(crate) expected_hits_at_least: Option,
+ pub(crate) expected_hits_at_most: Option,
+}
+
+impl fmt::Display for InnerMock {
+ #[allow(deprecated)]
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut formatted = String::new();
+
+ formatted.push_str("\r\n");
+ formatted.push_str(&self.method);
+ formatted.push(' ');
+ formatted.push_str(&self.path.to_string());
+
+ for &(ref key, ref value) in &self.headers {
+ formatted.push_str(key);
+ formatted.push_str(": ");
+ formatted.push_str(&value.to_string());
+ formatted.push_str("\r\n");
+ }
+
+ match self.body {
+ Matcher::Exact(ref value)
+ | Matcher::JsonString(ref value)
+ | Matcher::PartialJsonString(ref value)
+ | Matcher::Regex(ref value) => {
+ formatted.push_str(value);
+ formatted.push_str("\r\n");
+ }
+ Matcher::Binary(_) => {
+ formatted.push_str("(binary)\r\n");
+ }
+ Matcher::Json(ref json_obj) | Matcher::PartialJson(ref json_obj) => {
+ formatted.push_str(&json_obj.to_string());
+ formatted.push_str("\r\n")
+ }
+ Matcher::UrlEncoded(ref field, ref value) => {
+ formatted.push_str(field);
+ formatted.push('=');
+ formatted.push_str(value);
+ }
+ Matcher::Missing => formatted.push_str("(missing)\r\n"),
+ Matcher::AnyOf(..) => formatted.push_str("(any of)\r\n"),
+ Matcher::AllOf(..) => formatted.push_str("(all of)\r\n"),
+ Matcher::Any => {}
+ }
+
+ f.write_str(&formatted)
+ }
+}
+
+impl PartialEq for InnerMock {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ && self.method == other.method
+ && self.path == other.path
+ && self.headers == other.headers
+ && self.body == other.body
+ && self.response == other.response
+ && self.hits == other.hits
+ }
+}
+
+///
+/// Stores information about a mocked request. Should be initialized via `Server::mock()`.
+///
+#[derive(Debug)]
+pub struct Mock {
+ sender: Sender,
+ inner: InnerMock,
+ /// Used to warn of mocks missing a `.create()` call. See issue #112
+ created: bool,
+}
+
+impl Mock {
+ pub(crate) fn new>(sender: Sender, method: &str, path: P) -> Mock {
+ let inner = InnerMock {
+ id: thread_rng()
+ .sample_iter(&Alphanumeric)
+ .map(char::from)
+ .take(24)
+ .collect(),
+ method: method.to_owned().to_uppercase(),
+ path: PathAndQueryMatcher::Unified(path.into()),
+ headers: Vec::new(),
+ body: Matcher::Any,
+ response: Response::default(),
+ hits: 0,
+ expected_hits_at_least: None,
+ expected_hits_at_most: None,
+ };
+
+ Self {
+ sender,
+ inner,
+ created: false,
+ }
+ }
+
+ ///
+ /// Allows matching against the query part when responding with a mock.
+ ///
+ /// Note that you can also specify the query as part of the path argument
+ /// in a `mock` call, in which case an exact match will be performed.
+ /// Any future calls of `Mock#match_query` will override the query matcher.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// use mockito::Matcher;
+ ///
+ /// let mut s = mockito::Server::new();
+ ///
+ /// // This will match requests containing the URL-encoded
+ /// // query parameter `greeting=good%20day`
+ /// let _m1 = s.mock("GET", "/test")
+ /// .match_query(Matcher::UrlEncoded("greeting".into(), "good day".into()))
+ /// .create();
+ ///
+ /// // This will match requests containing the URL-encoded
+ /// // query parameters `hello=world` and `greeting=good%20day`
+ /// let _m2 = s.mock("GET", "/test")
+ /// .match_query(Matcher::AllOf(vec![
+ /// Matcher::UrlEncoded("hello".into(), "world".into()),
+ /// Matcher::UrlEncoded("greeting".into(), "good day".into())
+ /// ]))
+ /// .create();
+ ///
+ /// // You can achieve similar results with the regex matcher
+ /// let _m3 = s.mock("GET", "/test")
+ /// .match_query(Matcher::Regex("hello=world".into()))
+ /// .create();
+ /// ```
+ ///
+ pub fn match_query>(mut self, query: M) -> Self {
+ let new_path = match &self.inner.path {
+ PathAndQueryMatcher::Unified(matcher) => {
+ PathAndQueryMatcher::Split(Box::new(matcher.clone()), Box::new(query.into()))
+ }
+ PathAndQueryMatcher::Split(path, _) => {
+ PathAndQueryMatcher::Split(path.clone(), Box::new(query.into()))
+ }
+ };
+
+ self.inner.path = new_path;
+
+ self
+ }
+
+ ///
+ /// Allows matching a particular request header when responding with a mock.
+ ///
+ /// When matching a request, the field letter case is ignored.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m = s.mock("GET", "/").match_header("content-type", "application/json");
+ /// ```
+ ///
+ /// Like most other `Mock` methods, it allows chanining:
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m = s.mock("GET", "/")
+ /// .match_header("content-type", "application/json")
+ /// .match_header("authorization", "password");
+ /// ```
+ ///
+ pub fn match_header>(mut self, field: &str, value: M) -> Self {
+ self.inner
+ .headers
+ .push((field.to_owned().to_lowercase(), value.into()));
+
+ self
+ }
+
+ ///
+ /// Allows matching a particular request body when responding with a mock.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m1 = s.mock("POST", "/").match_body(r#"{"hello": "world"}"#).with_body("json").create();
+ /// let _m2 = s.mock("POST", "/").match_body("hello=world").with_body("form").create();
+ ///
+ /// // Requests passing `{"hello": "world"}` inside the body will be responded with "json".
+ /// // Requests passing `hello=world` inside the body will be responded with "form".
+ ///
+ /// // Create a temporary file
+ /// use std::env;
+ /// use std::fs::File;
+ /// use std::io::Write;
+ /// use std::path::Path;
+ /// use rand;
+ /// use rand::Rng;
+ ///
+ /// let random_bytes: Vec = (0..1024).map(|_| rand::random::()).collect();
+ ///
+ /// let mut tmp_file = env::temp_dir();
+ /// tmp_file.push("test_file.txt");
+ /// let mut f_write = File::create(tmp_file.clone()).unwrap();
+ /// f_write.write_all(random_bytes.as_slice()).unwrap();
+ /// let mut f_read = File::open(tmp_file.clone()).unwrap();
+ ///
+ ///
+ /// // the following are equivalent ways of defining a mock matching
+ /// // a binary payload
+ /// let _b1 = s.mock("POST", "/").match_body(tmp_file.as_path()).create();
+ /// let _b3 = s.mock("POST", "/").match_body(random_bytes).create();
+ /// let _b2 = s.mock("POST", "/").match_body(&mut f_read).create();
+ /// ```
+ ///
+ pub fn match_body>(mut self, body: M) -> Self {
+ self.inner.body = body.into();
+
+ self
+ }
+
+ ///
+ /// Sets the status code of the mock response. The default status code is 200.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m = s.mock("GET", "/").with_status(201);
+ /// ```
+ ///
+ #[track_caller]
+ pub fn with_status(mut self, status: usize) -> Self {
+ self.inner.response.status = StatusCode::from_u16(status as u16)
+ .map_err(|_| Error::new_with_context(ErrorKind::InvalidStatusCode, status))
+ .unwrap();
+
+ self
+ }
+
+ ///
+ /// Sets a header of the mock response.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m = s.mock("GET", "/").with_header("content-type", "application/json");
+ /// ```
+ ///
+ pub fn with_header(mut self, field: &str, value: &str) -> Self {
+ self.inner
+ .response
+ .headers
+ .push((field.to_owned(), value.to_owned()));
+
+ self
+ }
+
+ ///
+ /// Sets the body of the mock response. Its `Content-Length` is handled automatically.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m = s.mock("GET", "/").with_body("hello world");
+ /// ```
+ ///
+ pub fn with_body>(mut self, body: StrOrBytes) -> Self {
+ self.inner.response.body = Body::Bytes(body.as_ref().to_owned());
+ self
+ }
+
+ ///
+ /// Sets the body of the mock response dynamically. The response will use chunked transfer encoding.
+ ///
+ /// The function must be thread-safe. If it's a closure, it can't be borrowing its context.
+ /// Use `move` closures and `Arc` to share any data.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m = s.mock("GET", "/").with_body_from_fn(|w| w.write_all(b"hello world"));
+ /// ```
+ ///
+ pub fn with_body_from_fn(
+ mut self,
+ cb: impl Fn(&mut dyn io::Write) -> io::Result<()> + Send + Sync + 'static,
+ ) -> Self {
+ self.inner.response.body = Body::Fn(Arc::new(cb));
+ self
+ }
+
+ ///
+ /// Sets the body of the mock response from the contents of a file stored under `path`.
+ /// Its `Content-Length` is handled automatically.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m = s.mock("GET", "/").with_body_from_file("tests/files/simple.http");
+ /// ```
+ ///
+ #[track_caller]
+ pub fn with_body_from_file(mut self, path: impl AsRef) -> Self {
+ self.inner.response.body = Body::Bytes(
+ std::fs::read(path)
+ .map_err(|_| Error::new(ErrorKind::FileNotFound))
+ .unwrap(),
+ );
+ self
+ }
+
+ ///
+ /// Sets the expected amount of requests that this mock is supposed to receive.
+ /// This is only enforced when calling the `assert` method.
+ /// Defaults to 1 request.
+ ///
+ #[allow(clippy::missing_const_for_fn)]
+ pub fn expect(mut self, hits: usize) -> Self {
+ self.inner.expected_hits_at_least = Some(hits);
+ self.inner.expected_hits_at_most = Some(hits);
+ self
+ }
+
+ ///
+ /// Sets the minimum amount of requests that this mock is supposed to receive.
+ /// This is only enforced when calling the `assert` method.
+ ///
+ pub fn expect_at_least(mut self, hits: usize) -> Self {
+ self.inner.expected_hits_at_least = Some(hits);
+ if self.inner.expected_hits_at_most.is_some()
+ && self.inner.expected_hits_at_most < self.inner.expected_hits_at_least
+ {
+ self.inner.expected_hits_at_most = None;
+ }
+ self
+ }
+
+ ///
+ /// Sets the maximum amount of requests that this mock is supposed to receive.
+ /// This is only enforced when calling the `assert` method.
+ ///
+ pub fn expect_at_most(mut self, hits: usize) -> Self {
+ self.inner.expected_hits_at_most = Some(hits);
+ if self.inner.expected_hits_at_least.is_some()
+ && self.inner.expected_hits_at_least > self.inner.expected_hits_at_most
+ {
+ self.inner.expected_hits_at_least = None;
+ }
+ self
+ }
+
+ ///
+ /// Asserts that the expected amount of requests (defaults to 1 request) were performed.
+ ///
+ pub fn assert(&self) {
+ crate::RUNTIME.block_on(async { self.assert_async().await })
+ }
+
+ ///
+ /// Same as `Mock::assert` but async.
+ ///
+ pub async fn assert_async(&self) {
+ let mut opt_message = None;
+
+ {
+ let hits = Command::get_mock_hits(&self.sender, self.inner.id.clone()).await;
+
+ if let Some(hits) = hits {
+ let mut message = match (
+ self.inner.expected_hits_at_least,
+ self.inner.expected_hits_at_most,
+ ) {
+ (Some(min), Some(max)) if min == max => format!(
+ "\n> Expected {} request(s) to:\n{}\n...but received {}\n\n",
+ min, self, hits
+ ),
+ (Some(min), Some(max)) => format!(
+ "\n> Expected between {} and {} request(s) to:\n{}\n...but received {}\n\n",
+ min, max, self, hits
+ ),
+ (Some(min), None) => format!(
+ "\n> Expected at least {} request(s) to:\n{}\n...but received {}\n\n",
+ min, self, hits
+ ),
+ (None, Some(max)) => format!(
+ "\n> Expected at most {} request(s) to:\n{}\n...but received {}\n\n",
+ max, self, hits
+ ),
+ (None, None) => format!(
+ "\n> Expected 1 request(s) to:\n{}\n...but received {}\n\n",
+ self, hits
+ ),
+ };
+
+ if let Some(last_request) = Command::get_last_unmatched_request(&self.sender).await
+ {
+ message.push_str(&format!(
+ "> The last unmatched request was:\n{}\n",
+ last_request
+ ));
+
+ let difference = diff::compare(&self.to_string(), &last_request);
+ message.push_str(&format!("> Difference:\n{}\n", difference));
+ }
+
+ opt_message = Some(message);
+ }
+ }
+
+ if let Some(message) = opt_message {
+ assert!(self.matched_async().await, "{}", message)
+ } else {
+ panic!("Could not retrieve enough information about the remote mock.")
+ }
+ }
+
+ ///
+ /// Returns whether the expected amount of requests (defaults to 1) were performed.
+ ///
+ pub fn matched(&self) -> bool {
+ crate::RUNTIME.block_on(async { self.matched_async().await })
+ }
+
+ ///
+ /// Same as `Mock::matched` but async.
+ ///
+ pub async fn matched_async(&self) -> bool {
+ let Some(hits) = Command::get_mock_hits(&self.sender, self.inner.id.clone()).await else {
+ return false;
+ };
+
+ match (
+ self.inner.expected_hits_at_least,
+ self.inner.expected_hits_at_most,
+ ) {
+ (Some(min), Some(max)) => hits >= min && hits <= max,
+ (Some(min), None) => hits >= min,
+ (None, Some(max)) => hits <= max,
+ (None, None) => hits == 1,
+ }
+ }
+
+ ///
+ /// Registers the mock to the server - your mock will be served only after calling this method.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m = s.mock("GET", "/").with_body("hello world").create();
+ /// ```
+ ///
+ #[must_use]
+ pub fn create(self) -> Mock {
+ crate::RUNTIME.block_on(async { self.create_async().await })
+ }
+
+ ///
+ /// Same as `Mock::create` but async.
+ ///
+ pub async fn create_async(mut self) -> Mock {
+ let remote_mock = RemoteMock::new(self.inner.clone());
+ let created = Command::create_mock(&self.sender, remote_mock).await;
+
+ self.created = created;
+
+ self
+ }
+}
+
+impl Drop for Mock {
+ fn drop(&mut self) {
+ let sender = self.sender.clone();
+ let id = self.inner.id.clone();
+
+ futures::executor::block_on(async move {
+ Command::remove_mock(&sender, id).await;
+ });
+
+ log::debug!("Mock::drop() called for {}", self);
+
+ if !self.created {
+ log::warn!("Missing .create() call on mock {}", self);
+ }
+ }
+}
+
+impl fmt::Display for Mock {
+ #[allow(deprecated)]
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut formatted = String::new();
+ formatted.push_str(&self.inner.to_string());
+ f.write_str(&formatted)
+ }
+}
+
+impl PartialEq for Mock {
+ fn eq(&self, other: &Self) -> bool {
+ self.inner == other.inner
+ }
+}
diff --git a/src/request.rs b/src/request.rs
index 61619b7..2f001b7 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -1,223 +1,84 @@
-use std::convert::From;
-use std::default::Default;
-use std::fmt;
-use std::io::{self, BufRead, BufReader, Cursor, Read};
-use std::mem;
-use std::net::TcpStream;
-use std::str;
+use crate::{Error, ErrorKind};
+use hyper::body;
+use hyper::body::Buf;
+use hyper::Body as HyperBody;
+use hyper::Request as HyperRequest;
#[derive(Debug)]
-pub struct Request {
- pub version: (u8, u8),
- pub method: String,
- pub path: String,
- pub headers: Vec<(String, String)>,
- pub body: Vec,
- error: Option,
- is_parsed: bool,
- last_header_field: Option,
- last_header_value: Option,
+pub(crate) struct Request {
+ inner: HyperRequest,
+ body: Option>,
}
impl Request {
- pub fn is_head(&self) -> bool {
- self.method == "HEAD"
- }
-
- #[allow(clippy::missing_const_for_fn)]
- pub fn is_ok(&self) -> bool {
- self.error.is_none()
+ pub fn new(request: HyperRequest) -> Self {
+ Request {
+ inner: request,
+ body: None,
+ }
}
- #[allow(clippy::missing_const_for_fn)]
- pub fn is_err(&self) -> bool {
- self.error.is_some()
+ pub fn method(&self) -> &str {
+ self.inner.method().as_ref()
}
- #[allow(clippy::missing_const_for_fn)]
- pub fn error(&self) -> Option<&String> {
- self.error.as_ref()
+ pub fn path_and_query(&self) -> &str {
+ self.inner
+ .uri()
+ .path_and_query()
+ .map(|pq| pq.as_str())
+ .unwrap_or("")
}
- pub fn find_header_values(&self, searched_field: &str) -> Vec<&str> {
- self.headers
+ pub fn header(&self, field: &str) -> Vec<&str> {
+ self.inner
+ .headers()
+ .get_all(field)
.iter()
- .filter_map(|(field, value)| {
- if field == searched_field {
- Some(value.as_str())
- } else {
- None
- }
- })
- .collect()
+ .map(|item| item.to_str().unwrap())
+ .collect::>()
}
- fn record_last_header(&mut self) {
- if self.last_header_field.is_some() && self.last_header_value.is_some() {
- let last_header_field = mem::replace(&mut self.last_header_field, None).unwrap();
- let last_header_value = mem::replace(&mut self.last_header_value, None).unwrap();
- self.headers
- .push((last_header_field.to_lowercase(), last_header_value));
- }
+ pub fn has_header(&self, header_name: &str) -> bool {
+ self.inner.headers().contains_key(header_name)
}
- fn content_length(&self) -> Option {
- use std::str::FromStr;
-
- self.find_header_values("content-length")
- .first()
- .and_then(|len| usize::from_str(*len).ok())
- }
-
- fn has_chunked_body(&self) -> bool {
- self.headers
- .iter()
- .filter(|(name, _)| name == "transfer-encoding")
- .any(|(_, value)| value.contains("chunked"))
- }
-
- fn read_request_body(&mut self, mut raw_body_rd: impl Read) -> io::Result<()> {
- if let Some(content_length) = self.content_length() {
- let mut rd = raw_body_rd.take(content_length as u64);
- return io::copy(&mut rd, &mut self.body).map(|_| ());
- }
-
- if self.has_chunked_body() {
- let mut chunk_size_buf = String::new();
- let mut reader = BufReader::new(raw_body_rd);
-
- loop {
- reader.read_line(&mut chunk_size_buf)?;
-
- let chunk_size = {
- let chunk_size_str = chunk_size_buf.trim_matches(|c| c == '\r' || c == '\n');
-
- u64::from_str_radix(chunk_size_str, 16)
- .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?
- };
-
- if chunk_size == 0 {
- break;
- }
-
- io::copy(&mut (&mut reader).take(chunk_size), &mut self.body)?;
-
- chunk_size_buf.clear();
- reader.read_line(&mut chunk_size_buf)?;
- }
-
- return Ok(());
+ pub async fn read_body(&mut self) -> &Vec {
+ if self.body.is_none() {
+ let raw_body = self.inner.body_mut();
+ let mut buf = body::aggregate(raw_body)
+ .await
+ .map_err(|err| Error::new_with_context(ErrorKind::RequestBodyFailure, err))
+ .unwrap();
+ let bytes = buf.copy_to_bytes(buf.remaining()).to_vec();
+ self.body = Some(bytes);
}
- if self.version == (1, 0) {
- return io::copy(&mut raw_body_rd, &mut self.body).map(|_| ());
- }
-
- Ok(())
- }
-}
-
-impl Default for Request {
- fn default() -> Self {
- Self {
- version: (1, 1),
- method: String::new(),
- path: String::new(),
- headers: Vec::new(),
- body: Vec::new(),
- error: None,
- is_parsed: false,
- last_header_field: None,
- last_header_value: None,
- }
+ self.body.as_ref().unwrap()
}
-}
-
-impl<'a> From<&'a TcpStream> for Request {
- fn from(mut stream: &TcpStream) -> Self {
- let mut request = Self::default();
-
- let mut all_buf = Vec::new();
-
- loop {
- if request.is_parsed {
- break;
- }
-
- let mut headers = [httparse::EMPTY_HEADER; 16];
- let mut req = httparse::Request::new(&mut headers);
- let mut buf = [0; 1024];
-
- let rlen = match stream.read(&mut buf) {
- Err(e) => Err(e.to_string()),
- Ok(0) => Err("Nothing to read.".into()),
- Ok(i) => Ok(i),
- }
- .map_err(|e| request.error = Some(e))
- .unwrap_or(0);
-
- if rlen == 0 {
- break;
- }
-
- all_buf.extend_from_slice(&buf[..rlen]);
- let _ = req
- .parse(&all_buf)
- .map_err(|e| {
- request.error = Some(e.to_string());
- request.is_parsed = true;
- })
- .map(|status| match status {
- httparse::Status::Complete(head_length) => {
- if let Some(a @ 0..=1) = req.version {
- request.version = (1, a);
- }
-
- if let Some(a) = req.method {
- request.method += a;
- }
-
- if let Some(a) = req.path {
- request.path += a
- }
-
- for h in req.headers {
- request.last_header_field = Some(h.name.to_lowercase());
- request.last_header_value =
- Some(String::from_utf8_lossy(h.value).to_string());
-
- request.record_last_header();
- }
-
- let raw_body_rd = Cursor::new(&all_buf[head_length..]).chain(stream);
-
- if let Err(err) = request.read_request_body(raw_body_rd) {
- request.error = Some(err.to_string());
- }
-
- request.is_parsed = true;
- }
- httparse::Status::Partial => (),
- });
+ #[allow(clippy::wrong_self_convention)]
+ pub(crate) async fn to_string(&mut self) -> String {
+ let mut formatted = format!(
+ "\r\n{} {}\r\n",
+ &self.inner.method(),
+ &self.inner.uri().path()
+ );
+
+ for (key, value) in self.inner.headers() {
+ formatted.push_str(&format!(
+ "{}: {}\r\n",
+ key,
+ value.to_str().unwrap_or("")
+ ));
}
- request
- }
-}
+ let body = self.read_body().await;
-impl fmt::Display for Request {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "\r\n{} {}\r\n", &self.method, &self.path)?;
-
- for &(ref key, ref value) in &self.headers {
- writeln!(f, "{}: {}\r", key, value)?;
+ if !body.is_empty() {
+ formatted.push_str(&format!("{}\r\n", &String::from_utf8_lossy(body)));
}
- if self.body.is_empty() {
- write!(f, "")
- } else {
- writeln!(f, "{}\r", &String::from_utf8_lossy(&self.body))
- }
+ formatted
}
}
diff --git a/src/response.rs b/src/response.rs
index 52c8b35..ead4113 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -1,11 +1,14 @@
-use std::convert::From;
+use core::task::Poll;
+use futures::stream::Stream;
+use hyper::StatusCode;
use std::fmt;
use std::io;
+use std::mem;
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Response {
- pub status: Status,
+ pub status: StatusCode,
pub headers: Vec<(String, String)>,
pub body: Body,
}
@@ -43,254 +46,57 @@ impl PartialEq for Body {
impl Default for Response {
fn default() -> Self {
Self {
- status: Status::Ok,
+ status: StatusCode::OK,
headers: vec![("connection".into(), "close".into())],
body: Body::Bytes(Vec::new()),
}
}
}
-pub(crate) struct Chunked {
- writer: W,
+pub(crate) struct Chunked {
+ buffer: Vec,
+ finished: bool,
}
-impl Chunked {
- pub fn new(writer: W) -> Self {
- Self { writer }
- }
-
- pub fn finish(mut self) -> io::Result {
- self.writer.write_all(b"0\r\n\r\n")?;
- Ok(self.writer)
- }
-}
-
-impl io::Write for Chunked {
- fn write(&mut self, buf: &[u8]) -> io::Result {
- if buf.is_empty() {
- return Ok(0);
+impl Chunked {
+ pub fn new() -> Self {
+ Self {
+ buffer: vec![],
+ finished: false,
}
- self.writer
- .write_all(format!("{:x}\r\n", buf.len()).as_bytes())?;
- self.writer.write_all(buf)?;
- self.writer.write_all(b"\r\n")?;
- Ok(buf.len())
}
- fn flush(&mut self) -> io::Result<()> {
- self.writer.flush()
+ pub fn finish(&mut self) {
+ self.finished = true;
}
}
-#[derive(Clone, Debug, PartialEq)]
-#[cfg_attr(feature = "cargo-clippy", allow(clippy::enum_variant_names))]
-pub enum Status {
- Continue,
- SwitchingProtocols,
- Processing,
- Ok,
- Created,
- Accepted,
- NonAuthoritativeInformation,
- NoContent,
- ResetContent,
- PartialContent,
- MultiStatus,
- AlreadyReported,
- IMUsed,
- MultipleChoices,
- MovedPermanently,
- Found,
- SeeOther,
- NotModified,
- UseProxy,
- TemporaryRedirect,
- PermanentRedirect,
- BadRequest,
- Unauthorized,
- PaymentRequired,
- Forbidden,
- NotFound,
- MethodNotAllowed,
- NotAcceptable,
- ProxyAuthenticationRequired,
- RequestTimeout,
- Conflict,
- Gone,
- LengthRequired,
- PreconditionFailed,
- PayloadTooLarge,
- RequestURITooLong,
- UnsupportedMediaType,
- RequestedRangeNotSatisfiable,
- ExpectationFailed,
- ImATeapot,
- MisdirectedRequest,
- UnprocessableEntity,
- Locked,
- FailedDependency,
- UpgradeRequired,
- PreconditionRequired,
- TooManyRequests,
- RequestHeaderFieldsTooLarge,
- ConnectionClosedWithoutResponse,
- UnavailableForLegalReasons,
- ClientClosedRequest,
- InternalServerError,
- NotImplemented,
- BadGateway,
- ServiceUnavailable,
- GatewayTimeout,
- HTTPVersionNotSupported,
- VariantAlsoNegotiates,
- InsufficientStorage,
- LoopDetected,
- NotExtended,
- NetworkAuthenticationRequired,
- NetworkConnectTimeoutError,
- Custom(String),
-}
+impl Stream for Chunked {
+ type Item = Result, String>;
-impl From for Status {
- fn from(status_code: usize) -> Self {
- match status_code {
- 100 => Self::Continue,
- 101 => Self::SwitchingProtocols,
- 102 => Self::Processing,
- 200 => Self::Ok,
- 201 => Self::Created,
- 202 => Self::Accepted,
- 203 => Self::NonAuthoritativeInformation,
- 204 => Self::NoContent,
- 205 => Self::ResetContent,
- 206 => Self::PartialContent,
- 207 => Self::MultiStatus,
- 208 => Self::AlreadyReported,
- 226 => Self::IMUsed,
- 300 => Self::MultipleChoices,
- 301 => Self::MovedPermanently,
- 302 => Self::Found,
- 303 => Self::SeeOther,
- 304 => Self::NotModified,
- 305 => Self::UseProxy,
- 307 => Self::TemporaryRedirect,
- 308 => Self::PermanentRedirect,
- 400 => Self::BadRequest,
- 401 => Self::Unauthorized,
- 402 => Self::PaymentRequired,
- 403 => Self::Forbidden,
- 404 => Self::NotFound,
- 405 => Self::MethodNotAllowed,
- 406 => Self::NotAcceptable,
- 407 => Self::ProxyAuthenticationRequired,
- 408 => Self::RequestTimeout,
- 409 => Self::Conflict,
- 410 => Self::Gone,
- 411 => Self::LengthRequired,
- 412 => Self::PreconditionFailed,
- 413 => Self::PayloadTooLarge,
- 414 => Self::RequestURITooLong,
- 415 => Self::UnsupportedMediaType,
- 416 => Self::RequestedRangeNotSatisfiable,
- 417 => Self::ExpectationFailed,
- 418 => Self::ImATeapot,
- 421 => Self::MisdirectedRequest,
- 422 => Self::UnprocessableEntity,
- 423 => Self::Locked,
- 424 => Self::FailedDependency,
- 426 => Self::UpgradeRequired,
- 428 => Self::PreconditionRequired,
- 429 => Self::TooManyRequests,
- 431 => Self::RequestHeaderFieldsTooLarge,
- 444 => Self::ConnectionClosedWithoutResponse,
- 451 => Self::UnavailableForLegalReasons,
- 499 => Self::ClientClosedRequest,
- 500 => Self::InternalServerError,
- 501 => Self::NotImplemented,
- 502 => Self::BadGateway,
- 503 => Self::ServiceUnavailable,
- 504 => Self::GatewayTimeout,
- 505 => Self::HTTPVersionNotSupported,
- 506 => Self::VariantAlsoNegotiates,
- 507 => Self::InsufficientStorage,
- 508 => Self::LoopDetected,
- 510 => Self::NotExtended,
- 511 => Self::NetworkAuthenticationRequired,
- 599 => Self::NetworkConnectTimeoutError,
- _ => Status::Custom(format!("{} Custom", status_code)),
+ fn poll_next(
+ mut self: std::pin::Pin<&mut Self>,
+ _cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll> {
+ if !self.buffer.is_empty() {
+ let data = mem::take(&mut self.buffer);
+ Poll::Ready(Some(Ok(data)))
+ } else if !self.finished {
+ Poll::Pending
+ } else {
+ Poll::Ready(None)
}
}
}
-impl fmt::Display for Status {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let formatted = match self {
- Status::Continue => "100 Continue",
- Status::SwitchingProtocols => "101 Switching Protocols",
- Status::Processing => "102 Processing",
- Status::Ok => "200 OK",
- Status::Created => "201 Created",
- Status::Accepted => "202 Accepted",
- Status::NonAuthoritativeInformation => "203 Non-Authoritative Information",
- Status::NoContent => "204 No Content",
- Status::ResetContent => "205 Reset Content",
- Status::PartialContent => "206 Partial Content",
- Status::MultiStatus => "207 Multi-Status",
- Status::AlreadyReported => "208 Already Reported",
- Status::IMUsed => "226 IM Used",
- Status::MultipleChoices => "300 Multiple Choices",
- Status::MovedPermanently => "301 Moved Permanently",
- Status::Found => "302 Found",
- Status::SeeOther => "303 See Other",
- Status::NotModified => "304 Not Modified",
- Status::UseProxy => "305 Use Proxy",
- Status::TemporaryRedirect => "307 Temporary Redirect",
- Status::PermanentRedirect => "308 Permanent Redirect",
- Status::BadRequest => "400 Bad Request",
- Status::Unauthorized => "401 Unauthorized",
- Status::PaymentRequired => "402 Payment Required",
- Status::Forbidden => "403 Forbidden",
- Status::NotFound => "404 Not Found",
- Status::MethodNotAllowed => "405 Method Not Allowed",
- Status::NotAcceptable => "406 Not Acceptable",
- Status::ProxyAuthenticationRequired => "407 Proxy Authentication Required",
- Status::RequestTimeout => "408 Request Timeout",
- Status::Conflict => "409 Conflict",
- Status::Gone => "410 Gone",
- Status::LengthRequired => "411 Length Required",
- Status::PreconditionFailed => "412 Precondition Failed",
- Status::PayloadTooLarge => "413 Payload Too Large",
- Status::RequestURITooLong => "414 Request-URI Too Long",
- Status::UnsupportedMediaType => "415 Unsupported Media Type",
- Status::RequestedRangeNotSatisfiable => "416 Requested Range Not Satisfiable",
- Status::ExpectationFailed => "417 Expectation Failed",
- Status::ImATeapot => "418 I'm a teapot",
- Status::MisdirectedRequest => "421 Misdirected Request",
- Status::UnprocessableEntity => "422 Unprocessable Entity",
- Status::Locked => "423 Locked",
- Status::FailedDependency => "424 Failed Dependency",
- Status::UpgradeRequired => "426 Upgrade Required",
- Status::PreconditionRequired => "428 Precondition Required",
- Status::TooManyRequests => "429 Too Many Requests",
- Status::RequestHeaderFieldsTooLarge => "431 Request Header Fields Too Large",
- Status::ConnectionClosedWithoutResponse => "444 Connection Closed Without Response",
- Status::UnavailableForLegalReasons => "451 Unavailable For Legal Reasons",
- Status::ClientClosedRequest => "499 Client Closed Request",
- Status::InternalServerError => "500 Internal Server Error",
- Status::NotImplemented => "501 Not Implemented",
- Status::BadGateway => "502 Bad Gateway",
- Status::ServiceUnavailable => "503 Service Unavailable",
- Status::GatewayTimeout => "504 Gateway Timeout",
- Status::HTTPVersionNotSupported => "505 HTTP Version Not Supported",
- Status::VariantAlsoNegotiates => "506 Variant Also Negotiates",
- Status::InsufficientStorage => "507 Insufficient Storage",
- Status::LoopDetected => "508 Loop Detected",
- Status::NotExtended => "510 Not Extended",
- Status::NetworkAuthenticationRequired => "511 Network Authentication Required",
- Status::NetworkConnectTimeoutError => "599 Network Connect Timeout Error",
- Status::Custom(ref status_code) => status_code,
- };
+impl io::Write for Chunked {
+ fn write(&mut self, buf: &[u8]) -> io::Result {
+ self.buffer.append(&mut buf.to_vec());
+ Ok(buf.len())
+ }
- write!(f, "{}", formatted)
+ fn flush(&mut self) -> io::Result<()> {
+ self.finished = true;
+ Ok(())
}
}
diff --git a/src/server.rs b/src/server.rs
index 5b6ae23..922d462 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -1,266 +1,363 @@
-use crate::response::{Body, Chunked};
-use crate::{Mock, Request, SERVER_ADDRESS_INTERNAL};
-use std::fmt::Display;
-use std::io;
-use std::io::Write;
-use std::net::{SocketAddr, TcpListener, TcpStream};
-use std::sync::mpsc;
-use std::sync::Mutex;
+use crate::command::Command;
+use crate::mock::InnerMock;
+use crate::request::Request;
+use crate::response::{Body as ResponseBody, Chunked as ResponseChunked};
+use crate::server_pool::SERVER_POOL;
+use crate::{Error, ErrorKind, Matcher, Mock};
+use futures::stream::{self, StreamExt};
+use hyper::service::{make_service_fn, service_fn};
+use hyper::{Body, Request as HyperRequest, Response, Server as HyperServer, StatusCode};
+use std::net::{SocketAddr, TcpListener};
+use std::ops::DerefMut;
+use std::sync::Arc;
use std::thread;
+use tokio::sync::mpsc::{self, Receiver, Sender};
+use tokio::sync::Mutex;
+use tokio::task::LocalSet;
+
+#[derive(Clone, Debug)]
+pub(crate) struct RemoteMock {
+ pub(crate) inner: InnerMock,
+}
+
+impl RemoteMock {
+ pub(crate) fn new(inner: InnerMock) -> Self {
+ RemoteMock { inner }
+ }
+
+ async fn matches(&self, other: &mut Request) -> bool {
+ self.method_matches(other)
+ && self.path_matches(other)
+ && self.headers_match(other)
+ && self.body_matches(other).await
+ }
-impl Mock {
fn method_matches(&self, request: &Request) -> bool {
- self.method == request.method
+ self.inner.method.as_str() == request.method()
}
fn path_matches(&self, request: &Request) -> bool {
- self.path.matches_value(&request.path)
+ self.inner.path.matches_value(request.path_and_query())
}
fn headers_match(&self, request: &Request) -> bool {
- self.headers.iter().all(|&(ref field, ref expected)| {
- expected.matches_values(&request.find_header_values(field))
- })
+ self.inner
+ .headers
+ .iter()
+ .all(|&(ref field, ref expected)| expected.matches_values(&request.header(field)))
}
- fn body_matches(&self, request: &Request) -> bool {
- let raw_body = &request.body;
- let safe_body = &String::from_utf8_lossy(raw_body);
+ async fn body_matches(&self, request: &mut Request) -> bool {
+ let body = request.read_body().await;
+ let safe_body = &String::from_utf8_lossy(body);
- self.body.matches_value(safe_body) || self.body.matches_binary_value(raw_body)
+ self.inner.body.matches_value(safe_body) || self.inner.body.matches_binary_value(body)
}
#[allow(clippy::missing_const_for_fn)]
fn is_missing_hits(&self) -> bool {
- match (self.expected_hits_at_least, self.expected_hits_at_most) {
- (Some(_at_least), Some(at_most)) => self.hits < at_most,
- (Some(at_least), None) => self.hits < at_least,
- (None, Some(at_most)) => self.hits < at_most,
- (None, None) => self.hits < 1,
+ match (
+ self.inner.expected_hits_at_least,
+ self.inner.expected_hits_at_most,
+ ) {
+ (Some(_at_least), Some(at_most)) => self.inner.hits < at_most,
+ (Some(at_least), None) => self.inner.hits < at_least,
+ (None, Some(at_most)) => self.inner.hits < at_most,
+ (None, None) => self.inner.hits < 1,
}
}
}
-impl<'a> PartialEq for &'a mut Mock {
- fn eq(&self, other: &Request) -> bool {
- self.method_matches(other)
- && self.path_matches(other)
- && self.headers_match(other)
- && self.body_matches(other)
- }
-}
-
-pub struct State {
- pub listening_addr: Option,
- pub mocks: Vec,
- pub unmatched_requests: Vec,
+#[derive(Debug)]
+pub(crate) struct State {
+ pub(crate) mocks: Vec,
+ pub(crate) unmatched_requests: Vec,
}
impl State {
- #[allow(clippy::missing_const_for_fn)]
fn new() -> Self {
- Self {
- listening_addr: None,
- mocks: Vec::new(),
- unmatched_requests: Vec::new(),
+ State {
+ mocks: vec![],
+ unmatched_requests: vec![],
}
}
}
-lazy_static! {
- pub static ref STATE: Mutex = Mutex::new(State::new());
-}
-
-/// Address and port of the local server.
-/// Can be used with `std::net::TcpStream`.
///
-/// The server will be started if necessary.
-pub fn address() -> SocketAddr {
- try_start();
-
- let state = STATE.lock().map(|state| state.listening_addr);
- state
- .expect("state lock")
- .expect("server should be listening")
-}
-
-/// A local `http://…` URL of the server.
+/// One instance of the mock server.
///
-/// The server will be started if necessary.
-pub fn url() -> String {
- format!("http://{}", address())
+/// Mockito uses a server pool to manage running servers. Once the pool reaches capacity,
+/// new requests will have to wait for a free server. The size of the server pool
+/// is set to 100.
+///
+/// Most of the times, you should initialize new servers with `Server::new`, which fetches
+/// the next available instance from the pool:
+///
+/// ```
+/// let mut server = mockito::Server::new();
+/// ```
+///
+/// If for any reason you'd like to bypass the server pool, you can use `Server::new_with_port`:
+///
+/// ```
+/// let mut server = mockito::Server::new_with_port(0);
+/// ```
+///
+#[derive(Debug)]
+pub struct Server {
+ address: String,
+ state: Arc>,
+ sender: Sender,
}
-pub fn try_start() {
- let mut state = STATE.lock().unwrap();
+impl Server {
+ ///
+ /// Fetches a new mock server from the server pool.
+ ///
+ /// This method will panic on failure.
+ ///
+ /// If for any reason you'd like to bypass the server pool, you can use `Server::new_with_port`:
+ ///
+ #[track_caller]
+ pub fn new() -> impl DerefMut {
+ Server::try_new().unwrap()
+ }
- if state.listening_addr.is_some() {
- return;
+ ///
+ /// Same as `Server::new` but async.
+ ///
+ pub async fn new_async() -> impl DerefMut {
+ SERVER_POOL.get().await.unwrap()
}
- let (tx, rx) = mpsc::channel();
+ ///
+ /// Same as `Server::new` but won't panic on failure.
+ ///
+ pub(crate) fn try_new() -> Result, Error> {
+ crate::RUNTIME.block_on(async { Server::try_new_async().await })
+ }
- thread::spawn(move || {
- let res = TcpListener::bind(SERVER_ADDRESS_INTERNAL).or_else(|err| {
- warn!("{}", err);
- TcpListener::bind("127.0.0.1:0")
- });
- let (listener, addr) = match res {
- Ok(listener) => {
- let addr = listener.local_addr().unwrap();
- tx.send(Some(addr)).unwrap();
- (listener, addr)
- }
- Err(err) => {
- error!("{}", err);
- tx.send(None).unwrap();
- return;
+ ///
+ /// Same as `Server::try_new` but async.
+ ///
+ pub(crate) async fn try_new_async() -> Result, Error> {
+ SERVER_POOL
+ .get()
+ .await
+ .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))
+ }
+
+ ///
+ /// Starts a new server on a given port. If the port is set to `0`, a random available
+ /// port will be assigned. Note that **this call bypasses the server pool**.
+ ///
+ /// This method will panic on failure.
+ ///
+ #[track_caller]
+ pub fn new_with_port(port: u16) -> Server {
+ Server::try_new_with_port(port).unwrap()
+ }
+
+ ///
+ /// Same as `Server::try_new_with_port_async` but async.
+ ///
+ pub async fn new_with_port_async(port: u16) -> Server {
+ Server::try_new_with_port_async(port).await.unwrap()
+ }
+
+ ///
+ /// Same as `Server::new_with_port` but won't panic on failure.
+ ///
+ pub(crate) fn try_new_with_port(port: u16) -> Result {
+ crate::RUNTIME.block_on(async { Server::try_new_with_port_async(port).await })
+ }
+
+ ///
+ /// Same as `Server::try_new_with_port` but async.
+ ///
+ pub(crate) async fn try_new_with_port_async(port: u16) -> Result {
+ let state = Arc::new(Mutex::new(State::new()));
+ let address = SocketAddr::from(([127, 0, 0, 1], port));
+
+ let listener = TcpListener::bind(address)
+ .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))?;
+
+ let address = listener
+ .local_addr()
+ .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))?;
+
+ let mutex = state.clone();
+ let service = make_service_fn(move |_conn| {
+ let mutex = mutex.clone();
+ async move {
+ Ok::<_, Error>(service_fn(move |request: HyperRequest| {
+ handle_request(request, mutex.clone())
+ }))
}
+ });
+
+ let server = HyperServer::from_tcp(listener)
+ .map_err(|err| Error::new_with_context(ErrorKind::ServerFailure, err))?
+ .serve(service);
+
+ thread::spawn(move || LocalSet::new().block_on(&crate::RUNTIME, server));
+
+ let (sender, receiver) = mpsc::channel(32);
+
+ let mut server = Server {
+ address: address.to_string(),
+ state,
+ sender,
};
- debug!("Server is listening at {}", addr);
- for stream in listener.incoming() {
- if let Ok(stream) = stream {
- let request = Request::from(&stream);
- debug!("Request received: {}", request);
- if request.is_ok() {
- handle_request(request, stream);
- } else {
- let message = request
- .error()
- .map_or("Could not parse the request.", |err| err.as_str());
- debug!("Could not parse request because: {}", message);
- respond_with_error(stream, request.version, message);
- }
- } else {
- debug!("Could not read from stream");
+ server.accept_commands(receiver).await;
+
+ Ok(server)
+ }
+
+ ///
+ /// Initializes a mock with the given HTTP `method` and `path`.
+ ///
+ /// The mock is enabled on the server only after calling the `Mock::create` method.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// let mut s = mockito::Server::new();
+ ///
+ /// let _m1 = s.mock("GET", "/");
+ /// let _m2 = s.mock("POST", "/users");
+ /// let _m3 = s.mock("DELETE", "/users?id=1");
+ /// ```
+ ///
+ pub fn mock>(&mut self, method: &str, path: P) -> Mock {
+ Mock::new(self.sender.clone(), method, path)
+ }
+
+ ///
+ /// The URL of the mock server (including the protocol).
+ ///
+ pub fn url(&self) -> String {
+ format!("http://{}", self.address)
+ }
+
+ ///
+ /// The host and port of the mock server.
+ /// Can be used with `std::net::TcpStream`.
+ ///
+ pub fn host_with_port(&self) -> String {
+ self.address.clone()
+ }
+
+ ///
+ /// Removes all the mocks stored on the server.
+ ///
+ pub fn reset(&mut self) {
+ futures::executor::block_on(async { self.reset_async().await })
+ }
+
+ ///
+ /// Same as `Server::reset` but async.
+ ///
+ pub async fn reset_async(&mut self) {
+ let state = self.state.clone();
+ let mut state = state.lock().await;
+ state.mocks.clear();
+ state.unmatched_requests.clear();
+ }
+
+ async fn accept_commands(&mut self, mut receiver: Receiver) {
+ let state = self.state.clone();
+ tokio::spawn(async move {
+ while let Some(cmd) = receiver.recv().await {
+ let state = state.lock().await;
+ Command::handle(cmd, state).await;
}
- }
- });
+ });
- state.listening_addr = rx.recv().ok().and_then(|addr| addr);
+ log::debug!("Server is accepting commands");
+ }
}
-fn handle_request(request: Request, stream: TcpStream) {
- handle_match_mock(request, stream);
-}
+async fn handle_request(
+ hyper_request: HyperRequest,
+ state: Arc>,
+) -> Result, Error> {
+ let mut request = Request::new(hyper_request);
+ log::debug!("Request received: {}", request.to_string().await);
-fn handle_match_mock(request: Request, stream: TcpStream) {
- let mut state = STATE.lock().unwrap();
+ let mutex = state.clone();
+ let mut state = mutex.lock().await;
- let mut matchings_mocks = state
- .mocks
- .iter_mut()
- .filter(|mock| mock == &request)
- .collect::>();
+ let mut mocks_stream = stream::iter(&mut state.mocks);
+ let mut matching_mocks: Vec<&mut RemoteMock> = vec![];
- let maybe_missing_hits = matchings_mocks.iter_mut().find(|m| m.is_missing_hits());
+ while let Some(mock) = mocks_stream.next().await {
+ if mock.matches(&mut request).await {
+ matching_mocks.push(mock);
+ }
+ }
+
+ let maybe_missing_hits = matching_mocks.iter_mut().find(|m| m.is_missing_hits());
let mock = match maybe_missing_hits {
Some(m) => Some(m),
- None => matchings_mocks.last_mut(),
+ None => matching_mocks.last_mut(),
};
if let Some(mock) = mock {
- debug!("Mock found");
- mock.hits += 1;
- respond_with_mock(stream, request.version, mock, request.is_head());
+ log::debug!("Mock found");
+ mock.inner.hits += 1;
+ respond_with_mock(request, mock).await
} else {
- debug!("Mock not found");
- respond_with_mock_not_found(stream, request.version);
+ log::debug!("Mock not found");
state.unmatched_requests.push(request);
+ respond_with_mock_not_found()
}
}
-fn respond(
- stream: TcpStream,
- version: (u8, u8),
- status: impl Display,
- headers: Option<&Vec<(String, String)>>,
- body: Option<&str>,
-) {
- let body = body.map(|s| Body::Bytes(s.as_bytes().to_owned()));
- if let Err(e) = respond_bytes(stream, version, status, headers, body.as_ref()) {
- eprintln!("warning: Mock response write error: {}", e);
- }
-}
-
-fn respond_bytes(
- mut stream: TcpStream,
- version: (u8, u8),
- status: impl Display,
- headers: Option<&Vec<(String, String)>>,
- body: Option<&Body>,
-) -> io::Result<()> {
- let mut response = Vec::from(format!("HTTP/{}.{} {}\r\n", version.0, version.1, status));
- let mut has_content_length_header = false;
-
- if let Some(headers) = headers {
- for &(ref key, ref value) in headers {
- response.extend(key.as_bytes());
- response.extend(b": ");
- response.extend(value.as_bytes());
- response.extend(b"\r\n");
- }
+async fn respond_with_mock(request: Request, mock: &RemoteMock) -> Result, Error> {
+ let status: StatusCode = mock.inner.response.status;
+ let mut response = Response::builder().status(status);
- has_content_length_header = headers.iter().any(|(key, _)| key == "content-length");
+ for (name, value) in mock.inner.response.headers.iter() {
+ response = response.header(name, value);
}
- match body {
- Some(Body::Bytes(bytes)) => {
- if !has_content_length_header {
- response.extend(format!("content-length: {}\r\n", bytes.len()).as_bytes());
+ let body = if request.method() != "HEAD" {
+ match &mock.inner.response.body {
+ ResponseBody::Bytes(bytes) => {
+ if !request.has_header("content-length") {
+ response = response.header("content-length", bytes.len());
+ }
+ Body::from(bytes.clone())
+ }
+ ResponseBody::Fn(body_fn) => {
+ let mut chunked = ResponseChunked::new();
+ body_fn(&mut chunked)
+ .map_err(|_| Error::new(ErrorKind::ResponseBodyFailure))
+ .unwrap();
+ chunked.finish();
+
+ Body::wrap_stream(chunked)
}
}
- Some(Body::Fn(_)) => {
- response.extend(b"transfer-encoding: chunked\r\n");
- }
- None => {}
- };
- response.extend(b"\r\n");
- stream.write_all(&response)?;
- match body {
- Some(Body::Bytes(bytes)) => {
- stream.write_all(bytes)?;
- }
- Some(Body::Fn(cb)) => {
- let mut chunked = Chunked::new(&mut stream);
- cb(&mut chunked)?;
- chunked.finish()?;
- }
- None => {}
- };
- stream.flush()
-}
-
-fn respond_with_mock(stream: TcpStream, version: (u8, u8), mock: &Mock, skip_body: bool) {
- let body = if skip_body {
- None
} else {
- Some(&mock.response.body)
+ Body::empty()
};
- if let Err(e) = respond_bytes(
- stream,
- version,
- &mock.response.status,
- Some(&mock.response.headers),
- body,
- ) {
- eprintln!("warning: Mock response write error: {}", e);
- }
-}
+ let response: Response = response
+ .body(body)
+ .map_err(|err| Error::new_with_context(ErrorKind::ResponseFailure, err))?;
-fn respond_with_mock_not_found(stream: TcpStream, version: (u8, u8)) {
- respond(
- stream,
- version,
- "501 Mock Not Found",
- Some(&vec![("content-length".into(), "0".into())]),
- None,
- );
+ Ok(response)
}
-fn respond_with_error(stream: TcpStream, version: (u8, u8), message: &str) {
- respond(stream, version, "422 Mock Error", None, Some(message));
+fn respond_with_mock_not_found() -> Result, Error> {
+ let response: Response = Response::builder()
+ .status(StatusCode::NOT_IMPLEMENTED)
+ .body(Body::empty())
+ .map_err(|err| Error::new_with_context(ErrorKind::ResponseFailure, err))?;
+
+ Ok(response)
}
diff --git a/src/server_pool.rs b/src/server_pool.rs
new file mode 100644
index 0000000..1effe2f
--- /dev/null
+++ b/src/server_pool.rs
@@ -0,0 +1,38 @@
+use crate::Error;
+use crate::Server;
+use async_trait::async_trait;
+use deadpool::managed::{self, Pool};
+use lazy_static::lazy_static;
+
+const DEFAULT_POOL_SIZE: usize = 100;
+
+lazy_static! {
+ pub(crate) static ref SERVER_POOL: Pool = ServerPool::new(DEFAULT_POOL_SIZE);
+}
+
+pub(crate) struct ServerPool {}
+
+impl ServerPool {
+ fn new(max_size: usize) -> Pool {
+ let server_pool = ServerPool {};
+ Pool::builder(server_pool)
+ .max_size(max_size)
+ .build()
+ .expect("Could not create server pool")
+ }
+}
+
+#[async_trait]
+impl managed::Manager for ServerPool {
+ type Type = Server;
+ type Error = Error;
+
+ async fn create(&self) -> Result {
+ Server::try_new_with_port_async(0).await
+ }
+
+ async fn recycle(&self, server: &mut Server) -> managed::RecycleResult {
+ server.reset_async().await;
+ Ok(())
+ }
+}
diff --git a/tests/legacy.rs b/tests/legacy.rs
new file mode 100644
index 0000000..40e0cdd
--- /dev/null
+++ b/tests/legacy.rs
@@ -0,0 +1,1616 @@
+#[macro_use]
+extern crate serde_json;
+
+#[allow(deprecated)]
+use mockito::{mock, server_address, Matcher};
+use rand::distributions::Alphanumeric;
+use rand::Rng;
+use std::fs;
+use std::io::{BufRead, BufReader, Read, Write};
+use std::mem;
+use std::net::TcpStream;
+use std::path::Path;
+use std::str::FromStr;
+use std::thread;
+
+type Binary = Vec;
+
+#[allow(deprecated)]
+fn request_stream>(
+ version: &str,
+ route: &str,
+ headers: &str,
+ body: StrOrBytes,
+) -> TcpStream {
+ let mut stream = TcpStream::connect(server_address()).unwrap();
+ let mut message: Binary = Vec::new();
+ for b in [route, " HTTP/", version, "\r\n", headers, "\r\n"]
+ .join("")
+ .as_bytes()
+ {
+ message.push(*b);
+ }
+ for b in body.as_ref().iter() {
+ message.push(*b);
+ }
+
+ stream.write_all(&message).unwrap();
+
+ stream
+}
+
+fn parse_stream(stream: TcpStream, skip_body: bool) -> (String, Vec, Binary) {
+ let mut reader = BufReader::new(stream);
+
+ let mut status_line = String::new();
+ reader.read_line(&mut status_line).unwrap();
+
+ let mut headers = vec![];
+ let mut content_length: Option = None;
+ let mut is_chunked = false;
+ loop {
+ let mut header_line = String::new();
+ reader.read_line(&mut header_line).unwrap();
+
+ if header_line == "\r\n" {
+ break;
+ }
+
+ if header_line.starts_with("transfer-encoding:") && header_line.contains("chunked") {
+ is_chunked = true;
+ }
+
+ if header_line.starts_with("content-length:") {
+ let mut parts = header_line.split(':');
+ content_length = Some(u64::from_str(parts.nth(1).unwrap().trim()).unwrap());
+ }
+
+ headers.push(header_line.trim_end().to_string());
+ }
+
+ let mut body: Binary = Vec::new();
+ if !skip_body {
+ if let Some(content_length) = content_length {
+ reader.take(content_length).read_to_end(&mut body).unwrap();
+ } else if is_chunked {
+ let mut chunk_size_buf = String::new();
+ loop {
+ chunk_size_buf.clear();
+ reader.read_line(&mut chunk_size_buf).unwrap();
+
+ let chunk_size = u64::from_str_radix(
+ chunk_size_buf.trim_matches(|c| c == '\r' || c == '\n'),
+ 16,
+ )
+ .expect("chunk size");
+ if chunk_size == 0 {
+ break;
+ }
+
+ (&mut reader)
+ .take(chunk_size)
+ .read_to_end(&mut body)
+ .unwrap();
+
+ chunk_size_buf.clear();
+ reader.read_line(&mut chunk_size_buf).unwrap();
+ }
+ }
+ }
+
+ (status_line, headers, body)
+}
+
+fn binary_request>(
+ route: &str,
+ headers: &str,
+ body: StrOrBytes,
+) -> (String, Vec, Binary) {
+ parse_stream(
+ request_stream("1.1", route, headers, body),
+ route.starts_with("HEAD"),
+ )
+}
+
+fn request(route: &str, headers: &str) -> (String, Vec, String) {
+ let (status, headers, body) = binary_request(route, headers, "");
+ let parsed_body: String = std::str::from_utf8(body.as_slice()).unwrap().to_string();
+ (status, headers, parsed_body)
+}
+
+fn request_with_body(route: &str, headers: &str, body: &str) -> (String, Vec, String) {
+ let headers = format!("{}content-length: {}\r\n", headers, body.len());
+ let (status, headers, body) = binary_request(route, &headers, body);
+ let parsed_body: String = std::str::from_utf8(body.as_slice()).unwrap().to_string();
+ (status, headers, parsed_body)
+}
+
+#[allow(deprecated)]
+#[test]
+#[allow(deprecated)]
+fn test_legacy_create_starts_the_server() {
+ let _m = mock("GET", "/").with_body("hello").create();
+
+ let stream = TcpStream::connect(server_address());
+ assert!(stream.is_ok());
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_simple_route_mock() {
+ let _m = mock("GET", "/hello").with_body("world").create();
+
+ let (status_line, _, body) = request("GET /hello", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+ assert_eq!("world", body);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_two_route_mocks() {
+ let _m1 = mock("GET", "/a").with_body("aaa").create();
+ let _m2 = mock("GET", "/b").with_body("bbb").create();
+
+ let (_, _, body_a) = request("GET /a", "");
+
+ assert_eq!("aaa", body_a);
+ let (_, _, body_b) = request("GET /b", "");
+ assert_eq!("bbb", body_b);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_no_match_returns_501() {
+ let _m = mock("GET", "/").with_body("matched").create();
+
+ let (status_line, headers, _) = request("GET /nope", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+ assert_eq!("content-length: 0", headers[0]);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_header() {
+ let _m1 = mock("GET", "/")
+ .match_header("content-type", "application/json")
+ .with_body("{}")
+ .create();
+
+ let _m2 = mock("GET", "/")
+ .match_header("content-type", "text/plain")
+ .with_body("hello")
+ .create();
+
+ let (_, _, body_json) = request("GET /", "content-type: application/json\r\n");
+ assert_eq!("{}", body_json);
+
+ let (_, _, body_text) = request("GET /", "content-type: text/plain\r\n");
+ assert_eq!("hello", body_text);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_header_is_case_insensitive_on_the_field_name() {
+ let _m = mock("GET", "/")
+ .match_header("content-type", "text/plain")
+ .create();
+
+ let (uppercase_status_line, _, _) = request("GET /", "Content-Type: text/plain\r\n");
+ assert_eq!("HTTP/1.1 200 OK\r\n", uppercase_status_line);
+
+ let (lowercase_status_line, _, _) = request("GET /", "content-type: text/plain\r\n");
+ assert_eq!("HTTP/1.1 200 OK\r\n", lowercase_status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_multiple_headers() {
+ let _m = mock("GET", "/")
+ .match_header("Content-Type", "text/plain")
+ .match_header("Authorization", "secret")
+ .with_body("matched")
+ .create();
+
+ let (_, _, body_matching) = request(
+ "GET /",
+ "content-type: text/plain\r\nauthorization: secret\r\n",
+ );
+ assert_eq!("matched", body_matching);
+
+ let (status_not_matching, _, _) = request(
+ "GET /",
+ "content-type: text/plain\r\nauthorization: meh\r\n",
+ );
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_not_matching);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_header_any_matching() {
+ let _m = mock("GET", "/")
+ .match_header("Content-Type", Matcher::Any)
+ .with_body("matched")
+ .create();
+
+ let (_, _, body) = request("GET /", "content-type: something\r\n");
+ assert_eq!("matched", body);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_header_any_not_matching() {
+ let _m = mock("GET", "/")
+ .match_header("Content-Type", Matcher::Any)
+ .with_body("matched")
+ .create();
+
+ let (status, _, _) = request("GET /", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_header_missing_matching() {
+ let _m = mock("GET", "/")
+ .match_header("Authorization", Matcher::Missing)
+ .create();
+
+ let (status, _, _) = request("GET /", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_header_missing_not_matching() {
+ let _m = mock("GET", "/")
+ .match_header("Authorization", Matcher::Missing)
+ .create();
+
+ let (status, _, _) = request("GET /", "Authorization: something\r\n");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_header_missing_not_matching_even_when_empty() {
+ let _m = mock("GET", "/")
+ .match_header("Authorization", Matcher::Missing)
+ .create();
+
+ let (status, _, _) = request("GET /", "Authorization:\r\n");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_multiple_header_conditions_matching() {
+ let _m = mock("GET", "/")
+ .match_header("Hello", "World")
+ .match_header("Content-Type", Matcher::Any)
+ .match_header("Authorization", Matcher::Missing)
+ .create();
+
+ let (status, _, _) = request("GET /", "Hello: World\r\nContent-Type: something\r\n");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_multiple_header_conditions_not_matching() {
+ let _m = mock("GET", "/")
+ .match_header("hello", "world")
+ .match_header("Content-Type", Matcher::Any)
+ .match_header("Authorization", Matcher::Missing)
+ .create();
+
+ let (status, _, _) = request("GET /", "Hello: World\r\n");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_any_body_by_default() {
+ let _m = mock("POST", "/").create();
+
+ let (status, _, _) = request_with_body("POST /", "", "hello");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body() {
+ let _m = mock("POST", "/").match_body("hello").create();
+
+ let (status, _, _) = request_with_body("POST /", "", "hello");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_not_matching() {
+ let _m = mock("POST", "/").match_body("hello").create();
+
+ let (status, _, _) = request_with_body("POST /", "", "bye");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_binary_body() {
+ let _m = mock("POST", "/")
+ .match_body(Path::new("./tests/files/test_payload.bin"))
+ .create();
+
+ let mut file_content: Binary = Vec::new();
+ fs::File::open("./tests/files/test_payload.bin")
+ .unwrap()
+ .read_to_end(&mut file_content)
+ .unwrap();
+ let content_length_header = format!("Content-Length: {}\r\n", file_content.len());
+ let (status, _, _) = binary_request("POST /", &content_length_header, file_content);
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_does_not_match_binary_body() {
+ let _m = mock("POST", "/")
+ .match_body(Path::new("./tests/files/test_payload.bin"))
+ .create();
+
+ let file_content: Binary = (0..1024).map(|_| rand::random::()).collect();
+ let content_length_header = format!("Content-Length: {}\r\n", file_content.len());
+ let (status, _, _) = binary_request("POST /", &content_length_header, file_content);
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_regex() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::Regex("hello".to_string()))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", "test hello test");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_regex_not_matching() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::Regex("hello".to_string()))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", "bye");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_json() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::Json(json!({"hello":"world", "foo": "bar"})))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_more_headers_with_json() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::Json(json!({"hello":"world", "foo": "bar"})))
+ .create();
+
+ let headers = (0..15)
+ .map(|n| {
+ format!(
+ "x-header-{}: foo-bar-value-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\r\n",
+ n
+ )
+ })
+ .collect::>()
+ .concat();
+
+ let (status, _, _) =
+ request_with_body("POST /", &headers, r#"{"hello":"world", "foo": "bar"}"#);
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_json_order() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::Json(json!({"foo": "bar", "hello": "world"})))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_json_string() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::JsonString(
+ "{\"hello\":\"world\", \"foo\": \"bar\"}".to_string(),
+ ))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_json_string_order() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::JsonString(
+ "{\"foo\": \"bar\", \"hello\": \"world\"}".to_string(),
+ ))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_partial_json() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::PartialJson(json!({"hello":"world"})))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_partial_json_and_extra_fields() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::PartialJson(json!({"hello":"world", "foo": "bar"})))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world"}"#);
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_partial_json_string() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::PartialJsonString(
+ "{\"hello\": \"world\"}".to_string(),
+ ))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_body_with_partial_json_string_and_extra_fields() {
+ let _m = mock("POST", "/")
+ .match_body(Matcher::PartialJsonString(
+ "{\"foo\": \"bar\", \"hello\": \"world\"}".to_string(),
+ ))
+ .create();
+
+ let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world"}"#);
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_mock_with_status() {
+ let _m = mock("GET", "/").with_status(204).with_body("").create();
+
+ let (status_line, _, _) = request("GET /", "");
+ assert_eq!("HTTP/1.1 204 No Content\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_mock_with_custom_status() {
+ let _m = mock("GET", "/").with_status(499).with_body("").create();
+
+ let (status_line, _, _) = request("GET /", "");
+ assert_eq!("HTTP/1.1 499 \r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_mock_with_body() {
+ let _m = mock("GET", "/").with_body("hello").create();
+
+ let (_, _, body) = request("GET /", "");
+ assert_eq!("hello", body);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_mock_with_fn_body() {
+ let _m = mock("GET", "/")
+ .with_body_from_fn(|w| {
+ w.write_all(b"hel")?;
+ w.write_all(b"lo")
+ })
+ .create();
+
+ let (_, _, body) = request("GET /", "");
+ assert_eq!("hello", body);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_mock_with_header() {
+ let _m = mock("GET", "/")
+ .with_header("content-type", "application/json")
+ .with_body("{}")
+ .create();
+
+ let (_, headers, _) = request("GET /", "");
+ assert!(headers.contains(&"content-type: application/json".to_string()));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_mock_with_multiple_headers() {
+ let _m = mock("GET", "/")
+ .with_header("content-type", "application/json")
+ .with_header("x-api-key", "1234")
+ .with_body("{}")
+ .create();
+
+ let (_, headers, _) = request("GET /", "");
+ assert!(headers.contains(&"content-type: application/json".to_string()));
+ assert!(headers.contains(&"x-api-key: 1234".to_string()));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_mock_preserves_header_order() {
+ let mut expected_headers = Vec::new();
+ let mut mock = mock("GET", "/");
+
+ // Add a large number of headers so getting the same order accidentally is unlikely.
+ for i in 0..100 {
+ let field = format!("x-custom-header-{}", i);
+ let value = "test";
+ mock = mock.with_header(&field, value);
+ expected_headers.push(format!("{}: {}", field, value));
+ }
+
+ let _m = mock.create();
+
+ let (_, headers, _) = request("GET /", "");
+ let custom_headers: Vec<_> = headers
+ .into_iter()
+ .filter(|header| header.starts_with("x-custom-header"))
+ .collect();
+
+ assert_eq!(custom_headers, expected_headers);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_going_out_of_context_removes_mock() {
+ {
+ let _m = mock("GET", "/reset").create();
+
+ let (working_status_line, _, _) = request("GET /reset", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", working_status_line);
+ }
+
+ let (reset_status_line, _, _) = request("GET /reset", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", reset_status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_going_out_of_context_doesnt_remove_other_mocks() {
+ let _m1 = mock("GET", "/long").create();
+
+ {
+ let _m2 = mock("GET", "/short").create();
+
+ let (short_status_line, _, _) = request("GET /short", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", short_status_line);
+ }
+
+ let (long_status_line, _, _) = request("GET /long", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", long_status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_explicitly_calling_drop_removes_the_mock() {
+ let mock = mock("GET", "/").create();
+
+ let (status_line, _, _) = request("GET /", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+ mem::drop(mock);
+
+ let (dropped_status_line, _, _) = request("GET /", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", dropped_status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_regex_match_path() {
+ let _m1 = mock("GET", Matcher::Regex(r"^/a/\d{1}$".to_string()))
+ .with_body("aaa")
+ .create();
+ let _m2 = mock("GET", Matcher::Regex(r"^/b/\d{1}$".to_string()))
+ .with_body("bbb")
+ .create();
+
+ let (_, _, body_a) = request("GET /a/1", "");
+ assert_eq!("aaa", body_a);
+
+ let (_, _, body_b) = request("GET /b/2", "");
+ assert_eq!("bbb", body_b);
+
+ let (status_line, _, _) = request("GET /a/11", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+
+ let (status_line, _, _) = request("GET /c/2", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_regex_match_header() {
+ let _m = mock("GET", "/")
+ .match_header(
+ "Authorization",
+ Matcher::Regex(r"^Bearer token\.\w+$".to_string()),
+ )
+ .with_body("{}")
+ .create();
+
+ let (_, _, body_json) = request("GET /", "Authorization: Bearer token.payload\r\n");
+ assert_eq!("{}", body_json);
+
+ let (status_line, _, _) = request("GET /", "authorization: Beare none\r\n");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_any_of_match_header() {
+ let _m = mock("GET", "/")
+ .match_header(
+ "Via",
+ Matcher::AnyOf(vec![
+ Matcher::Exact("one".into()),
+ Matcher::Exact("two".into()),
+ ]),
+ )
+ .with_body("{}")
+ .create();
+
+ let (_, _, body_json) = request("GET /", "Via: one\r\n");
+ assert_eq!("{}", body_json);
+
+ let (_, _, body_json) = request("GET /", "Via: two\r\n");
+ assert_eq!("{}", body_json);
+
+ let (_, _, body_json) = request("GET /", "Via: one\r\nVia: two\r\n");
+ assert_eq!("{}", body_json);
+
+ let (status_line, _, _) = request("GET /", "Via: one\r\nVia: two\r\nVia: wrong\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: wrong\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_any_of_match_body() {
+ let _m = mock("GET", "/")
+ .match_body(Matcher::AnyOf(vec![
+ Matcher::Regex("one".to_string()),
+ Matcher::Regex("two".to_string()),
+ ]))
+ .create();
+
+ let (status_line, _, _) = request_with_body("GET /", "", "one");
+ assert!(status_line.starts_with("HTTP/1.1 200 "));
+
+ let (status_line, _, _) = request_with_body("GET /", "", "two");
+ assert!(status_line.starts_with("HTTP/1.1 200 "));
+
+ let (status_line, _, _) = request_with_body("GET /", "", "one two");
+ assert!(status_line.starts_with("HTTP/1.1 200 "));
+
+ let (status_line, _, _) = request_with_body("GET /", "", "three");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_any_of_missing_match_header() {
+ let _m = mock("GET", "/")
+ .match_header(
+ "Via",
+ Matcher::AnyOf(vec![Matcher::Exact("one".into()), Matcher::Missing]),
+ )
+ .with_body("{}")
+ .create();
+
+ let (_, _, body_json) = request("GET /", "Via: one\r\n");
+ assert_eq!("{}", body_json);
+
+ let (_, _, body_json) = request("GET /", "Via: one\r\nVia: one\r\nVia: one\r\n");
+ assert_eq!("{}", body_json);
+
+ let (_, _, body_json) = request("GET /", "NotVia: one\r\n");
+ assert_eq!("{}", body_json);
+
+ let (status_line, _, _) = request("GET /", "Via: wrong\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: wrong\r\nVia: one\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: one\r\nVia: wrong\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_all_of_match_header() {
+ let _m = mock("GET", "/")
+ .match_header(
+ "Via",
+ Matcher::AllOf(vec![
+ Matcher::Regex("one".into()),
+ Matcher::Regex("two".into()),
+ ]),
+ )
+ .with_body("{}")
+ .create();
+
+ let (status_line, _, _) = request("GET /", "Via: one\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: two\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: one two\r\nVia: one two three\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 200 "));
+
+ let (status_line, _, _) = request("GET /", "Via: one\r\nVia: two\r\nVia: wrong\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: wrong\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_all_of_match_body() {
+ let _m = mock("GET", "/")
+ .match_body(Matcher::AllOf(vec![
+ Matcher::Regex("one".to_string()),
+ Matcher::Regex("two".to_string()),
+ ]))
+ .create();
+
+ let (status_line, _, _) = request_with_body("GET /", "", "one");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request_with_body("GET /", "", "two");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request_with_body("GET /", "", "one two");
+ assert!(status_line.starts_with("HTTP/1.1 200 "));
+
+ let (status_line, _, _) = request_with_body("GET /", "", "three");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_all_of_missing_match_header() {
+ let _m = mock("GET", "/")
+ .match_header("Via", Matcher::AllOf(vec![Matcher::Missing]))
+ .with_body("{}")
+ .create();
+
+ let (status_line, _, _) = request("GET /", "Via: one\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: one\r\nVia: one\r\nVia: one\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "NotVia: one\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 200 "));
+
+ let (status_line, _, _) = request("GET /", "Via: wrong\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: wrong\r\nVia: one\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+
+ let (status_line, _, _) = request("GET /", "Via: one\r\nVia: wrong\r\n");
+ assert!(status_line.starts_with("HTTP/1.1 501 "));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_large_utf8_body() {
+ let mock_body: String = rand::thread_rng()
+ .sample_iter(&Alphanumeric)
+ .map(char::from)
+ .take(3 * 1024) // Must be larger than the request read buffer
+ .map(char::from)
+ .collect();
+
+ let _m = mock("GET", "/").with_body(&mock_body).create();
+
+ let (_, _, body) = request("GET /", "");
+ assert_eq!(mock_body, body);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_body_from_file() {
+ let _m = mock("GET", "/")
+ .with_body_from_file("tests/files/simple.http")
+ .create();
+ let (status_line, _, body) = request("GET /", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+ assert_eq!("test body\n", body);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_exact_path() {
+ let mock = mock("GET", "/hello");
+
+ assert_eq!("\r\nGET /hello\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_regex_path() {
+ let mock = mock("GET", Matcher::Regex(r"^/hello/\d+$".to_string()));
+
+ assert_eq!("\r\nGET ^/hello/\\d+$ (regex)\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_any_path() {
+ let mock = mock("GET", Matcher::Any);
+
+ assert_eq!("\r\nGET (any)\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_exact_query() {
+ let mock = mock("GET", "/test?hello=world");
+
+ assert_eq!("\r\nGET /test?hello=world\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_regex_query() {
+ let mock = mock("GET", "/test").match_query(Matcher::Regex("hello=world".to_string()));
+
+ assert_eq!("\r\nGET /test?hello=world (regex)\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_any_query() {
+ let mock = mock("GET", "/test").match_query(Matcher::Any);
+
+ assert_eq!("\r\nGET /test?(any)\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_exact_header() {
+ let mock = mock("GET", "/")
+ .match_header("content-type", "text")
+ .create();
+
+ assert_eq!("\r\nGET /\r\ncontent-type: text\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_multiple_headers() {
+ let mock = mock("GET", "/")
+ .match_header("content-type", "text")
+ .match_header("content-length", Matcher::Regex(r"\d+".to_string()))
+ .match_header("authorization", Matcher::Any)
+ .match_header("x-request-id", Matcher::Missing)
+ .create();
+
+ assert_eq!("\r\nGET /\r\ncontent-type: text\r\ncontent-length: \\d+ (regex)\r\nauthorization: (any)\r\nx-request-id: (missing)\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_exact_body() {
+ let mock = mock("POST", "/").match_body("hello").create();
+
+ assert_eq!("\r\nPOST /\r\nhello\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_regex_body() {
+ let mock = mock("POST", "/")
+ .match_body(Matcher::Regex("hello".to_string()))
+ .create();
+
+ assert_eq!("\r\nPOST /\r\nhello\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_any_body() {
+ let mock = mock("POST", "/").match_body(Matcher::Any).create();
+
+ assert_eq!("\r\nPOST /\r\n", format!("{}", mock));
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_headers_and_body() {
+ let mock = mock("POST", "/")
+ .match_header("content-type", "text")
+ .match_body("hello")
+ .create();
+
+ assert_eq!(
+ "\r\nPOST /\r\ncontent-type: text\r\nhello\r\n",
+ format!("{}", mock)
+ );
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_all_of_queries() {
+ let mock = mock("POST", "/")
+ .match_query(Matcher::AllOf(vec![
+ Matcher::Exact("query1".to_string()),
+ Matcher::UrlEncoded("key".to_string(), "val".to_string()),
+ ]))
+ .create();
+
+ assert_eq!(
+ "\r\nPOST /?(query1, key=val (urlencoded)) (all of)\r\n",
+ format!("{}", mock)
+ );
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_display_mock_matching_any_of_headers() {
+ let mock = mock("POST", "/")
+ .match_header(
+ "content-type",
+ Matcher::AnyOf(vec![
+ Matcher::Exact("type1".to_string()),
+ Matcher::Regex("type2".to_string()),
+ ]),
+ )
+ .create();
+
+ assert_eq!(
+ "\r\nPOST /\r\ncontent-type: (type1, type2 (regex)) (any of)\r\n",
+ format!("{}", mock)
+ );
+}
+#[test]
+#[allow(deprecated)]
+fn test_legacy_assert_defaults_to_one_hit() {
+ let mock = mock("GET", "/hello").create();
+
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_expect() {
+ let mock = mock("GET", "/hello").expect(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_expect_at_least_and_at_most() {
+ let mock = mock("GET", "/hello")
+ .expect_at_least(3)
+ .expect_at_most(6)
+ .create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_expect_at_least() {
+ let mock = mock("GET", "/hello").expect_at_least(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_expect_at_least_more() {
+ let mock = mock("GET", "/hello").expect_at_least(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_expect_at_most_with_needed_requests() {
+ let mock = mock("GET", "/hello").expect_at_most(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_expect_at_most_with_few_requests() {
+ let mock = mock("GET", "/hello").expect_at_most(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected at least 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n"
+)]
+fn test_legacy_assert_panics_expect_at_least_with_too_few_requests() {
+ let mock = mock("GET", "/hello").expect_at_least(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected at most 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n"
+)]
+fn test_legacy_assert_panics_expect_at_most_with_too_many_requests() {
+ let mock = mock("GET", "/hello").expect_at_most(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected between 3 and 5 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n"
+)]
+fn test_legacy_assert_panics_expect_at_least_and_at_most_with_too_few_requests() {
+ let mock = mock("GET", "/hello")
+ .expect_at_least(3)
+ .expect_at_most(5)
+ .create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected between 3 and 5 request(s) to:\n\r\nGET /hello\r\n\n...but received 6\n"
+)]
+fn test_legacy_assert_panics_expect_at_least_and_at_most_with_too_many_requests() {
+ let mock = mock("GET", "/hello")
+ .expect_at_least(3)
+ .expect_at_most(5)
+ .create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n")]
+fn test_legacy_assert_panics_if_no_request_was_performed() {
+ let mock = mock("GET", "/hello").create();
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n")]
+fn test_legacy_assert_panics_with_too_few_requests() {
+ let mock = mock("GET", "/hello").expect(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n")]
+fn test_legacy_assert_panics_with_too_many_requests() {
+ let mock = mock("GET", "/hello").expect(3).create();
+
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+ request("GET /hello", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nGET /bye\r\n\n> Difference:\n\n\u{1b}[31mGET /hello\n\u{1b}[0m\u{1b}[32mGET\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[42;37m/bye\u{1b}[0m\u{1b}[32m\n\u{1b}[0m\n\n"
+)]
+#[cfg(feature = "color")]
+fn test_legacy_assert_with_last_unmatched_request() {
+ let mock = mock("GET", "/hello").create();
+
+ request("GET /bye", "");
+
+ mock.assert();
+}
+
+// Same test but without colors (for Appveyor)
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nGET /bye\r\n\n> Difference:\n\nGET /hello\nGET /bye\n\n\n"
+)]
+#[cfg(not(feature = "color"))]
+fn test_legacy_assert_with_last_unmatched_request() {
+ let mock = mock("GET", "/hello").create();
+
+ request("GET /bye", "");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nGET /bye\r\nauthorization: 1234\r\naccept: text\r\n\n> Difference:\n\n\u{1b}[31mGET /hello\n\u{1b}[0m\u{1b}[32mGET\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[42;37m/bye\u{1b}[0m\u{1b}[32m\n\u{1b}[0m\u{1b}[92mauthorization: 1234\n\u{1b}[0m\u{1b}[92maccept: text\n\u{1b}[0m\n\n"
+)]
+#[cfg(feature = "color")]
+fn test_legacy_assert_with_last_unmatched_request_and_headers() {
+ let mock = mock("GET", "/hello").create();
+
+ request("GET /bye", "authorization: 1234\r\naccept: text\r\n");
+
+ mock.assert();
+}
+
+// Same test but without colors (for Appveyor)
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nGET /bye\r\nauthorization: 1234\r\naccept: text\r\n\n> Difference:\n\nGET /hello\nGET /bye\nauthorization: 1234\naccept: text\n\n\n"
+)]
+#[cfg(not(feature = "color"))]
+fn test_legacy_assert_with_last_unmatched_request_and_headers() {
+ let mock = mock("GET", "/hello").create();
+
+ request("GET /bye", "authorization: 1234\r\naccept: text\r\n");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[should_panic(
+ expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nPOST /bye\r\ncontent-length: 5\r\nhello\r\n\n"
+)]
+fn test_legacy_assert_with_last_unmatched_request_and_body() {
+ let mock = mock("GET", "/hello").create();
+
+ request_with_body("POST /bye", "", "hello");
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_request_from_thread() {
+ let mock = mock("GET", "/").create();
+
+ let process = thread::spawn(move || {
+ request("GET /", "");
+ });
+
+ process.join().unwrap();
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+#[ignore]
+// Can't work unless there's a way to apply LOCAL_TEST_MUTEX only to test threads and
+// not to any of their sub-threads.
+fn test_legacy_mock_from_inside_thread_does_not_lock_forever() {
+ let _mock_outside_thread = mock("GET", "/").with_body("outside").create();
+
+ let process = thread::spawn(move || {
+ let _mock_inside_thread = mock("GET", "/").with_body("inside").create();
+ });
+
+ process.join().unwrap();
+
+ let (_, _, body) = request("GET /", "");
+
+ assert_eq!("outside", body);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_head_request_with_overridden_content_length() {
+ let _mock = mock("HEAD", "/")
+ .with_header("content-length", "100")
+ .create();
+
+ let (_, headers, _) = request("HEAD /", "");
+
+ assert_eq!(
+ vec!["connection: close", "content-length: 100"],
+ headers[0..=1]
+ );
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_propagate_protocol_to_response() {
+ let _mock = mock("GET", "/").create();
+
+ let stream = request_stream("1.0", "GET /", "", "");
+
+ let (status_line, _, _) = parse_stream(stream, true);
+ assert_eq!("HTTP/1.0 200 OK\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_large_body_without_content_length() {
+ let body = "123".repeat(2048);
+
+ let _mock = mock("POST", "/").match_body(body.as_str()).create();
+
+ let headers = format!("content-length: {}\r\n", body.len());
+ let stream = request_stream("1.0", "POST /", &headers, &body);
+
+ let (status_line, _, _) = parse_stream(stream, false);
+ assert_eq!("HTTP/1.0 200 OK\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_transfer_encoding_chunked() {
+ let _mock = mock("POST", "/")
+ .match_body("Hello, chunked world!")
+ .create();
+
+ let body = "3\r\nHel\r\n5\r\nlo, c\r\nD\r\nhunked world!\r\n0\r\n\r\n";
+
+ let (status, _, _) = parse_stream(
+ request_stream("1.1", "POST /", "Transfer-Encoding: chunked\r\n", body),
+ false,
+ );
+
+ assert_eq!("HTTP/1.1 200 OK\r\n", status);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_exact_query() {
+ let _m = mock("GET", "/hello")
+ .match_query(Matcher::Exact("number=one".to_string()))
+ .create();
+
+ let (status_line, _, _) = request("GET /hello?number=one", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+ let (status_line, _, _) = request("GET /hello?number=two", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_exact_query_via_path() {
+ let _m = mock("GET", "/hello?number=one").create();
+
+ let (status_line, _, _) = request("GET /hello?number=one", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+ let (status_line, _, _) = request("GET /hello?number=two", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_partial_query_by_regex() {
+ let _m = mock("GET", "/hello")
+ .match_query(Matcher::Regex("number=one".to_string()))
+ .create();
+
+ let (status_line, _, _) = request("GET /hello?something=else&number=one", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_partial_query_by_urlencoded() {
+ let _m = mock("GET", "/hello")
+ .match_query(Matcher::UrlEncoded("num ber".into(), "o ne".into()))
+ .create();
+
+ let (status_line, _, _) = request("GET /hello?something=else&num%20ber=o%20ne", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+ let (status_line, _, _) = request("GET /hello?something=else&number=one", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_partial_query_by_regex_all_of() {
+ let _m = mock("GET", "/hello")
+ .match_query(Matcher::AllOf(vec![
+ Matcher::Regex("number=one".to_string()),
+ Matcher::Regex("hello=world".to_string()),
+ ]))
+ .create();
+
+ let (status_line, _, _) = request("GET /hello?hello=world&something=else&number=one", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+ let (status_line, _, _) = request("GET /hello?hello=world&something=else", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_partial_query_by_urlencoded_all_of() {
+ let _m = mock("GET", "/hello")
+ .match_query(Matcher::AllOf(vec![
+ Matcher::UrlEncoded("num ber".into(), "o ne".into()),
+ Matcher::UrlEncoded("hello".into(), "world".into()),
+ ]))
+ .create();
+
+ let (status_line, _, _) = request("GET /hello?hello=world&something=else&num%20ber=o%20ne", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+ let (status_line, _, _) = request("GET /hello?hello=world&something=else", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_query_with_non_percent_url_escaping() {
+ let _m = mock("GET", "/hello")
+ .match_query(Matcher::AllOf(vec![
+ Matcher::UrlEncoded("num ber".into(), "o ne".into()),
+ Matcher::UrlEncoded("hello".into(), "world".into()),
+ ]))
+ .create();
+
+ let (status_line, _, _) = request("GET /hello?hello=world&something=else&num+ber=o+ne", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_match_missing_query() {
+ let _m = mock("GET", "/hello").match_query(Matcher::Missing).create();
+
+ let (status_line, _, _) = request("GET /hello?", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+ let (status_line, _, _) = request("GET /hello?number=one", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_anyof_exact_path_and_query_matcher() {
+ let mock = mock(
+ "GET",
+ Matcher::AnyOf(vec![Matcher::Exact("/hello?world".to_string())]),
+ )
+ .create();
+
+ let (status_line, _, _) = request("GET /hello?world", "");
+ assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+ mock.assert();
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_default_headers() {
+ let _m = mock("GET", "/").create();
+
+ let (_, headers, _) = request("GET /", "");
+ assert_eq!(
+ vec!["connection: close", "content-length: 0"],
+ headers[0..=1]
+ );
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_missing_create_bad() {
+ testing_logger::setup();
+
+ let m = mock("GET", "/");
+ drop(m);
+
+ // Expecting one warning
+ testing_logger::validate(|captured_logs| {
+ let warnings = captured_logs
+ .iter()
+ .filter(|c| c.level == log::Level::Warn)
+ .collect::>();
+
+ assert_eq!(warnings.len(), 1);
+ assert_eq!(
+ warnings[0].body,
+ "Missing .create() call on mock \r\nGET /\r\n"
+ );
+ assert_eq!(warnings[0].level, log::Level::Warn);
+ });
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_missing_create_good() {
+ testing_logger::setup();
+
+ let m = mock("GET", "/").create();
+ drop(m);
+
+ // No warnings should occur
+ testing_logger::validate(|captured_logs| {
+ assert_eq!(
+ captured_logs
+ .iter()
+ .filter(|c| c.level == log::Level::Warn)
+ .count(),
+ 0
+ );
+ });
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_same_endpoint_different_responses() {
+ let mock_200 = mock("GET", "/hello").with_status(200).create();
+ let mock_404 = mock("GET", "/hello").with_status(404).create();
+ let mock_500 = mock("GET", "/hello").with_status(500).create();
+
+ let response_200 = request("GET /hello", "");
+ let response_404 = request("GET /hello", "");
+ let response_500 = request("GET /hello", "");
+
+ mock_200.assert();
+ mock_404.assert();
+ mock_500.assert();
+
+ assert_eq!(response_200.0, "HTTP/1.1 200 OK\r\n");
+ assert_eq!(response_404.0, "HTTP/1.1 404 Not Found\r\n");
+ assert_eq!(response_500.0, "HTTP/1.1 500 Internal Server Error\r\n");
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_same_endpoint_different_responses_last_one_forever() {
+ let _mock_200 = mock("GET", "/hello").with_status(200).create();
+ let _mock_404 = mock("GET", "/hello").with_status(404).create();
+ let _mock_500 = mock("GET", "/hello")
+ .expect_at_least(1)
+ .with_status(500)
+ .create();
+
+ let response_200 = request("GET /hello", "");
+ let response_404 = request("GET /hello", "");
+ let response_500_1 = request("GET /hello", "");
+ let response_500_2 = request("GET /hello", "");
+ let response_500_3 = request("GET /hello", "");
+
+ assert_eq!(response_200.0, "HTTP/1.1 200 OK\r\n");
+ assert_eq!(response_404.0, "HTTP/1.1 404 Not Found\r\n");
+ assert_eq!(response_500_1.0, "HTTP/1.1 500 Internal Server Error\r\n");
+ assert_eq!(response_500_2.0, "HTTP/1.1 500 Internal Server Error\r\n");
+ assert_eq!(response_500_3.0, "HTTP/1.1 500 Internal Server Error\r\n");
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_matched_bool() {
+ let m = mock("GET", "/").create();
+
+ let (_, _, _) = request_with_body("GET /", "", "");
+ m.assert();
+ assert!(m.matched(), "matched method returns correctly");
+
+ let (_, _, _) = request_with_body("GET /", "", "");
+ assert!(!m.matched(), "matched method returns correctly");
+}
+
+#[test]
+#[allow(deprecated)]
+fn test_legacy_invalid_header_field_name() {
+ let _m = mock("GET", "/").create();
+
+ let (uppercase_status_line, _, body) = request("GET /", "Bad Header: something\r\n");
+ assert_eq!("HTTP/1.1 400 Bad Request\r\n", uppercase_status_line);
+ assert_eq!(body, "");
+}
diff --git a/tests/lib.rs b/tests/lib.rs
index 82b3012..99cb0f8 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1,26 +1,32 @@
#[macro_use]
extern crate serde_json;
-use mockito::{mock, server_address, Matcher};
+use hyper::{body::Buf, client::conn, Body, Version};
+use mockito::{Matcher, Server};
use rand::distributions::Alphanumeric;
use rand::Rng;
+use std::fmt::Display;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
use std::mem;
-use std::net::{Shutdown, TcpStream};
+use std::net::TcpStream;
use std::path::Path;
use std::str::FromStr;
+use std::sync::{Arc, Mutex};
use std::thread;
type Binary = Vec;
-fn request_stream>(
+fn request_stream>(
version: &str,
+ host: S,
route: &str,
headers: &str,
body: StrOrBytes,
) -> TcpStream {
- let mut stream = TcpStream::connect(server_address()).unwrap();
+ let host = host.to_string();
+ let mut stream =
+ TcpStream::connect(&host).unwrap_or_else(|_| panic!("couldn't connect to {}", &host));
let mut message: Binary = Vec::new();
for b in [route, " HTTP/", version, "\r\n", headers, "\r\n"]
.join("")
@@ -99,224 +105,318 @@ fn parse_stream(stream: TcpStream, skip_body: bool) -> (String, Vec, Bin
(status_line, headers, body)
}
-fn binary_request>(
+fn binary_request>(
+ host: S,
route: &str,
headers: &str,
body: StrOrBytes,
) -> (String, Vec, Binary) {
parse_stream(
- request_stream("1.1", route, headers, body),
+ request_stream("1.1", host, route, headers, body),
route.starts_with("HEAD"),
)
}
-fn request(route: &str, headers: &str) -> (String, Vec, String) {
- let (status, headers, body) = binary_request(route, headers, "");
+fn request(host: S, route: &str, headers: &str) -> (String, Vec, String) {
+ let (status, headers, body) = binary_request(host, route, headers, "");
let parsed_body: String = std::str::from_utf8(body.as_slice()).unwrap().to_string();
(status, headers, parsed_body)
}
-fn request_with_body(route: &str, headers: &str, body: &str) -> (String, Vec, String) {
+fn request_with_body(
+ host: S,
+ route: &str,
+ headers: &str,
+ body: &str,
+) -> (String, Vec, String) {
let headers = format!("{}content-length: {}\r\n", headers, body.len());
- let (status, headers, body) = binary_request(route, &headers, body);
+ let (status, headers, body) = binary_request(host, route, &headers, body);
let parsed_body: String = std::str::from_utf8(body.as_slice()).unwrap().to_string();
(status, headers, parsed_body)
}
+async fn hyper_request(
+ host: &str,
+ version: &str,
+ method: &str,
+ uri: &str,
+ body: Option,
+) -> (u16, Vec<(String, String)>, String) {
+ use tokio::net::TcpStream;
+
+ let version = match version {
+ "1.0" => Version::HTTP_10,
+ "1.1" => Version::HTTP_11,
+ "2.0" => Version::HTTP_2,
+ _ => panic!("unrecognized version"),
+ };
+
+ let target_stream = TcpStream::connect(host)
+ .await
+ .map_err(|_err| -> Result {
+ Err(format!("couldn't connect to {}", host))
+ })
+ .unwrap();
+ let (mut request_sender, connection) =
+ conn::Builder::new().handshake(target_stream).await.unwrap();
+ tokio::task::spawn(async move { connection.await });
+
+ let body = body.map_or_else(Body::empty, Body::from);
+ let req = hyper::Request::builder()
+ .method(method)
+ .uri(uri)
+ .version(version)
+ .body(body)
+ .unwrap();
+
+ let mut response = request_sender.send_request(req).await.unwrap();
+
+ let status = response.status().as_u16();
+
+ let raw_body = response.body_mut();
+ let mut buf = hyper::body::aggregate(raw_body).await.unwrap();
+ let body_bytes = buf.copy_to_bytes(buf.remaining()).to_vec();
+ let body = String::from_utf8_lossy(&body_bytes).to_string();
+
+ (status, vec![], body)
+}
+
#[test]
fn test_create_starts_the_server() {
- let _m = mock("GET", "/").with_body("hello").create();
+ let mut s = Server::new();
+ let _m1 = s.mock("GET", "/").with_body("hello").create();
- let stream = TcpStream::connect(server_address());
+ let stream = TcpStream::connect(s.host_with_port());
assert!(stream.is_ok());
}
#[test]
fn test_simple_route_mock() {
- let _m = mock("GET", "/hello").with_body("world").create();
+ let mut s = Server::new();
+ let _m1 = s.mock("GET", "/hello").with_body("world").create();
- let (status_line, _, body) = request("GET /hello", "");
+ let (status_line, _, body) = request(&s.host_with_port(), "GET /hello", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
assert_eq!("world", body);
}
#[test]
fn test_two_route_mocks() {
- let _m1 = mock("GET", "/a").with_body("aaa").create();
- let _m2 = mock("GET", "/b").with_body("bbb").create();
-
- let (_, _, body_a) = request("GET /a", "");
+ let mut s = Server::new();
+ let _m1 = s.mock("GET", "/a").with_body("aaa").create();
+ let _m2 = s.mock("GET", "/b").with_body("bbb").create();
+ let (_, _, body_a) = request(&s.host_with_port(), "GET /a", "");
assert_eq!("aaa", body_a);
- let (_, _, body_b) = request("GET /b", "");
+
+ let (_, _, body_b) = request(&s.host_with_port(), "GET /b", "");
assert_eq!("bbb", body_b);
}
#[test]
fn test_no_match_returns_501() {
- let _m = mock("GET", "/").with_body("matched").create();
+ let mut s = Server::new();
+ let _m1 = s.mock("GET", "/").with_body("matched").create();
- let (status_line, headers, _) = request("GET /nope", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
- assert_eq!(vec!["content-length: 0"], headers);
+ let (status_line, _headers, body) = request(&s.host_with_port(), "GET /nope", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
+ assert_eq!("", body);
}
#[test]
fn test_match_header() {
- let _m1 = mock("GET", "/")
+ let mut s = Server::new();
+ let _m1 = s
+ .mock("GET", "/")
.match_header("content-type", "application/json")
.with_body("{}")
.create();
- let _m2 = mock("GET", "/")
+ let _m2 = s
+ .mock("GET", "/")
.match_header("content-type", "text/plain")
.with_body("hello")
.create();
- let (_, _, body_json) = request("GET /", "content-type: application/json\r\n");
+ let (_, _, body_json) = request(
+ &s.host_with_port(),
+ "GET /",
+ "content-type: application/json\r\n",
+ );
assert_eq!("{}", body_json);
- let (_, _, body_text) = request("GET /", "content-type: text/plain\r\n");
+ let (_, _, body_text) = request(&s.host_with_port(), "GET /", "content-type: text/plain\r\n");
assert_eq!("hello", body_text);
}
#[test]
fn test_match_header_is_case_insensitive_on_the_field_name() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("content-type", "text/plain")
.create();
- let (uppercase_status_line, _, _) = request("GET /", "Content-Type: text/plain\r\n");
+ let (uppercase_status_line, _, _) =
+ request(&s.host_with_port(), "GET /", "Content-Type: text/plain\r\n");
assert_eq!("HTTP/1.1 200 OK\r\n", uppercase_status_line);
- let (lowercase_status_line, _, _) = request("GET /", "content-type: text/plain\r\n");
+ let (lowercase_status_line, _, _) =
+ request(&s.host_with_port(), "GET /", "content-type: text/plain\r\n");
assert_eq!("HTTP/1.1 200 OK\r\n", lowercase_status_line);
}
#[test]
fn test_match_multiple_headers() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("Content-Type", "text/plain")
.match_header("Authorization", "secret")
.with_body("matched")
.create();
let (_, _, body_matching) = request(
+ &s.host_with_port(),
"GET /",
"content-type: text/plain\r\nauthorization: secret\r\n",
);
assert_eq!("matched", body_matching);
let (status_not_matching, _, _) = request(
+ &s.host_with_port(),
"GET /",
"content-type: text/plain\r\nauthorization: meh\r\n",
);
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_not_matching);
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_not_matching);
}
#[test]
fn test_match_header_any_matching() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("Content-Type", Matcher::Any)
.with_body("matched")
.create();
- let (_, _, body) = request("GET /", "content-type: something\r\n");
+ let (_, _, body) = request(&s.host_with_port(), "GET /", "content-type: something\r\n");
assert_eq!("matched", body);
}
#[test]
fn test_match_header_any_not_matching() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("Content-Type", Matcher::Any)
.with_body("matched")
.create();
- let (status, _, _) = request("GET /", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) = request(&s.host_with_port(), "GET /", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_match_header_missing_matching() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("Authorization", Matcher::Missing)
.create();
- let (status, _, _) = request("GET /", "");
+ let (status, _, _) = request(&s.host_with_port(), "GET /", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_header_missing_not_matching() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("Authorization", Matcher::Missing)
.create();
- let (status, _, _) = request("GET /", "Authorization: something\r\n");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) = request(&s.host_with_port(), "GET /", "Authorization: something\r\n");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_match_header_missing_not_matching_even_when_empty() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("Authorization", Matcher::Missing)
.create();
- let (status, _, _) = request("GET /", "Authorization:\r\n");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) = request(&s.host_with_port(), "GET /", "Authorization:\r\n");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_match_multiple_header_conditions_matching() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("Hello", "World")
.match_header("Content-Type", Matcher::Any)
.match_header("Authorization", Matcher::Missing)
.create();
- let (status, _, _) = request("GET /", "Hello: World\r\nContent-Type: something\r\n");
+ let (status, _, _) = request(
+ &s.host_with_port(),
+ "GET /",
+ "Hello: World\r\nContent-Type: something\r\n",
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_multiple_header_conditions_not_matching() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("hello", "world")
.match_header("Content-Type", Matcher::Any)
.match_header("Authorization", Matcher::Missing)
.create();
- let (status, _, _) = request("GET /", "Hello: World\r\n");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) = request(&s.host_with_port(), "GET /", "Hello: World\r\n");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_match_any_body_by_default() {
- let _m = mock("POST", "/").create();
+ let mut s = Server::new();
+ let _m = s.mock("POST", "/").create();
- let (status, _, _) = request_with_body("POST /", "", "hello");
+ let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "hello");
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body() {
- let _m = mock("POST", "/").match_body("hello").create();
+ let mut s = Server::new();
+ let _m = s.mock("POST", "/").match_body("hello").create();
- let (status, _, _) = request_with_body("POST /", "", "hello");
+ let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "hello");
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_not_matching() {
- let _m = mock("POST", "/").match_body("hello").create();
+ let mut s = Server::new();
+ let _m = s.mock("POST", "/").match_body("hello").create();
- let (status, _, _) = request_with_body("POST /", "", "bye");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "bye");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_match_binary_body() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Path::new("./tests/files/test_payload.bin"))
.create();
@@ -326,55 +426,80 @@ fn test_match_binary_body() {
.read_to_end(&mut file_content)
.unwrap();
let content_length_header = format!("Content-Length: {}\r\n", file_content.len());
- let (status, _, _) = binary_request("POST /", &content_length_header, file_content);
+ let (status, _, _) = binary_request(
+ &s.host_with_port(),
+ "POST /",
+ &content_length_header,
+ file_content,
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_does_not_match_binary_body() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Path::new("./tests/files/test_payload.bin"))
.create();
let file_content: Binary = (0..1024).map(|_| rand::random::()).collect();
let content_length_header = format!("Content-Length: {}\r\n", file_content.len());
- let (status, _, _) = binary_request("POST /", &content_length_header, file_content);
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) = binary_request(
+ &s.host_with_port(),
+ "POST /",
+ &content_length_header,
+ file_content,
+ );
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_match_body_with_regex() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::Regex("hello".to_string()))
.create();
- let (status, _, _) = request_with_body("POST /", "", "test hello test");
+ let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "test hello test");
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_with_regex_not_matching() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::Regex("hello".to_string()))
.create();
- let (status, _, _) = request_with_body("POST /", "", "bye");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) = request_with_body(&s.host_with_port(), "POST /", "", "bye");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_match_body_with_json() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::Json(json!({"hello":"world", "foo": "bar"})))
.create();
- let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ let (status, _, _) = request_with_body(
+ &s.host_with_port(),
+ "POST /",
+ "",
+ r#"{"hello":"world", "foo": "bar"}"#,
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_with_more_headers_with_json() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::Json(json!({"hello":"world", "foo": "bar"})))
.create();
@@ -388,154 +513,209 @@ fn test_match_body_with_more_headers_with_json() {
.collect::>()
.concat();
- let (status, _, _) =
- request_with_body("POST /", &headers, r#"{"hello":"world", "foo": "bar"}"#);
+ let (status, _, _) = request_with_body(
+ &s.host_with_port(),
+ "POST /",
+ &headers,
+ r#"{"hello":"world", "foo": "bar"}"#,
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_with_json_order() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::Json(json!({"foo": "bar", "hello": "world"})))
.create();
- let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ let (status, _, _) = request_with_body(
+ &s.host_with_port(),
+ "POST /",
+ "",
+ r#"{"hello":"world", "foo": "bar"}"#,
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_with_json_string() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::JsonString(
"{\"hello\":\"world\", \"foo\": \"bar\"}".to_string(),
))
.create();
- let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ let (status, _, _) = request_with_body(
+ &s.host_with_port(),
+ "POST /",
+ "",
+ r#"{"hello":"world", "foo": "bar"}"#,
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_with_json_string_order() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::JsonString(
"{\"foo\": \"bar\", \"hello\": \"world\"}".to_string(),
))
.create();
- let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ let (status, _, _) = request_with_body(
+ &s.host_with_port(),
+ "POST /",
+ "",
+ r#"{"hello":"world", "foo": "bar"}"#,
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_with_partial_json() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::PartialJson(json!({"hello":"world"})))
.create();
- let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ let (status, _, _) = request_with_body(
+ &s.host_with_port(),
+ "POST /",
+ "",
+ r#"{"hello":"world", "foo": "bar"}"#,
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_with_partial_json_and_extra_fields() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::PartialJson(json!({"hello":"world", "foo": "bar"})))
.create();
- let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world"}"#);
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) =
+ request_with_body(&s.host_with_port(), "POST /", "", r#"{"hello":"world"}"#);
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_match_body_with_partial_json_string() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::PartialJsonString(
"{\"hello\": \"world\"}".to_string(),
))
.create();
- let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world", "foo": "bar"}"#);
+ let (status, _, _) = request_with_body(
+ &s.host_with_port(),
+ "POST /",
+ "",
+ r#"{"hello":"world", "foo": "bar"}"#,
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status);
}
#[test]
fn test_match_body_with_partial_json_string_and_extra_fields() {
- let _m = mock("POST", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("POST", "/")
.match_body(Matcher::PartialJsonString(
"{\"foo\": \"bar\", \"hello\": \"world\"}".to_string(),
))
.create();
- let (status, _, _) = request_with_body("POST /", "", r#"{"hello":"world"}"#);
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status);
+ let (status, _, _) =
+ request_with_body(&s.host_with_port(), "POST /", "", r#"{"hello":"world"}"#);
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status);
}
#[test]
fn test_mock_with_status() {
- let _m = mock("GET", "/").with_status(204).with_body("").create();
+ let mut s = Server::new();
+ let _m = s.mock("GET", "/").with_status(204).with_body("").create();
- let (status_line, _, _) = request("GET /", "");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "");
assert_eq!("HTTP/1.1 204 No Content\r\n", status_line);
}
#[test]
fn test_mock_with_custom_status() {
- let _m = mock("GET", "/").with_status(333).with_body("").create();
+ let mut s = Server::new();
+ let _m = s.mock("GET", "/").with_status(499).with_body("").create();
- let (status_line, _, _) = request("GET /", "");
- assert_eq!("HTTP/1.1 333 Custom\r\n", status_line);
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "");
+ assert_eq!("HTTP/1.1 499 \r\n", status_line);
}
#[test]
fn test_mock_with_body() {
- let _m = mock("GET", "/").with_body("hello").create();
+ let mut s = Server::new();
+ let _m = s.mock("GET", "/").with_body("hello").create();
- let (_, _, body) = request("GET /", "");
+ let (_, _, body) = request(&s.host_with_port(), "GET /", "");
assert_eq!("hello", body);
}
#[test]
fn test_mock_with_fn_body() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.with_body_from_fn(|w| {
w.write_all(b"hel")?;
w.write_all(b"lo")
})
.create();
- let (_, _, body) = request("GET /", "");
+ let (_, _, body) = request(&s.host_with_port(), "GET /", "");
assert_eq!("hello", body);
}
#[test]
fn test_mock_with_header() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.with_header("content-type", "application/json")
.with_body("{}")
.create();
- let (_, headers, _) = request("GET /", "");
+ let (_, headers, _) = request(&s.host_with_port(), "GET /", "");
assert!(headers.contains(&"content-type: application/json".to_string()));
}
#[test]
fn test_mock_with_multiple_headers() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.with_header("content-type", "application/json")
.with_header("x-api-key", "1234")
.with_body("{}")
.create();
- let (_, headers, _) = request("GET /", "");
+ let (_, headers, _) = request(&s.host_with_port(), "GET /", "");
assert!(headers.contains(&"content-type: application/json".to_string()));
assert!(headers.contains(&"x-api-key: 1234".to_string()));
}
#[test]
fn test_mock_preserves_header_order() {
+ let mut s = Server::new();
let mut expected_headers = Vec::new();
- let mut mock = mock("GET", "/");
+ let mut mock = s.mock("GET", "/");
// Add a large number of headers so getting the same order accidentally is unlikely.
for i in 0..100 {
@@ -547,7 +727,7 @@ fn test_mock_preserves_header_order() {
let _m = mock.create();
- let (_, headers, _) = request("GET /", "");
+ let (_, headers, _) = request(&s.host_with_port(), "GET /", "");
let custom_headers: Vec<_> = headers
.into_iter()
.filter(|header| header.starts_with("x-custom-header"))
@@ -558,70 +738,79 @@ fn test_mock_preserves_header_order() {
#[test]
fn test_going_out_of_context_removes_mock() {
+ let mut s = Server::new();
{
- let _m = mock("GET", "/reset").create();
+ let _m = s.mock("GET", "/reset").create();
- let (working_status_line, _, _) = request("GET /reset", "");
+ let (working_status_line, _, _) = request(&s.host_with_port(), "GET /reset", "");
assert_eq!("HTTP/1.1 200 OK\r\n", working_status_line);
}
- let (reset_status_line, _, _) = request("GET /reset", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", reset_status_line);
+ let (reset_status_line, _, _) = request(&s.host_with_port(), "GET /reset", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", reset_status_line);
}
#[test]
fn test_going_out_of_context_doesnt_remove_other_mocks() {
- let _m1 = mock("GET", "/long").create();
+ let mut s = Server::new();
+ let _m1 = s.mock("GET", "/long").create();
{
- let _m2 = mock("GET", "/short").create();
+ let _m2 = s.mock("GET", "/short").create();
- let (short_status_line, _, _) = request("GET /short", "");
+ let (short_status_line, _, _) = request(&s.host_with_port(), "GET /short", "");
assert_eq!("HTTP/1.1 200 OK\r\n", short_status_line);
}
- let (long_status_line, _, _) = request("GET /long", "");
+ let (long_status_line, _, _) = request(&s.host_with_port(), "GET /long", "");
assert_eq!("HTTP/1.1 200 OK\r\n", long_status_line);
}
#[test]
fn test_explicitly_calling_drop_removes_the_mock() {
- let mock = mock("GET", "/").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/").create();
- let (status_line, _, _) = request("GET /", "");
+ let (status_line, _, _) = request(&host, "GET /", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
mem::drop(mock);
- let (dropped_status_line, _, _) = request("GET /", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", dropped_status_line);
+ let (dropped_status_line, _, _) = request(&s.host_with_port(), "GET /", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", dropped_status_line);
}
#[test]
fn test_regex_match_path() {
- let _m1 = mock("GET", Matcher::Regex(r"^/a/\d{1}$".to_string()))
+ let mut s = Server::new();
+ let _m1 = s
+ .mock("GET", Matcher::Regex(r"^/a/\d{1}$".to_string()))
.with_body("aaa")
.create();
- let _m2 = mock("GET", Matcher::Regex(r"^/b/\d{1}$".to_string()))
+ let _m2 = s
+ .mock("GET", Matcher::Regex(r"^/b/\d{1}$".to_string()))
.with_body("bbb")
.create();
- let (_, _, body_a) = request("GET /a/1", "");
+ let (_, _, body_a) = request(&s.host_with_port(), "GET /a/1", "");
assert_eq!("aaa", body_a);
- let (_, _, body_b) = request("GET /b/2", "");
+ let (_, _, body_b) = request(&s.host_with_port(), "GET /b/2", "");
assert_eq!("bbb", body_b);
- let (status_line, _, _) = request("GET /a/11", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /a/11", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
- let (status_line, _, _) = request("GET /c/2", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /c/2", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
}
#[test]
fn test_regex_match_header() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header(
"Authorization",
Matcher::Regex(r"^Bearer token\.\w+$".to_string()),
@@ -629,16 +818,26 @@ fn test_regex_match_header() {
.with_body("{}")
.create();
- let (_, _, body_json) = request("GET /", "Authorization: Bearer token.payload\r\n");
+ let (_, _, body_json) = request(
+ &s.host_with_port(),
+ "GET /",
+ "Authorization: Bearer token.payload\r\n",
+ );
assert_eq!("{}", body_json);
- let (status_line, _, _) = request("GET /", "authorization: Beare none\r\n");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(
+ &s.host_with_port(),
+ "GET /",
+ "authorization: Beare none\r\n",
+ );
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
}
#[test]
fn test_any_of_match_header() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header(
"Via",
Matcher::AnyOf(vec![
@@ -649,47 +848,55 @@ fn test_any_of_match_header() {
.with_body("{}")
.create();
- let (_, _, body_json) = request("GET /", "Via: one\r\n");
+ let (_, _, body_json) = request(&s.host_with_port(), "GET /", "Via: one\r\n");
assert_eq!("{}", body_json);
- let (_, _, body_json) = request("GET /", "Via: two\r\n");
+ let (_, _, body_json) = request(&s.host_with_port(), "GET /", "Via: two\r\n");
assert_eq!("{}", body_json);
- let (_, _, body_json) = request("GET /", "Via: one\r\nVia: two\r\n");
+ let (_, _, body_json) = request(&s.host_with_port(), "GET /", "Via: one\r\nVia: two\r\n");
assert_eq!("{}", body_json);
- let (status_line, _, _) = request("GET /", "Via: one\r\nVia: two\r\nVia: wrong\r\n");
+ let (status_line, _, _) = request(
+ &s.host_with_port(),
+ "GET /",
+ "Via: one\r\nVia: two\r\nVia: wrong\r\n",
+ );
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: wrong\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
}
#[test]
fn test_any_of_match_body() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_body(Matcher::AnyOf(vec![
Matcher::Regex("one".to_string()),
Matcher::Regex("two".to_string()),
]))
.create();
- let (status_line, _, _) = request_with_body("GET /", "", "one");
+ let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "one");
assert!(status_line.starts_with("HTTP/1.1 200 "));
- let (status_line, _, _) = request_with_body("GET /", "", "two");
+ let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "two");
assert!(status_line.starts_with("HTTP/1.1 200 "));
- let (status_line, _, _) = request_with_body("GET /", "", "one two");
+ let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "one two");
assert!(status_line.starts_with("HTTP/1.1 200 "));
- let (status_line, _, _) = request_with_body("GET /", "", "three");
+ let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "three");
assert!(status_line.starts_with("HTTP/1.1 501 "));
}
#[test]
fn test_any_of_missing_match_header() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header(
"Via",
Matcher::AnyOf(vec![Matcher::Exact("one".into()), Matcher::Missing]),
@@ -697,28 +904,34 @@ fn test_any_of_missing_match_header() {
.with_body("{}")
.create();
- let (_, _, body_json) = request("GET /", "Via: one\r\n");
+ let (_, _, body_json) = request(&s.host_with_port(), "GET /", "Via: one\r\n");
assert_eq!("{}", body_json);
- let (_, _, body_json) = request("GET /", "Via: one\r\nVia: one\r\nVia: one\r\n");
+ let (_, _, body_json) = request(
+ &s.host_with_port(),
+ "GET /",
+ "Via: one\r\nVia: one\r\nVia: one\r\n",
+ );
assert_eq!("{}", body_json);
- let (_, _, body_json) = request("GET /", "NotVia: one\r\n");
+ let (_, _, body_json) = request(&s.host_with_port(), "GET /", "NotVia: one\r\n");
assert_eq!("{}", body_json);
- let (status_line, _, _) = request("GET /", "Via: wrong\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: wrong\r\nVia: one\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\nVia: one\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: one\r\nVia: wrong\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: one\r\nVia: wrong\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
}
#[test]
fn test_all_of_match_header() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header(
"Via",
Matcher::AllOf(vec![
@@ -729,72 +942,89 @@ fn test_all_of_match_header() {
.with_body("{}")
.create();
- let (status_line, _, _) = request("GET /", "Via: one\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: one\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: two\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: two\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: one two\r\nVia: one two three\r\n");
+ let (status_line, _, _) = request(
+ &s.host_with_port(),
+ "GET /",
+ "Via: one two\r\nVia: one two three\r\n",
+ );
assert!(status_line.starts_with("HTTP/1.1 200 "));
- let (status_line, _, _) = request("GET /", "Via: one\r\nVia: two\r\nVia: wrong\r\n");
+ let (status_line, _, _) = request(
+ &s.host_with_port(),
+ "GET /",
+ "Via: one\r\nVia: two\r\nVia: wrong\r\n",
+ );
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: wrong\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
}
#[test]
fn test_all_of_match_body() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_body(Matcher::AllOf(vec![
Matcher::Regex("one".to_string()),
Matcher::Regex("two".to_string()),
]))
.create();
- let (status_line, _, _) = request_with_body("GET /", "", "one");
+ let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "one");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request_with_body("GET /", "", "two");
+ let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "two");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request_with_body("GET /", "", "one two");
+ let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "one two");
assert!(status_line.starts_with("HTTP/1.1 200 "));
- let (status_line, _, _) = request_with_body("GET /", "", "three");
+ let (status_line, _, _) = request_with_body(&s.host_with_port(), "GET /", "", "three");
assert!(status_line.starts_with("HTTP/1.1 501 "));
}
#[test]
fn test_all_of_missing_match_header() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.match_header("Via", Matcher::AllOf(vec![Matcher::Missing]))
.with_body("{}")
.create();
- let (status_line, _, _) = request("GET /", "Via: one\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: one\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: one\r\nVia: one\r\nVia: one\r\n");
+ let (status_line, _, _) = request(
+ &s.host_with_port(),
+ "GET /",
+ "Via: one\r\nVia: one\r\nVia: one\r\n",
+ );
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "NotVia: one\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "NotVia: one\r\n");
assert!(status_line.starts_with("HTTP/1.1 200 "));
- let (status_line, _, _) = request("GET /", "Via: wrong\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: wrong\r\nVia: one\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: wrong\r\nVia: one\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
- let (status_line, _, _) = request("GET /", "Via: one\r\nVia: wrong\r\n");
+ let (status_line, _, _) = request(&s.host_with_port(), "GET /", "Via: one\r\nVia: wrong\r\n");
assert!(status_line.starts_with("HTTP/1.1 501 "));
}
#[test]
fn test_large_utf8_body() {
+ let mut s = Server::new();
let mock_body: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.map(char::from)
@@ -802,67 +1032,79 @@ fn test_large_utf8_body() {
.map(char::from)
.collect();
- let _m = mock("GET", "/").with_body(&mock_body).create();
+ let _m = s.mock("GET", "/").with_body(&mock_body).create();
- let (_, _, body) = request("GET /", "");
+ let (_, _, body) = request(&s.host_with_port(), "GET /", "");
assert_eq!(mock_body, body);
}
#[test]
fn test_body_from_file() {
- let _m = mock("GET", "/")
+ let mut s = Server::new();
+ let _m = s
+ .mock("GET", "/")
.with_body_from_file("tests/files/simple.http")
.create();
- let (status_line, _, body) = request("GET /", "");
+ let (status_line, _, body) = request(&s.host_with_port(), "GET /", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
assert_eq!("test body\n", body);
}
#[test]
fn test_display_mock_matching_exact_path() {
- let mock = mock("GET", "/hello");
+ let mut s = Server::new();
+ let mock = s.mock("GET", "/hello");
assert_eq!("\r\nGET /hello\r\n", format!("{}", mock));
}
#[test]
fn test_display_mock_matching_regex_path() {
- let mock = mock("GET", Matcher::Regex(r"^/hello/\d+$".to_string()));
+ let mut s = Server::new();
+ let mock = s.mock("GET", Matcher::Regex(r"^/hello/\d+$".to_string()));
assert_eq!("\r\nGET ^/hello/\\d+$ (regex)\r\n", format!("{}", mock));
}
#[test]
fn test_display_mock_matching_any_path() {
- let mock = mock("GET", Matcher::Any);
+ let mut s = Server::new();
+ let mock = s.mock("GET", Matcher::Any);
assert_eq!("\r\nGET (any)\r\n", format!("{}", mock));
}
#[test]
fn test_display_mock_matching_exact_query() {
- let mock = mock("GET", "/test?hello=world");
+ let mut s = Server::new();
+ let mock = s.mock("GET", "/test?hello=world");
assert_eq!("\r\nGET /test?hello=world\r\n", format!("{}", mock));
}
#[test]
fn test_display_mock_matching_regex_query() {
- let mock = mock("GET", "/test").match_query(Matcher::Regex("hello=world".to_string()));
+ let mut s = Server::new();
+ let mock = s
+ .mock("GET", "/test")
+ .match_query(Matcher::Regex("hello=world".to_string()));
assert_eq!("\r\nGET /test?hello=world (regex)\r\n", format!("{}", mock));
}
#[test]
fn test_display_mock_matching_any_query() {
- let mock = mock("GET", "/test").match_query(Matcher::Any);
+ let mut s = Server::new();
+ let mock = s.mock("GET", "/test").match_query(Matcher::Any);
assert_eq!("\r\nGET /test?(any)\r\n", format!("{}", mock));
}
#[test]
fn test_display_mock_matching_exact_header() {
- let mock = mock("GET", "/")
+ let mut s = Server::new();
+ let mock = s
+ .mock("GET", "/")
.match_header("content-type", "text")
.create();
@@ -871,7 +1113,9 @@ fn test_display_mock_matching_exact_header() {
#[test]
fn test_display_mock_matching_multiple_headers() {
- let mock = mock("GET", "/")
+ let mut s = Server::new();
+ let mock = s
+ .mock("GET", "/")
.match_header("content-type", "text")
.match_header("content-length", Matcher::Regex(r"\d+".to_string()))
.match_header("authorization", Matcher::Any)
@@ -883,14 +1127,17 @@ fn test_display_mock_matching_multiple_headers() {
#[test]
fn test_display_mock_matching_exact_body() {
- let mock = mock("POST", "/").match_body("hello").create();
+ let mut s = Server::new();
+ let mock = s.mock("POST", "/").match_body("hello").create();
assert_eq!("\r\nPOST /\r\nhello\r\n", format!("{}", mock));
}
#[test]
fn test_display_mock_matching_regex_body() {
- let mock = mock("POST", "/")
+ let mut s = Server::new();
+ let mock = s
+ .mock("POST", "/")
.match_body(Matcher::Regex("hello".to_string()))
.create();
@@ -899,14 +1146,17 @@ fn test_display_mock_matching_regex_body() {
#[test]
fn test_display_mock_matching_any_body() {
- let mock = mock("POST", "/").match_body(Matcher::Any).create();
+ let mut s = Server::new();
+ let mock = s.mock("POST", "/").match_body(Matcher::Any).create();
assert_eq!("\r\nPOST /\r\n", format!("{}", mock));
}
#[test]
fn test_display_mock_matching_headers_and_body() {
- let mock = mock("POST", "/")
+ let mut s = Server::new();
+ let mock = s
+ .mock("POST", "/")
.match_header("content-type", "text")
.match_body("hello")
.create();
@@ -919,7 +1169,9 @@ fn test_display_mock_matching_headers_and_body() {
#[test]
fn test_display_mock_matching_all_of_queries() {
- let mock = mock("POST", "/")
+ let mut s = Server::new();
+ let mock = s
+ .mock("POST", "/")
.match_query(Matcher::AllOf(vec![
Matcher::Exact("query1".to_string()),
Matcher::UrlEncoded("key".to_string(), "val".to_string()),
@@ -934,7 +1186,9 @@ fn test_display_mock_matching_all_of_queries() {
#[test]
fn test_display_mock_matching_any_of_headers() {
- let mock = mock("POST", "/")
+ let mut s = Server::new();
+ let mock = s
+ .mock("POST", "/")
.match_header(
"content-type",
Matcher::AnyOf(vec![
@@ -949,82 +1203,98 @@ fn test_display_mock_matching_any_of_headers() {
format!("{}", mock)
);
}
+
#[test]
fn test_assert_defaults_to_one_hit() {
- let mock = mock("GET", "/hello").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").create();
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
#[test]
fn test_expect() {
- let mock = mock("GET", "/hello").expect(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
#[test]
fn test_expect_at_least_and_at_most() {
- let mock = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s
+ .mock("GET", "/hello")
.expect_at_least(3)
.expect_at_most(6)
.create();
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
#[test]
fn test_expect_at_least() {
- let mock = mock("GET", "/hello").expect_at_least(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect_at_least(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
#[test]
fn test_expect_at_least_more() {
- let mock = mock("GET", "/hello").expect_at_least(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect_at_least(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
#[test]
fn test_expect_at_most_with_needed_requests() {
- let mock = mock("GET", "/hello").expect_at_most(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect_at_most(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
#[test]
fn test_expect_at_most_with_few_requests() {
- let mock = mock("GET", "/hello").expect_at_most(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect_at_most(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
@@ -1034,10 +1304,12 @@ fn test_expect_at_most_with_few_requests() {
expected = "\n> Expected at least 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n"
)]
fn test_assert_panics_expect_at_least_with_too_few_requests() {
- let mock = mock("GET", "/hello").expect_at_least(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect_at_least(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
@@ -1047,12 +1319,14 @@ fn test_assert_panics_expect_at_least_with_too_few_requests() {
expected = "\n> Expected at most 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n"
)]
fn test_assert_panics_expect_at_most_with_too_many_requests() {
- let mock = mock("GET", "/hello").expect_at_most(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect_at_most(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
@@ -1062,13 +1336,16 @@ fn test_assert_panics_expect_at_most_with_too_many_requests() {
expected = "\n> Expected between 3 and 5 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n"
)]
fn test_assert_panics_expect_at_least_and_at_most_with_too_few_requests() {
- let mock = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s
+ .mock("GET", "/hello")
.expect_at_least(3)
.expect_at_most(5)
.create();
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
@@ -1078,17 +1355,20 @@ fn test_assert_panics_expect_at_least_and_at_most_with_too_few_requests() {
expected = "\n> Expected between 3 and 5 request(s) to:\n\r\nGET /hello\r\n\n...but received 6\n"
)]
fn test_assert_panics_expect_at_least_and_at_most_with_too_many_requests() {
- let mock = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s
+ .mock("GET", "/hello")
.expect_at_least(3)
.expect_at_most(5)
.create();
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
@@ -1096,7 +1376,8 @@ fn test_assert_panics_expect_at_least_and_at_most_with_too_many_requests() {
#[test]
#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n")]
fn test_assert_panics_if_no_request_was_performed() {
- let mock = mock("GET", "/hello").create();
+ let mut s = Server::new();
+ let mock = s.mock("GET", "/hello").create();
mock.assert();
}
@@ -1104,10 +1385,12 @@ fn test_assert_panics_if_no_request_was_performed() {
#[test]
#[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 2\n")]
fn test_assert_panics_with_too_few_requests() {
- let mock = mock("GET", "/hello").expect(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
@@ -1115,12 +1398,14 @@ fn test_assert_panics_with_too_few_requests() {
#[test]
#[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello\r\n\n...but received 4\n")]
fn test_assert_panics_with_too_many_requests() {
- let mock = mock("GET", "/hello").expect(3).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").expect(3).create();
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
- request("GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
+ request(&host, "GET /hello", "");
mock.assert();
}
@@ -1131,9 +1416,11 @@ fn test_assert_panics_with_too_many_requests() {
)]
#[cfg(feature = "color")]
fn test_assert_with_last_unmatched_request() {
- let mock = mock("GET", "/hello").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").create();
- request("GET /bye", "");
+ request(&host, "GET /bye", "");
mock.assert();
}
@@ -1145,9 +1432,11 @@ fn test_assert_with_last_unmatched_request() {
)]
#[cfg(not(feature = "color"))]
fn test_assert_with_last_unmatched_request() {
- let mock = mock("GET", "/hello").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").create();
- request("GET /bye", "");
+ request(&host, "GET /bye", "");
mock.assert();
}
@@ -1158,9 +1447,11 @@ fn test_assert_with_last_unmatched_request() {
)]
#[cfg(feature = "color")]
fn test_assert_with_last_unmatched_request_and_headers() {
- let mock = mock("GET", "/hello").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").create();
- request("GET /bye", "authorization: 1234\r\naccept: text\r\n");
+ request(&host, "GET /bye", "authorization: 1234\r\naccept: text\r\n");
mock.assert();
}
@@ -1172,9 +1463,11 @@ fn test_assert_with_last_unmatched_request_and_headers() {
)]
#[cfg(not(feature = "color"))]
fn test_assert_with_last_unmatched_request_and_headers() {
- let mock = mock("GET", "/hello").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").create();
- request("GET /bye", "authorization: 1234\r\naccept: text\r\n");
+ request(&host, "GET /bye", "authorization: 1234\r\naccept: text\r\n");
mock.assert();
}
@@ -1184,19 +1477,23 @@ fn test_assert_with_last_unmatched_request_and_headers() {
expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello\r\n\n...but received 0\n\n> The last unmatched request was:\n\r\nPOST /bye\r\ncontent-length: 5\r\nhello\r\n\n"
)]
fn test_assert_with_last_unmatched_request_and_body() {
- let mock = mock("GET", "/hello").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/hello").create();
- request_with_body("POST /bye", "", "hello");
+ request_with_body(&host, "POST /bye", "", "hello");
mock.assert();
}
#[test]
fn test_request_from_thread() {
- let mock = mock("GET", "/").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s.mock("GET", "/").create();
let process = thread::spawn(move || {
- request("GET /", "");
+ request(&host, "GET /", "");
});
process.join().unwrap();
@@ -1206,39 +1503,48 @@ fn test_request_from_thread() {
#[test]
#[ignore]
-// Can't work unless there's a way to apply LOCAL_TEST_MUTEX only to test threads and
-// not to any of their sub-threads.
fn test_mock_from_inside_thread_does_not_lock_forever() {
- let _mock_outside_thread = mock("GET", "/").with_body("outside").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _mock_outside_thread = s.mock("GET", "/").with_body("outside").create();
+ let server_mutex = Arc::new(Mutex::new(s));
+ let server_clone = server_mutex;
let process = thread::spawn(move || {
- let _mock_inside_thread = mock("GET", "/").with_body("inside").create();
+ let mut s = server_clone.lock().unwrap();
+ let _mock_inside_thread = s.mock("GET", "/").with_body("inside").create();
});
process.join().unwrap();
- let (_, _, body) = request("GET /", "");
-
+ let (_, _, body) = request(&host, "GET /", "");
assert_eq!("outside", body);
}
#[test]
fn test_head_request_with_overridden_content_length() {
- let _mock = mock("HEAD", "/")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _mock = s
+ .mock("HEAD", "/")
.with_header("content-length", "100")
.create();
- let (_, headers, _) = request("HEAD /", "");
+ let (_, headers, _) = request(&host, "HEAD /", "");
- assert_eq!(vec!["connection: close", "content-length: 100"], headers);
+ assert_eq!(
+ vec!["connection: close", "content-length: 100"],
+ headers[0..=1]
+ );
}
#[test]
fn test_propagate_protocol_to_response() {
- let _mock = mock("GET", "/").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _mock = s.mock("GET", "/").create();
- let stream = request_stream("1.0", "GET /", "", "");
- stream.shutdown(Shutdown::Write).unwrap();
+ let stream = request_stream("1.0", &host, "GET /", "", "");
let (status_line, _, _) = parse_stream(stream, true);
assert_eq!("HTTP/1.0 200 OK\r\n", status_line);
@@ -1246,12 +1552,14 @@ fn test_propagate_protocol_to_response() {
#[test]
fn test_large_body_without_content_length() {
+ let mut s = Server::new();
+ let host = s.host_with_port();
let body = "123".repeat(2048);
- let _mock = mock("POST", "/").match_body(body.as_str()).create();
+ let _mock = s.mock("POST", "/").match_body(body.as_str()).create();
- let stream = request_stream("1.0", "POST /", "", &body);
- stream.shutdown(Shutdown::Write).unwrap();
+ let headers = format!("content-length: {}\r\n", body.len());
+ let stream = request_stream("1.0", &host, "POST /", &headers, &body);
let (status_line, _, _) = parse_stream(stream, false);
assert_eq!("HTTP/1.0 200 OK\r\n", status_line);
@@ -1259,14 +1567,23 @@ fn test_large_body_without_content_length() {
#[test]
fn test_transfer_encoding_chunked() {
- let _mock = mock("POST", "/")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _mock = s
+ .mock("POST", "/")
.match_body("Hello, chunked world!")
.create();
let body = "3\r\nHel\r\n5\r\nlo, c\r\nD\r\nhunked world!\r\n0\r\n\r\n";
let (status, _, _) = parse_stream(
- request_stream("1.1", "POST /", "Transfer-Encoding: chunked\r\n", body),
+ request_stream(
+ "1.1",
+ &host,
+ "POST /",
+ "Transfer-Encoding: chunked\r\n",
+ body,
+ ),
false,
);
@@ -1275,116 +1592,156 @@ fn test_transfer_encoding_chunked() {
#[test]
fn test_match_exact_query() {
- let _m = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s
+ .mock("GET", "/hello")
.match_query(Matcher::Exact("number=one".to_string()))
.create();
- let (status_line, _, _) = request("GET /hello?number=one", "");
+ let (status_line, _, _) = request(&host, "GET /hello?number=one", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
- let (status_line, _, _) = request("GET /hello?number=two", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(&host, "GET /hello?number=two", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
}
#[test]
fn test_match_exact_query_via_path() {
- let _m = mock("GET", "/hello?number=one").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s.mock("GET", "/hello?number=one").create();
- let (status_line, _, _) = request("GET /hello?number=one", "");
+ let (status_line, _, _) = request(&host, "GET /hello?number=one", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
- let (status_line, _, _) = request("GET /hello?number=two", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(&host, "GET /hello?number=two", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
}
#[test]
fn test_match_partial_query_by_regex() {
- let _m = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s
+ .mock("GET", "/hello")
.match_query(Matcher::Regex("number=one".to_string()))
.create();
- let (status_line, _, _) = request("GET /hello?something=else&number=one", "");
+ let (status_line, _, _) = request(&host, "GET /hello?something=else&number=one", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
}
#[test]
fn test_match_partial_query_by_urlencoded() {
- let _m = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s
+ .mock("GET", "/hello")
.match_query(Matcher::UrlEncoded("num ber".into(), "o ne".into()))
.create();
- let (status_line, _, _) = request("GET /hello?something=else&num%20ber=o%20ne", "");
+ let (status_line, _, _) = request(&host, "GET /hello?something=else&num%20ber=o%20ne", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
- let (status_line, _, _) = request("GET /hello?something=else&number=one", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(&host, "GET /hello?something=else&number=one", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
}
#[test]
fn test_match_partial_query_by_regex_all_of() {
- let _m = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s
+ .mock("GET", "/hello")
.match_query(Matcher::AllOf(vec![
Matcher::Regex("number=one".to_string()),
Matcher::Regex("hello=world".to_string()),
]))
.create();
- let (status_line, _, _) = request("GET /hello?hello=world&something=else&number=one", "");
+ let (status_line, _, _) = request(
+ &host,
+ "GET /hello?hello=world&something=else&number=one",
+ "",
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
- let (status_line, _, _) = request("GET /hello?hello=world&something=else", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(&host, "GET /hello?hello=world&something=else", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
}
#[test]
fn test_match_partial_query_by_urlencoded_all_of() {
- let _m = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s
+ .mock("GET", "/hello")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("num ber".into(), "o ne".into()),
Matcher::UrlEncoded("hello".into(), "world".into()),
]))
.create();
- let (status_line, _, _) = request("GET /hello?hello=world&something=else&num%20ber=o%20ne", "");
+ let (status_line, _, _) = request(
+ &host,
+ "GET /hello?hello=world&something=else&num%20ber=o%20ne",
+ "",
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
- let (status_line, _, _) = request("GET /hello?hello=world&something=else", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(&host, "GET /hello?hello=world&something=else", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
}
#[test]
fn test_match_query_with_non_percent_url_escaping() {
- let _m = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s
+ .mock("GET", "/hello")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("num ber".into(), "o ne".into()),
Matcher::UrlEncoded("hello".into(), "world".into()),
]))
.create();
- let (status_line, _, _) = request("GET /hello?hello=world&something=else&num+ber=o+ne", "");
+ let (status_line, _, _) = request(
+ &host,
+ "GET /hello?hello=world&something=else&num+ber=o+ne",
+ "",
+ );
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
}
#[test]
fn test_match_missing_query() {
- let _m = mock("GET", "/hello").match_query(Matcher::Missing).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s
+ .mock("GET", "/hello")
+ .match_query(Matcher::Missing)
+ .create();
- let (status_line, _, _) = request("GET /hello?", "");
+ let (status_line, _, _) = request(&host, "GET /hello?", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
- let (status_line, _, _) = request("GET /hello?number=one", "");
- assert_eq!("HTTP/1.1 501 Mock Not Found\r\n", status_line);
+ let (status_line, _, _) = request(&host, "GET /hello?number=one", "");
+ assert_eq!("HTTP/1.1 501 Not Implemented\r\n", status_line);
}
#[test]
fn test_anyof_exact_path_and_query_matcher() {
- let mock = mock(
- "GET",
- Matcher::AnyOf(vec![Matcher::Exact("/hello?world".to_string())]),
- )
- .create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let mock = s
+ .mock(
+ "GET",
+ Matcher::AnyOf(vec![Matcher::Exact("/hello?world".to_string())]),
+ )
+ .create();
- let (status_line, _, _) = request("GET /hello?world", "");
+ let (status_line, _, _) = request(&host, "GET /hello?world", "");
assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
mock.assert();
@@ -1392,17 +1749,26 @@ fn test_anyof_exact_path_and_query_matcher() {
#[test]
fn test_default_headers() {
- let _m = mock("GET", "/").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s.mock("GET", "/").create();
- let (_, headers, _) = request("GET /", "");
- assert_eq!(vec!["connection: close", "content-length: 0"], headers);
+ let (_, headers, _) = request(&host, "GET /", "");
+ assert_eq!(3, headers.len());
+ assert_eq!(
+ vec!["connection: close", "content-length: 0"],
+ headers[0..=1]
+ );
+ let date_parts: Vec<&str> = headers[2].split(':').collect();
+ assert_eq!("date", date_parts[0]);
}
#[test]
fn test_missing_create_bad() {
testing_logger::setup();
- let m = mock("GET", "/");
+ let mut s = Server::new();
+ let m = s.mock("GET", "/");
drop(m);
// Expecting one warning
@@ -1425,7 +1791,8 @@ fn test_missing_create_bad() {
fn test_missing_create_good() {
testing_logger::setup();
- let m = mock("GET", "/").create();
+ let mut s = Server::new();
+ let m = s.mock("GET", "/").create();
drop(m);
// No warnings should occur
@@ -1442,13 +1809,16 @@ fn test_missing_create_good() {
#[test]
fn test_same_endpoint_different_responses() {
- let mock_200 = mock("GET", "/hello").with_status(200).create();
- let mock_404 = mock("GET", "/hello").with_status(404).create();
- let mock_500 = mock("GET", "/hello").with_status(500).create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+
+ let mock_200 = s.mock("GET", "/hello").with_status(200).create();
+ let mock_404 = s.mock("GET", "/hello").with_status(404).create();
+ let mock_500 = s.mock("GET", "/hello").with_status(500).create();
- let response_200 = request("GET /hello", "");
- let response_404 = request("GET /hello", "");
- let response_500 = request("GET /hello", "");
+ let response_200 = request(&host, "GET /hello", "");
+ let response_404 = request(&host, "GET /hello", "");
+ let response_500 = request(&host, "GET /hello", "");
mock_200.assert();
mock_404.assert();
@@ -1461,18 +1831,22 @@ fn test_same_endpoint_different_responses() {
#[test]
fn test_same_endpoint_different_responses_last_one_forever() {
- let _mock_200 = mock("GET", "/hello").with_status(200).create();
- let _mock_404 = mock("GET", "/hello").with_status(404).create();
- let _mock_500 = mock("GET", "/hello")
+ let mut s = Server::new();
+ let host = s.host_with_port();
+
+ let _mock_200 = s.mock("GET", "/hello").with_status(200).create();
+ let _mock_404 = s.mock("GET", "/hello").with_status(404).create();
+ let _mock_500 = s
+ .mock("GET", "/hello")
.expect_at_least(1)
.with_status(500)
.create();
- let response_200 = request("GET /hello", "");
- let response_404 = request("GET /hello", "");
- let response_500_1 = request("GET /hello", "");
- let response_500_2 = request("GET /hello", "");
- let response_500_3 = request("GET /hello", "");
+ let response_200 = request(&host, "GET /hello", "");
+ let response_404 = request(&host, "GET /hello", "");
+ let response_500_1 = request(&host, "GET /hello", "");
+ let response_500_2 = request(&host, "GET /hello", "");
+ let response_500_3 = request(&host, "GET /hello", "");
assert_eq!(response_200.0, "HTTP/1.1 200 OK\r\n");
assert_eq!(response_404.0, "HTTP/1.1 404 Not Found\r\n");
@@ -1483,21 +1857,127 @@ fn test_same_endpoint_different_responses_last_one_forever() {
#[test]
fn test_matched_bool() {
- let m = mock("GET", "/").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let m = s.mock("GET", "/").create();
- let (_, _, _) = request_with_body("GET /", "", "");
+ let (_, _, _) = request_with_body(&host, "GET /", "", "");
m.assert();
assert!(m.matched(), "matched method returns correctly");
- let (_, _, _) = request_with_body("GET /", "", "");
+ let (_, _, _) = request_with_body(&host, "GET /", "", "");
assert!(!m.matched(), "matched method returns correctly");
}
#[test]
fn test_invalid_header_field_name() {
- let _m = mock("GET", "/").create();
+ let mut s = Server::new();
+ let host = s.host_with_port();
+ let _m = s.mock("GET", "/").create();
+
+ let (uppercase_status_line, _, _body) = request(&host, "GET /", "Bad Header: something\r\n");
+ assert_eq!("HTTP/1.1 400 Bad Request\r\n", uppercase_status_line);
+}
+
+#[test]
+fn test_running_multiple_servers() {
+ let mut s1 = Server::new();
+ let mut s2 = Server::new();
+ let mut s3 = Server::new();
+
+ let _m = s2.mock("GET", "/").with_body("s2").create();
+ let _m = s1.mock("GET", "/").with_body("s1").create();
+ let _m = s3.mock("GET", "/").with_body("s3").create();
+
+ let (_, _, body1) = request_with_body(&s1.host_with_port(), "GET /", "", "");
+ let (_, _, body2) = request_with_body(&s2.host_with_port(), "GET /", "", "");
+ let (_, _, body3) = request_with_body(&s3.host_with_port(), "GET /", "", "");
+
+ assert!(s1.host_with_port() != s2.host_with_port());
+ assert!(s2.host_with_port() != s3.host_with_port());
+ assert_eq!("s1", body1);
+ assert_eq!("s2", body2);
+ assert_eq!("s3", body3);
+}
+
+#[test]
+fn test_running_lots_of_servers_wont_block() {
+ let mut s1 = Server::new();
+ let _s2 = Server::new();
+ let _s3 = Server::new();
+ let _s4 = Server::new();
+ let _s5 = Server::new();
+ let _s7 = Server::new();
+ let _s8 = Server::new();
+ let _s9 = Server::new();
+ let _s10 = Server::new();
+ let _s11 = Server::new();
+ let _s12 = Server::new();
+ let _s13 = Server::new();
+ let _s14 = Server::new();
+ let _s15 = Server::new();
+ let _s17 = Server::new();
+ let _s18 = Server::new();
+ let _s19 = Server::new();
+ let _s20 = Server::new();
+ let _s21 = Server::new();
+ let _s22 = Server::new();
+ let _s23 = Server::new();
+ let _s24 = Server::new();
+ let _s25 = Server::new();
+ let _s27 = Server::new();
+ let _s28 = Server::new();
+ let _s29 = Server::new();
+ let mut s30 = Server::new();
+
+ let m1 = s1.mock("GET", "/pool").create();
+ let (_, _, _) = request_with_body(&s1.host_with_port(), "GET /pool", "", "");
+ m1.assert();
+
+ let m30 = s30.mock("GET", "/pool").create();
+ let (_, _, _) = request_with_body(&s30.host_with_port(), "GET /pool", "", "");
+ m30.assert();
+}
+
+#[tokio::test]
+async fn test_http2_requests_async() {
+ let mut s = Server::new_async().await;
+ let m1 = s.mock("GET", "/").with_body("test").create_async().await;
+
+ let (status, _headers, body) =
+ hyper_request(&s.host_with_port(), "2.0", "GET", "/", None).await;
+ assert_eq!(200, status);
+ assert_eq!("test", body);
+
+ m1.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_simple_route_mock_async() {
+ let mut s = Server::new_async().await;
+ let _m1 = s
+ .mock("GET", "/hello")
+ .with_body("world")
+ .create_async()
+ .await;
+
+ let (status, _headers, body) =
+ hyper_request(&s.host_with_port(), "1.1", "GET", "/hello", None).await;
+ assert_eq!(200, status);
+ assert_eq!("world", body);
+}
+
+#[tokio::test]
+async fn test_two_route_mocks_async() {
+ let mut s = Server::new_async().await;
+ let _m1 = s.mock("GET", "/a").with_body("aaa").create_async();
+ let _m2 = s.mock("GET", "/b").with_body("bbb").create_async();
- let (uppercase_status_line, _, body) = request("GET /", "Bad Header: something\r\n");
- assert_eq!("HTTP/1.1 422 Mock Error\r\n", uppercase_status_line);
- assert_eq!(body, httparse::Error::HeaderName.to_string())
+ let (_m1, _m2) = futures::join!(_m1, _m2);
+
+ let (_, _, body_a) = hyper_request(&s.host_with_port(), "1.1", "GET", "/a", None).await;
+ assert_eq!("aaa", body_a);
+
+ let (_, _, body_b) = hyper_request(&s.host_with_port(), "1.1", "GET", "/b", None).await;
+ assert_eq!("bbb", body_b);
}