Skip to content

feat: support matching form url encoded fields #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions src/matchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,216 @@ impl Match for QueryParamIsMissingMatcher {
!request.url.query_pairs().any(|(k, _)| k == self.0)
}
}

#[derive(Debug)]
/// Match **exactly** the form url encoded field of a request.
///
/// ### Example:
/// ```rust
/// use wiremock::{MockServer, Mock, ResponseTemplate};
/// use wiremock::matchers::form_url_encoded;
/// use url::form_urlencoded;
///
/// #[async_std::main]
/// async fn main() {
/// // Arrange
/// let mock_server = MockServer::start().await;
///
/// Mock::given(form_url_encoded("hello", "world"))
/// .respond_with(ResponseTemplate::new(200))
/// .mount(&mock_server)
/// .await;
///
/// let form = form_urlencoded::Serializer::new(String::new())
/// .append_pair("hello", "world")
/// .finish();
///
/// let client = reqwest::Client::new();
///
/// // Act
/// let status = client.post(&mock_server.uri())
/// .body(form)
/// .send()
/// .await
/// .unwrap()
/// .status();
///
/// // Assert
/// assert_eq!(status, 200);
/// }
/// ```
pub struct FormUrlEncodedExactMatcher(String, String);

impl FormUrlEncodedExactMatcher {
/// Specify the expected value for a field inside the form url encoded body.
pub fn new<K: Into<String>, V: Into<String>>(key: K, value: V) -> Self {
let key = key.into();
let value = value.into();
Self(key, value)
}
}

/// Shorthand for [`FormUrlEncodedExactMatcher::new`].
pub fn form_url_encoded<K, V>(key: K, value: V) -> FormUrlEncodedExactMatcher
where
K: Into<String>,
V: Into<String>,
{
FormUrlEncodedExactMatcher::new(key, value)
}

impl Match for FormUrlEncodedExactMatcher {
fn matches(&self, request: &Request) -> bool {
request
.body_form_urlencoded()
.any(|(k, v)| k == self.0 && v == self.1)
}
}

#[derive(Debug)]
/// Match when the form url encoded body contains the specified value as a substring.
///
/// ### Example:
/// ```rust
/// use wiremock::{MockServer, Mock, ResponseTemplate};
/// use wiremock::matchers::form_url_encoded_contains;
/// use url::form_urlencoded;
///
/// #[async_std::main]
/// async fn main() {
/// // Arrange
/// let mock_server = MockServer::start().await;
///
/// // It matches since "world" is a substring of "some_world".
/// Mock::given(form_url_encoded_contains("hello", "world"))
/// .respond_with(ResponseTemplate::new(200))
/// .mount(&mock_server)
/// .await;
///
/// let form = form_urlencoded::Serializer::new(String::new())
/// .append_pair("hello", "some_world")
/// .finish();
///
/// let client = reqwest::Client::new();
///
/// // Act
/// let status = client.post(&mock_server.uri())
/// .body(form)
/// .send()
/// .await
/// .unwrap()
/// .status();
///
/// // Assert
/// assert_eq!(status, 200);
/// }
/// ```
pub struct FormUrlEncodedContainsMatcher(String, String);

impl FormUrlEncodedContainsMatcher {
/// Specify the key value pair that the form url encoded body should contain.
pub fn new<K: Into<String>, V: Into<String>>(key: K, value: V) -> Self {
let key = key.into();
let value = value.into();
Self(key, value)
}
}

/// Shorthand for [`FormUrlEncodedContainsMatcher::new`].
pub fn form_url_encoded_contains<K, V>(key: K, value: V) -> FormUrlEncodedContainsMatcher
where
K: Into<String>,
V: Into<String>,
{
FormUrlEncodedContainsMatcher::new(key, value)
}

impl Match for FormUrlEncodedContainsMatcher {
fn matches(&self, request: &Request) -> bool {
request
.body_form_urlencoded()
.any(|(k, v)| k == self.0 && v.contains(self.1.as_str()))
}
}

#[derive(Debug)]
/// Only match requests that do **not** contain a specified form url encoded field.
///
/// ### Example:
/// ```rust
/// use wiremock::{MockServer, Mock, ResponseTemplate};
/// use wiremock::matchers::{method, form_url_encoded_field_is_missing};
/// use url::form_urlencoded;
///
/// #[async_std::main]
/// async fn main() {
/// // Arrange
/// let mock_server = MockServer::start().await;
///
/// Mock::given(method("POST"))
/// .and(form_url_encoded_field_is_missing("unexpected"))
/// .respond_with(ResponseTemplate::new(200))
/// .mount(&mock_server)
/// .await;
///
/// let form = form_urlencoded::Serializer::new(String::new())
/// .append_pair("hello", "world")
/// .finish();
///
/// let client = reqwest::Client::new();
///
/// // Act
/// let ok_status = client.post(mock_server.uri().to_string())
/// .body(form)
/// .send()
/// .await
/// .unwrap()
/// .status();
///
/// // Assert
/// assert_eq!(ok_status, 200);
///
/// let form = form_urlencoded::Serializer::new(String::new())
/// .append_pair("unexpected", "foo")
/// .finish();
///
/// let client = reqwest::Client::new();
///
/// // Act
/// let err_status = client.post(mock_server.uri())
/// .body(form)
/// .send()
/// .await
/// .unwrap().status();
///
/// // Assert
/// assert_eq!(err_status, 404);
/// }
/// ```
pub struct FormUrlEncodedFieldIsMissingMatcher(String);

impl FormUrlEncodedFieldIsMissingMatcher {
/// Specify the form field that is expected to not exist.
pub fn new<K: Into<String>>(key: K) -> Self {
let key = key.into();
Self(key)
}
}

/// Shorthand for [`FormUrlEncodedFieldIsMissingMatcher::new`].
pub fn form_url_encoded_field_is_missing<K>(key: K) -> FormUrlEncodedFieldIsMissingMatcher
where
K: Into<String>,
{
FormUrlEncodedFieldIsMissingMatcher::new(key)
}

impl Match for FormUrlEncodedFieldIsMissingMatcher {
fn matches(&self, request: &Request) -> bool {
!request.body_form_urlencoded().any(|(k, _)| k == self.0)
}
}

/// Match an incoming request if its body is encoded as JSON and can be deserialized
/// according to the specified schema.
///
Expand Down
6 changes: 5 additions & 1 deletion src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::fmt;
use http::{HeaderMap, Method};
use http_body_util::BodyExt;
use serde::de::DeserializeOwned;
use url::Url;
use url::{form_urlencoded, Url};

pub const BODY_PRINT_LIMIT: usize = 10_000;

Expand Down Expand Up @@ -48,6 +48,10 @@ impl Request {
serde_json::from_slice(&self.body)
}

pub fn body_form_urlencoded(&self) -> form_urlencoded::Parse<'_> {
form_urlencoded::parse(&self.body)
}

pub(crate) async fn from_hyper(request: hyper::Request<hyper::body::Incoming>) -> Request {
let (parts, body) = request.into_parts();
let url = match parts.uri.authority() {
Expand Down
82 changes: 81 additions & 1 deletion tests/mocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ use std::io::ErrorKind;
use std::iter;
use std::net::TcpStream;
use std::time::Duration;
use wiremock::matchers::{body_json, body_partial_json, method, path, PathExactMatcher};
use url::form_urlencoded;
use wiremock::matchers::{
body_json, body_partial_json, form_url_encoded, form_url_encoded_field_is_missing, method,
path, PathExactMatcher,
};
use wiremock::{Mock, MockServer, Request, ResponseTemplate};

#[async_std::test]
Expand Down Expand Up @@ -228,6 +232,82 @@ async fn body_json_partial_matches_a_part_of_response_json() {
assert_eq!(response.status(), StatusCode::OK);
}

#[async_std::test]
async fn body_form_matches_independent_of_key_ordering() {
let body = form_urlencoded::Serializer::new(String::new())
.append_pair("b", "2")
.append_pair("a", "1")
.finish();

let mock_server = MockServer::start().await;
let response = ResponseTemplate::new(200);
let mock = Mock::given(method("POST"))
.and(form_url_encoded("a", "1"))
.and(form_url_encoded("b", "2"))
.respond_with(response);
mock_server.register(mock).await;

let client = reqwest::Client::new();

// Act
let response = client
.post(mock_server.uri())
.body(body)
.send()
.await
.unwrap();

// Assert
assert_eq!(response.status(), StatusCode::OK);
}

#[async_std::test]
async fn body_form_partial_matches() {
let body: String = form_urlencoded::Serializer::new(String::new())
.append_pair("a", "1")
.append_pair("b", "2")
.finish();

let mock_server = MockServer::start().await;
let response = ResponseTemplate::new(200);
let mock = Mock::given(method("POST"))
.and(form_url_encoded_field_is_missing("c"))
.respond_with(response);
mock_server.register(mock).await;

let client = reqwest::Client::new();

// Act
let response = client
.post(mock_server.uri())
.body(body)
.send()
.await
.unwrap();

// Assert
assert_eq!(response.status(), StatusCode::OK);

let body: String = form_urlencoded::Serializer::new(String::new())
.append_pair("a", "1")
.append_pair("b", "2")
.append_pair("c", "unexpected")
.finish();

let client = reqwest::Client::new();

// Act
let err_response = client
.post(mock_server.uri())
.body(body)
.send()
.await
.unwrap();

// Assert
assert_eq!(err_response.status(), StatusCode::NOT_FOUND);
}

#[should_panic(expected = "\
Wiremock can't match the path `abcd?` because it contains a `?`. You must use `wiremock::matchers::query_param` to match on query parameters (the part of the path after the `?`).")]
#[async_std::test]
Expand Down