Repository: lipanski/mockito (Rust)

Issue:
Exact Matchers on get strings with '?' inside an AnyOf matcher return 501 since 0.18.0
I have created a minimal [repro](https://github.com/thomasetter/mockito_query_bug_repro).

Bisecting pointed to #77 as the PR which broke this.

#83 could be related, as the symptoms in both cases are `501` errors.

The [test](https://github.com/thomasetter/mockito_query_bug_repro/blob/master/tests/repro.rs) which fails is:
```
#[test]
fn query_in_exact_test() {
    env_logger::init();
    let mocker = mockito::mock(
        "GET",
        Matcher::AnyOf(vec![
            Matcher::Exact("/bar?stuff".to_string()),
        ]),
    )
    .expect(1)
    .create();
    let moved_mocker = mocker;
    let client = reqwest::Client::new();
    println!(
        "{:?}",
        client.get(&format!("{}/bar?stuff", mockito::server_url())).send()
    );
    moved_mocker.assert();
}
```

I did not see any obvious reasons why this got broken by #77, do you have any ideas?


Discussion:
@thomasetter I managed to figure out the problem. before #77 the path and query parts were being matched together. though #77  preserved backwards compatibility in regards to matching the query part in calls like `mock("GET", "/path?with_query")`, under the hood this is translated right away into something like `mock("GET", "/path").match_query("with_query")`. this happens here: https://github.com/lipanski/mockito/blob/5c7b6c172510a9378421a3ec0d1a350c602adcfc/src/lib.rs#L689-L700

the problem with that piece of code is that your `AnyOf` gets matched to the second pattern. `Mock#path` ends up getting set to `/bar?stuff` while `Mock#query` is set to `Matcher::Any`. because https://github.com/lipanski/mockito/pull/77/files#diff-4ce93534efc34e923ce01e975eb7ed80R18 is now splitting the path matching from the query matching, Mockito is trying to match the requested path "/bar" to the mocked path "/bar?stuff" and fails in the process.

a good workaround could be doing something like this:

```rust
let mocker = mockito::mock(
    "GET",
    Matcher::AnyOf(vec![
        Matcher::Exact("/bar".to_string()),
    ]),
)
.match_query("stuff")
.expect(1)
.create();
```

fixing this inside the crate could be done in two ways, but both aren't exactly ideal:

(1) that [piece of code](https://github.com/lipanski/mockito/blob/5c7b6c172510a9378421a3ec0d1a350c602adcfc/src/lib.rs#L689-L700) inside the constructor could traverse the matcher recursively in search of the query. however in the case of an `AnyOf` matcher it should consider the query part also an `AnyOf`, which in turn translate to something different than you intended in your initial code. this is what I mean:

```rust
# This accepts requests to either /bar?stuff or /foo?other
let _m1 = mockito::mock(
    "GET",
    Matcher::AnyOf(vec![
        Matcher::Exact("/bar?stuff".to_string()),
        Matcher::Exact("/foo?other".to_string()),
    ]),
)
.create();

# This accepts requests to /bar?stuff, /bar?other, /foo?stuff, /foo?other
let _m2 = mockito::mock(
    "GET",
    Matcher::AnyOf(vec![
        Matcher::Exact("/bar".to_string()),
        Matcher::Exact("/foo".to_string())
    ])
).match_query(Matcher::AnyOf(
    vec![
        Matcher::Exact("stuff"),
        Matcher::Exact("other")
])
.create();
```

(2) bring path and query matching back together, though this means breaking the interface again and being less flexible.

all in all, I'd be tempted to simply introduce a warning or an error message in these cases.

btw could you share your extended use case? how does your entire mock look like? is it going in the direction of my example above with `/foo?stuff` and `/bar?other` or can you use the workaround I suggested?
Thanks for the feedback!

I have looked into this some more and I think I found a way which should not break much.

#87 basically implements (2) but still supports the `match_query` interface.

What do you think, would something like that PR work for you?
Released in 0.22.0



Patch:
diff --git a/src/lib.rs b/src/lib.rs
index 268b602..e5e0be2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -136,7 +136,7 @@
 //! 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:
@@ -667,6 +667,27 @@ impl Matcher {
     }
 }
 
+#[derive(Clone, PartialEq, Debug)]
+enum PathAndQueryMatcher {
+    Unified(Matcher),
+    Split(Box<Matcher>, Box<Matcher>),
+}
+
+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("");
+
+                return path_matcher.matches_value(path) && query_matcher.matches_value(query);
+            }
+        }
+    }
+}
+
 ///
 /// Stores information about a mocked request. Should be initialized via `mockito::mock()`.
 ///
@@ -674,8 +695,7 @@ impl Matcher {
 pub struct Mock {
     id: String,
     method: String,
-    path: Matcher,
-    query: Matcher,
+    path: PathAndQueryMatcher,
     headers: Vec<(String, Matcher)>,
     body: Matcher,
     response: Response,
@@ -686,24 +706,11 @@ pub struct Mock {
 
 impl Mock {
     fn new<P: Into<Matcher>>(method: &str, path: P) -> Self {
-        let (path, query) =
-            match path.into() {
-                // We also allow setting the query as part of the path argument
-                // but we split it under the hood into `Matcher::Exact` elements.
-                Matcher::Exact(ref raw_path) if raw_path.contains('?') => {
-                    let mut parts = raw_path.splitn(2, '?');
-                    (parts.next().unwrap().into(), parts.next().unwrap_or("").into())
-                },
-                other => {
-                    (other, Matcher::Any)
-                },
-            };
 
         Self {
             id: thread_rng().sample_iter(&Alphanumeric).take(24).collect(),
             method: method.to_owned().to_uppercase(),
-            path,
-            query,
+            path: PathAndQueryMatcher::Unified(path.into()),
             headers: Vec::new(),
             body: Matcher::Any,
             response: Response::default(),
@@ -747,7 +754,17 @@ impl Mock {
     /// ```
     ///
     pub fn match_query<M: Into<Matcher>>(mut self, query: M) -> Self {
-        self.query = query.into();
+        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
     }
@@ -980,88 +997,142 @@ impl Drop for Mock {
     }
 }
 
-impl fmt::Display for Mock {
+impl fmt::Display for PathAndQueryMatcher {
     #[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_str(" ");
+        match self {
+            PathAndQueryMatcher::Unified(matcher) => {
+                match matcher {
+                    Matcher::Exact(ref value) => {
+                        formatted.push_str(value);
+                    },
+                    Matcher::Regex(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (regex)")
+                    },
+                    Matcher::Json(ref json_obj) => {
+                        formatted.push_str(&json_obj.to_string());
+                        formatted.push_str(" (json)")
+                    },
+                    Matcher::JsonString(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (json)")
+                    },
+                    Matcher::PartialJson(ref json_obj) => {
+                        formatted.push_str(&json_obj.to_string());
+                        formatted.push_str(" (partial json)")
+                    }
+                    Matcher::PartialJsonString(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (partial json)")
+                    },
+                    Matcher::UrlEncoded(ref field, ref value) => {
+                        formatted.push_str(field);
+                        formatted.push_str("=");
+                        formatted.push_str(value);
+                        formatted.push_str(" (urlencoded)")
+                    },
+                    Matcher::Any => formatted.push_str("(any)"),
+                    Matcher::AnyOf(..) => formatted.push_str("(any of)"),
+                    Matcher::AllOf(..) => formatted.push_str("(all of)"),
+                    Matcher::Missing => formatted.push_str("(missing)"),
+                }
 
-        match self.path {
-            Matcher::Exact(ref value) => {
-                formatted.push_str(value);
-            },
-            Matcher::Regex(ref value) => {
-                formatted.push_str(value);
-                formatted.push_str(" (regex)")
-            },
-            Matcher::Json(ref json_obj) => {
-                formatted.push_str(&json_obj.to_string());
-                formatted.push_str(" (json)")
-            },
-            Matcher::JsonString(ref value) => {
-                formatted.push_str(value);
-                formatted.push_str(" (json)")
+                formatted.push_str("\r\n");
             },
-            Matcher::PartialJson(ref json_obj) => {
-                formatted.push_str(&json_obj.to_string());
-                formatted.push_str(" (partial json)")
+            PathAndQueryMatcher::Split(path, query) => {
+                match **path {
+                    Matcher::Exact(ref value) => {
+                        formatted.push_str(value);
+                    },
+                    Matcher::Regex(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (regex)")
+                    },
+                    Matcher::Json(ref json_obj) => {
+                        formatted.push_str(&json_obj.to_string());
+                        formatted.push_str(" (json)")
+                    },
+                    Matcher::JsonString(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (json)")
+                    },
+                    Matcher::PartialJson(ref json_obj) => {
+                        formatted.push_str(&json_obj.to_string());
+                        formatted.push_str(" (partial json)")
+                    }
+                    Matcher::PartialJsonString(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (partial json)")
+                    },
+                    Matcher::UrlEncoded(ref field, ref value) => {
+                        formatted.push_str(field);
+                        formatted.push_str("=");
+                        formatted.push_str(value);
+                        formatted.push_str(" (urlencoded)")
+                    },
+                    Matcher::Any => formatted.push_str("(any)"),
+                    Matcher::AnyOf(..) => formatted.push_str("(any of)"),
+                    Matcher::AllOf(..) => formatted.push_str("(all of)"),
+                    Matcher::Missing => formatted.push_str("(missing)"),
+                }
+
+                formatted.push_str("?");
+
+                match **query {
+                    Matcher::Exact(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str("\r\n");
+                    },
+                    Matcher::Regex(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (regex)\r\n")
+                    },
+                    Matcher::Json(ref json_obj) => {
+                        formatted.push_str(&json_obj.to_string());
+                        formatted.push_str(" (json)\r\n")
+                    },
+                    Matcher::JsonString(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (json)\r\n")
+                    },
+                    Matcher::PartialJson(ref json_obj) => {
+                        formatted.push_str(&json_obj.to_string());
+                        formatted.push_str(" (partial json)\r\n")
+                    },
+                    Matcher::PartialJsonString(ref value) => {
+                        formatted.push_str(value);
+                        formatted.push_str(" (partial json)\r\n")
+                    },
+                    Matcher::UrlEncoded(ref field, ref value) => {
+                        formatted.push_str(field);
+                        formatted.push_str("=");
+                        formatted.push_str(value);
+                    },
+                    Matcher::Any => formatted.push_str("(any)\r\n"),
+                    Matcher::AnyOf(..) => formatted.push_str("(any of)\r\n"),
+                    Matcher::AllOf(..) => formatted.push_str("(all of)\r\n"),
+                    Matcher::Missing => formatted.push_str("(missing)\r\n"),
+                }
             }
-            Matcher::PartialJsonString(ref value) => {
-                formatted.push_str(value);
-                formatted.push_str(" (partial json)")
-            },
-            Matcher::UrlEncoded(ref field, ref value) => {
-                formatted.push_str(field);
-                formatted.push_str("=");
-                formatted.push_str(value);
-                formatted.push_str(" (urlencoded)")
-            },
-            Matcher::Any => formatted.push_str("(any)"),
-            Matcher::AnyOf(..) => formatted.push_str("(any of)"),
-            Matcher::AllOf(..) => formatted.push_str("(all of)"),
-            Matcher::Missing => formatted.push_str("(missing)"),
         }
 
-        formatted.push_str("?");
+        f.write_str(&formatted)
+    }
+}
+
+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_str(" ");
+        formatted.push_str(&self.path.to_string());
 
-        match self.query {
-            Matcher::Exact(ref value) => {
-                formatted.push_str(value);
-                formatted.push_str("\r\n");
-            },
-            Matcher::Regex(ref value) => {
-                formatted.push_str(value);
-                formatted.push_str(" (regex)\r\n")
-            },
-            Matcher::Json(ref json_obj) => {
-                formatted.push_str(&json_obj.to_string());
-                formatted.push_str(" (json)\r\n")
-            },
-            Matcher::JsonString(ref value) => {
-                formatted.push_str(value);
-                formatted.push_str(" (json)\r\n")
-            },
-            Matcher::PartialJson(ref json_obj) => {
-                formatted.push_str(&json_obj.to_string());
-                formatted.push_str(" (partial json)\r\n")
-            },
-            Matcher::PartialJsonString(ref value) => {
-                formatted.push_str(value);
-                formatted.push_str(" (partial json)\r\n")
-            },
-            Matcher::UrlEncoded(ref field, ref value) => {
-                formatted.push_str(field);
-                formatted.push_str("=");
-                formatted.push_str(value);
-            },
-            Matcher::Any => formatted.push_str("(any)\r\n"),
-            Matcher::AnyOf(..) => formatted.push_str("(any of)\r\n"),
-            Matcher::AllOf(..) => formatted.push_str("(all of)\r\n"),
-            Matcher::Missing => formatted.push_str("(missing)\r\n"),
-        }
 
         for &(ref key, ref value) in &self.headers {
             match value {
diff --git a/src/request.rs b/src/request.rs
index c84325c..b0d0665 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -13,7 +13,6 @@ pub struct Request {
     pub version: (u8, u8),
     pub method: String,
     pub path: String,
-    pub query: String,
     pub headers: Vec<(String, String)>,
     pub body: Vec<u8>,
     error: Option<String>,
@@ -118,7 +117,6 @@ impl Default for Request {
             version: (1, 1),
             method: String::new(),
             path: String::new(),
-            query: String::new(),
             headers: Vec::new(),
             body: Vec::new(),
             error: None,
@@ -174,9 +172,7 @@ impl<'a> From<&'a TcpStream> for Request {
                         }
 
                         if let Some(a) = req.path {
-                            let mut parts = a.splitn(2, '?');
-                            request.path += parts.next().unwrap();
-                            request.query += parts.next().unwrap_or("");
+                            request.path += a
                         }
 
                         for h in req.headers {
@@ -207,7 +203,7 @@ impl<'a> From<&'a TcpStream> for Request {
 
 impl fmt::Display for Request {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "\r\n{} {}?{}\r\n", &self.method, &self.path, &self.query)?;
+        write!(f, "\r\n{} {}\r\n", &self.method, &self.path)?;
 
         for &(ref key, ref value) in &self.headers {
             writeln!(f, "{}: {}\r", key, value)?;
diff --git a/src/server.rs b/src/server.rs
index 21c4e2b..c5502ac 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -17,9 +17,6 @@ impl Mock {
         self.path.matches_value(&request.path)
     }
 
-    fn query_matches(&self, request: &Request) -> bool {
-        self.query.matches_value(&request.query)
-    }
 
     fn headers_match(&self, request: &Request) -> bool {
         self.headers.iter().all(|&(ref field, ref expected)| {
@@ -36,7 +33,6 @@ impl<'a> PartialEq<Request> for &'a mut Mock {
     fn eq(&self, other: &Request) -> bool {
         self.method_matches(other)
             && self.path_matches(other)
-            && self.query_matches(other)
             && self.headers_match(other)
             && self.body_matches(other)
     }


Test patch:
diff --git a/tests/lib.rs b/tests/lib.rs
index c65343a..57e27b5 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -728,21 +728,21 @@ fn test_body_from_file() {
 fn test_display_mock_matching_exact_path() {
     let mock = mock("GET", "/hello");
 
-    assert_eq!("\r\nGET /hello?(any)\r\n", format!("{}", mock));
+    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()));
 
-    assert_eq!("\r\nGET ^/hello/\\d+$ (regex)?(any)\r\n", format!("{}", mock));
+    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);
 
-    assert_eq!("\r\nGET (any)?(any)\r\n", format!("{}", mock));
+    assert_eq!("\r\nGET (any)\r\n", format!("{}", mock));
 }
 
 #[test]
@@ -770,7 +770,7 @@ fn test_display_mock_matching_any_query() {
 fn test_display_mock_matching_exact_header() {
     let mock = mock("GET", "/").match_header("content-type", "text").create();
 
-    assert_eq!("\r\nGET /?(any)\r\ncontent-type: text\r\n", format!("{}", mock));
+    assert_eq!("\r\nGET /\r\ncontent-type: text\r\n", format!("{}", mock));
 }
 
 #[test]
@@ -782,35 +782,35 @@ fn test_display_mock_matching_multiple_headers() {
         .match_header("x-request-id", Matcher::Missing)
         .create();
 
-    assert_eq!("\r\nGET /?(any)\r\ncontent-type: text\r\ncontent-length: \\d+ (regex)\r\nauthorization: (any)\r\nx-request-id: (missing)\r\n", format!("{}", mock));
+    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]
 fn test_display_mock_matching_exact_body() {
     let mock = mock("POST", "/").match_body("hello").create();
 
-    assert_eq!("\r\nPOST /?(any)\r\nhello\r\n", format!("{}", mock));
+    assert_eq!("\r\nPOST /\r\nhello\r\n", format!("{}", mock));
 }
 
 #[test]
 fn test_display_mock_matching_regex_body() {
     let mock = mock("POST", "/").match_body(Matcher::Regex("hello".to_string())).create();
 
-    assert_eq!("\r\nPOST /?(any)\r\nhello\r\n", format!("{}", mock));
+    assert_eq!("\r\nPOST /\r\nhello\r\n", format!("{}", mock));
 }
 
 #[test]
 fn test_display_mock_matching_any_body() {
     let mock = mock("POST", "/").match_body(Matcher::Any).create();
 
-    assert_eq!("\r\nPOST /?(any)\r\n", format!("{}", mock));
+    assert_eq!("\r\nPOST /\r\n", format!("{}", mock));
 }
 
 #[test]
 fn test_display_mock_matching_headers_and_body() {
     let mock = mock("POST", "/").match_header("content-type", "text").match_body("hello").create();
 
-    assert_eq!("\r\nPOST /?(any)\r\ncontent-type: text\r\nhello\r\n", format!("{}", mock));
+    assert_eq!("\r\nPOST /\r\ncontent-type: text\r\nhello\r\n", format!("{}", mock));
 }
 
 #[test]
@@ -834,7 +834,7 @@ fn test_expect() {
 }
 
 #[test]
-#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello?(any)\r\n\n...but received 0\n")]
+#[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();
 
@@ -842,7 +842,7 @@ fn test_assert_panics_if_no_request_was_performed() {
 }
 
 #[test]
-#[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello?(any)\r\n\n...but received 2\n")]
+#[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();
 
@@ -853,7 +853,7 @@ fn test_assert_panics_with_too_few_requests() {
 }
 
 #[test]
-#[should_panic(expected = "\n> Expected 3 request(s) to:\n\r\nGET /hello?(any)\r\n\n...but received 4\n")]
+#[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();
 
@@ -866,7 +866,7 @@ fn test_assert_panics_with_too_many_requests() {
 }
 
 #[test]
-#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello?(any)\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?(any)\u{1b}[0m\n\u{1b}[32mGET\u{1b}[0m \u{1b}[42;37m/bye?\u{1b}[0m\n\n\n")]
+#[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\u{1b}[0m\n\u{1b}[32mGET\u{1b}[0m \u{1b}[42;37m/bye\u{1b}[0m\n\n\n")]
 fn test_assert_with_last_unmatched_request() {
     let mock = mock("GET", "/hello").create();
 
@@ -876,7 +876,7 @@ fn test_assert_with_last_unmatched_request() {
 }
 
 #[test]
-#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello?(any)\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?(any)\u{1b}[0m\n\u{1b}[32mGET\u{1b}[0m \u{1b}[42;37m/bye?\nauthorization: 1234\naccept: text\u{1b}[0m\n\n\n")]
+#[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\u{1b}[0m\n\u{1b}[32mGET\u{1b}[0m \u{1b}[42;37m/bye\nauthorization: 1234\naccept: text\u{1b}[0m\n\n\n")]
 fn test_assert_with_last_unmatched_request_and_headers() {
     let mock = mock("GET", "/hello").create();
 
@@ -886,7 +886,7 @@ fn test_assert_with_last_unmatched_request_and_headers() {
 }
 
 #[test]
-#[should_panic(expected = "\n> Expected 1 request(s) to:\n\r\nGET /hello?(any)\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")]
+#[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_assert_with_last_unmatched_request_and_body() {
     let mock = mock("GET", "/hello").create();
 
@@ -1073,3 +1073,19 @@ fn test_match_missing_query() {
     let (status_line, _, _) = request("GET /hello?number=one", "");
     assert_eq!("HTTP/1.1 501 Mock Not Found\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 (status_line, _, _) = request("GET /hello?world", "");
+    assert_eq!("HTTP/1.1 200 OK\r\n", status_line);
+
+    mock.assert();
+}