aws_smithy_protocol_test/
lib.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6/* Automatically managed default lints */
7#![cfg_attr(docsrs, feature(doc_auto_cfg))]
8/* End of automatically managed default lints */
9#![warn(
10    // missing_docs,
11    // rustdoc::missing_crate_level_docs,
12    unreachable_pub,
13    rust_2018_idioms
14)]
15
16mod urlencoded;
17mod xml;
18
19use crate::sealed::GetNormalizedHeader;
20use crate::xml::try_xml_equivalent;
21use assert_json_diff::assert_json_eq_no_panic;
22use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
23use aws_smithy_runtime_api::http::Headers;
24use http::{HeaderMap, Uri};
25use pretty_assertions::Comparison;
26use std::borrow::Cow;
27use std::collections::HashSet;
28use std::fmt::{self, Debug};
29use thiserror::Error;
30use urlencoded::try_url_encoded_form_equivalent;
31
32/// Helper trait for tests for float comparisons
33///
34/// This trait differs in float's default `PartialEq` implementation by considering all `NaN` values to
35/// be equal.
36pub trait FloatEquals {
37    fn float_equals(&self, other: &Self) -> bool;
38}
39
40impl FloatEquals for f64 {
41    fn float_equals(&self, other: &Self) -> bool {
42        (self.is_nan() && other.is_nan()) || self.eq(other)
43    }
44}
45
46impl FloatEquals for f32 {
47    fn float_equals(&self, other: &Self) -> bool {
48        (self.is_nan() && other.is_nan()) || self.eq(other)
49    }
50}
51
52impl<T> FloatEquals for Option<T>
53where
54    T: FloatEquals,
55{
56    fn float_equals(&self, other: &Self) -> bool {
57        match (self, other) {
58            (Some(this), Some(other)) => this.float_equals(other),
59            (None, None) => true,
60            _else => false,
61        }
62    }
63}
64
65#[derive(Debug, PartialEq, Eq, Error)]
66pub enum ProtocolTestFailure {
67    #[error("missing query param: expected `{expected}`, found {found:?}")]
68    MissingQueryParam {
69        expected: String,
70        found: Vec<String>,
71    },
72    #[error("forbidden query param present: `{expected}`")]
73    ForbiddenQueryParam { expected: String },
74    #[error("required query param missing: `{expected}`")]
75    RequiredQueryParam { expected: String },
76
77    #[error("invalid header value for key `{key}`: expected `{expected}`, found `{found}`")]
78    InvalidHeader {
79        key: String,
80        expected: String,
81        found: String,
82    },
83    #[error("missing required header: `{expected}`")]
84    MissingHeader { expected: String },
85    #[error("Header `{forbidden}` was forbidden but found: `{found}`")]
86    ForbiddenHeader { forbidden: String, found: String },
87    #[error(
88        "body did not match. left=expected, right=actual\n{comparison:?} \n == hint:\n{hint}."
89    )]
90    BodyDidNotMatch {
91        // the comparison includes colorized escapes. PrettyString ensures that even during
92        // debug printing, these appear
93        comparison: PrettyString,
94        hint: String,
95    },
96    #[error("Expected body to be valid {expected} but instead: {found}")]
97    InvalidBodyFormat { expected: String, found: String },
98}
99
100/// Check that the protocol test succeeded & print the pretty error
101/// if it did not
102///
103/// The primary motivation is making multiline debug output
104/// readable & using the cleaner Display implementation
105#[track_caller]
106pub fn assert_ok(inp: Result<(), ProtocolTestFailure>) {
107    match inp {
108        Ok(_) => (),
109        Err(e) => {
110            eprintln!("{}", e);
111            panic!("Protocol test failed");
112        }
113    }
114}
115
116#[derive(Eq, PartialEq, Hash)]
117struct QueryParam<'a> {
118    key: &'a str,
119    value: Option<&'a str>,
120}
121
122impl<'a> QueryParam<'a> {
123    fn parse(s: &'a str) -> Self {
124        let mut parsed = s.split('=');
125        QueryParam {
126            key: parsed.next().unwrap(),
127            value: parsed.next(),
128        }
129    }
130}
131
132fn extract_params(uri: &str) -> HashSet<&str> {
133    let query = uri.rsplit_once('?').map(|s| s.1).unwrap_or_default();
134    query.split('&').collect()
135}
136
137#[track_caller]
138pub fn assert_uris_match(left: impl AsRef<str>, right: impl AsRef<str>) {
139    let left = left.as_ref();
140    let right = right.as_ref();
141    if left == right {
142        return;
143    }
144    assert_eq!(
145        extract_params(left),
146        extract_params(right),
147        "Query parameters did not match. left: {}, right: {}",
148        left,
149        right
150    );
151    let left: Uri = left.parse().expect("left is not a valid URI");
152    let right: Uri = right.parse().expect("left is not a valid URI");
153    assert_eq!(left.authority(), right.authority());
154    assert_eq!(left.scheme(), right.scheme());
155    assert_eq!(left.path(), right.path());
156}
157
158pub fn validate_query_string(
159    request: &HttpRequest,
160    expected_params: &[&str],
161) -> Result<(), ProtocolTestFailure> {
162    let actual_params = extract_params(request.uri());
163    for param in expected_params {
164        if !actual_params.contains(param) {
165            return Err(ProtocolTestFailure::MissingQueryParam {
166                expected: param.to_string(),
167                found: actual_params.iter().map(|s| s.to_string()).collect(),
168            });
169        }
170    }
171    Ok(())
172}
173
174pub fn forbid_query_params(
175    request: &HttpRequest,
176    forbid_params: &[&str],
177) -> Result<(), ProtocolTestFailure> {
178    let actual_params: HashSet<QueryParam<'_>> = extract_params(request.uri())
179        .iter()
180        .map(|param| QueryParam::parse(param))
181        .collect();
182    let actual_keys: HashSet<&str> = actual_params.iter().map(|param| param.key).collect();
183    for param in forbid_params {
184        let parsed = QueryParam::parse(param);
185        // If the forbidden param is k=v, then forbid this key-value pair
186        if actual_params.contains(&parsed) {
187            return Err(ProtocolTestFailure::ForbiddenQueryParam {
188                expected: param.to_string(),
189            });
190        }
191        // If the assertion is only about a key, then check keys
192        if parsed.value.is_none() && actual_keys.contains(parsed.key) {
193            return Err(ProtocolTestFailure::ForbiddenQueryParam {
194                expected: param.to_string(),
195            });
196        }
197    }
198    Ok(())
199}
200
201pub fn require_query_params(
202    request: &HttpRequest,
203    require_keys: &[&str],
204) -> Result<(), ProtocolTestFailure> {
205    let actual_keys: HashSet<&str> = extract_params(request.uri())
206        .iter()
207        .map(|param| QueryParam::parse(param).key)
208        .collect();
209    for key in require_keys {
210        if !actual_keys.contains(*key) {
211            return Err(ProtocolTestFailure::RequiredQueryParam {
212                expected: key.to_string(),
213            });
214        }
215    }
216    Ok(())
217}
218
219mod sealed {
220    pub trait GetNormalizedHeader {
221        fn get_header(&self, key: &str) -> Option<String>;
222    }
223}
224
225impl<'a> GetNormalizedHeader for &'a Headers {
226    fn get_header(&self, key: &str) -> Option<String> {
227        if !self.contains_key(key) {
228            None
229        } else {
230            Some(self.get_all(key).collect::<Vec<_>>().join(", "))
231        }
232    }
233}
234
235impl<'a> GetNormalizedHeader for &'a HeaderMap {
236    fn get_header(&self, key: &str) -> Option<String> {
237        if !self.contains_key(key) {
238            None
239        } else {
240            Some(
241                self.get_all(key)
242                    .iter()
243                    .map(|value| std::str::from_utf8(value.as_bytes()).expect("invalid utf-8"))
244                    .collect::<Vec<_>>()
245                    .join(", "),
246            )
247        }
248    }
249}
250
251pub fn validate_headers<'a>(
252    actual_headers: impl GetNormalizedHeader,
253    expected_headers: impl IntoIterator<Item = (impl AsRef<str> + 'a, impl AsRef<str> + 'a)>,
254) -> Result<(), ProtocolTestFailure> {
255    for (key, expected_value) in expected_headers {
256        let key = key.as_ref();
257        let expected_value = expected_value.as_ref();
258        match actual_headers.get_header(key) {
259            None => {
260                return Err(ProtocolTestFailure::MissingHeader {
261                    expected: key.to_string(),
262                })
263            }
264            Some(actual_value) if actual_value != *expected_value => {
265                return Err(ProtocolTestFailure::InvalidHeader {
266                    key: key.to_string(),
267                    expected: expected_value.to_string(),
268                    found: actual_value,
269                })
270            }
271            _ => (),
272        }
273    }
274    Ok(())
275}
276
277pub fn forbid_headers(
278    headers: impl GetNormalizedHeader,
279    forbidden_headers: &[&str],
280) -> Result<(), ProtocolTestFailure> {
281    for key in forbidden_headers {
282        // Protocol tests store header lists as comma-delimited
283        if let Some(value) = headers.get_header(key) {
284            return Err(ProtocolTestFailure::ForbiddenHeader {
285                forbidden: key.to_string(),
286                found: format!("{}: {}", key, value),
287            });
288        }
289    }
290    Ok(())
291}
292
293pub fn require_headers(
294    headers: impl GetNormalizedHeader,
295    required_headers: &[&str],
296) -> Result<(), ProtocolTestFailure> {
297    for key in required_headers {
298        // Protocol tests store header lists as comma-delimited
299        if headers.get_header(key).is_none() {
300            return Err(ProtocolTestFailure::MissingHeader {
301                expected: key.to_string(),
302            });
303        }
304    }
305    Ok(())
306}
307
308#[derive(Clone)]
309pub enum MediaType {
310    /// JSON media types are deserialized and compared
311    Json,
312    /// XML media types are normalized and compared
313    Xml,
314    /// CBOR media types are decoded from base64 to binary and compared
315    Cbor,
316    /// For x-www-form-urlencoded, do some map order comparison shenanigans
317    UrlEncodedForm,
318    /// Other media types are compared literally
319    Other(String),
320}
321
322impl<T: AsRef<str>> From<T> for MediaType {
323    fn from(inp: T) -> Self {
324        match inp.as_ref() {
325            "application/json" => MediaType::Json,
326            "application/x-amz-json-1.1" => MediaType::Json,
327            "application/xml" => MediaType::Xml,
328            "application/cbor" => MediaType::Cbor,
329            "application/x-www-form-urlencoded" => MediaType::UrlEncodedForm,
330            other => MediaType::Other(other.to_string()),
331        }
332    }
333}
334
335pub fn validate_body<T: AsRef<[u8]> + Debug>(
336    actual_body: T,
337    expected_body: &str,
338    media_type: MediaType,
339) -> Result<(), ProtocolTestFailure> {
340    let body_str = std::str::from_utf8(actual_body.as_ref());
341    match (media_type, body_str) {
342        (MediaType::Json, Ok(actual_body)) => try_json_eq(expected_body, actual_body),
343        (MediaType::Json, Err(_)) => Err(ProtocolTestFailure::InvalidBodyFormat {
344            expected: "json".to_owned(),
345            found: "input was not valid UTF-8".to_owned(),
346        }),
347        (MediaType::Xml, Ok(actual_body)) => try_xml_equivalent(actual_body, expected_body),
348        (MediaType::Xml, Err(_)) => Err(ProtocolTestFailure::InvalidBodyFormat {
349            expected: "XML".to_owned(),
350            found: "input was not valid UTF-8".to_owned(),
351        }),
352        (MediaType::UrlEncodedForm, Ok(actual_body)) => {
353            try_url_encoded_form_equivalent(expected_body, actual_body)
354        }
355        (MediaType::UrlEncodedForm, Err(_)) => Err(ProtocolTestFailure::InvalidBodyFormat {
356            expected: "x-www-form-urlencoded".to_owned(),
357            found: "input was not valid UTF-8".to_owned(),
358        }),
359        (MediaType::Cbor, _) => try_cbor_eq(actual_body, expected_body),
360        (MediaType::Other(media_type), Ok(actual_body)) => {
361            if actual_body != expected_body {
362                Err(ProtocolTestFailure::BodyDidNotMatch {
363                    comparison: pretty_comparison(expected_body, actual_body),
364                    hint: format!("media type: {}", media_type),
365                })
366            } else {
367                Ok(())
368            }
369        }
370        // It's not clear from the Smithy spec exactly how a binary / base64 encoded body is supposed
371        // to work. Defer implementation for now until an actual test exists.
372        (MediaType::Other(_), Err(_)) => {
373            unimplemented!("binary/non-utf8 formats not yet supported")
374        }
375    }
376}
377
378#[derive(Eq, PartialEq)]
379struct PrettyStr<'a>(&'a str);
380impl Debug for PrettyStr<'_> {
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        f.write_str(self.0)
383    }
384}
385
386#[derive(Eq, PartialEq)]
387pub struct PrettyString(String);
388impl Debug for PrettyString {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        f.write_str(&self.0)
391    }
392}
393
394fn pretty_comparison(expected: &str, actual: &str) -> PrettyString {
395    PrettyString(format!(
396        "{}",
397        Comparison::new(&PrettyStr(expected), &PrettyStr(actual))
398    ))
399}
400
401fn try_json_eq(expected: &str, actual: &str) -> Result<(), ProtocolTestFailure> {
402    let expected_json: serde_json::Value =
403        serde_json::from_str(expected).expect("expected value must be valid JSON");
404    let actual_json: serde_json::Value =
405        serde_json::from_str(actual).map_err(|e| ProtocolTestFailure::InvalidBodyFormat {
406            expected: "json".to_owned(),
407            found: e.to_string() + actual,
408        })?;
409    match assert_json_eq_no_panic(&actual_json, &expected_json) {
410        Ok(()) => Ok(()),
411        Err(message) => Err(ProtocolTestFailure::BodyDidNotMatch {
412            comparison: pretty_comparison(expected, actual),
413            hint: message,
414        }),
415    }
416}
417
418/// Compares two `ciborium::value::Value` instances for semantic equality.
419///
420/// This function recursively compares two CBOR values, correctly handling arrays and maps
421/// according to the CBOR specification. Arrays are compared element-wise in order,
422/// while maps are compared without considering the order of key-value pairs.
423fn cbor_values_equal(
424    a: &ciborium::value::Value,
425    b: &ciborium::value::Value,
426) -> Result<bool, ProtocolTestFailure> {
427    match (a, b) {
428        (ciborium::value::Value::Array(a_array), ciborium::value::Value::Array(b_array)) => {
429            // Both arrays should be equal in size.
430            if a_array.len() != b_array.len() {
431                return Ok(false);
432            }
433            // Compare arrays element-wise.
434            for (a_elem, b_elem) in a_array.iter().zip(b_array.iter()) {
435                if !cbor_values_equal(a_elem, b_elem)? {
436                    return Ok(false);
437                }
438            }
439            Ok(true)
440        }
441
442        // Convert `ciborium::value::Value::Map` to a `HashMap`, and then compare the values of
443        // each key in `a` with those in `b`.
444        (ciborium::value::Value::Map(a_map), ciborium::value::Value::Map(b_map)) => {
445            if a_map.len() != b_map.len() {
446                return Ok(false);
447            }
448
449            let b_hashmap = ciborium_map_to_hashmap(b_map)?;
450            // Each key in `a` should exist in `b`, and the values should match.
451            for a_key_value in a_map.iter() {
452                let (a_key, a_value) = get_text_key_value(a_key_value)?;
453                match b_hashmap.get(a_key) {
454                    Some(b_value) => {
455                        if !cbor_values_equal(a_value, b_value)? {
456                            return Ok(false);
457                        }
458                    }
459                    None => return Ok(false),
460                }
461            }
462            Ok(true)
463        }
464
465        (ciborium::value::Value::Float(a_float), ciborium::value::Value::Float(b_float)) => {
466            Ok(a_float == b_float || (a_float.is_nan() && b_float.is_nan()))
467        }
468
469        _ => Ok(a == b),
470    }
471}
472
473/// Converts a `ciborium::value::Value::Map` into a `HashMap<&String, &ciborium::value::Value>`.
474///
475/// CBOR maps (`Value::Map`) are internally represented as vectors of key-value pairs,
476/// and direct comparison is affected by the order of these pairs.
477/// Since the CBOR specification treats maps as unordered collections,
478/// this function transforms the vector into a `HashMap`, for order-independent comparisons
479/// between maps.
480fn ciborium_map_to_hashmap(
481    cbor_map: &[(ciborium::value::Value, ciborium::value::Value)],
482) -> Result<std::collections::HashMap<&String, &ciborium::value::Value>, ProtocolTestFailure> {
483    cbor_map.iter().map(get_text_key_value).collect()
484}
485
486/// Extracts a string key and its associated value from a CBOR key-value pair.
487/// Returns a `ProtocolTestFailure::InvalidBodyFormat` error if the key is not a text value.
488fn get_text_key_value(
489    (key, value): &(ciborium::value::Value, ciborium::value::Value),
490) -> Result<(&String, &ciborium::value::Value), ProtocolTestFailure> {
491    match key {
492        ciborium::value::Value::Text(key_str) => Ok((key_str, value)),
493        _ => Err(ProtocolTestFailure::InvalidBodyFormat {
494            expected: "a text key as map entry".to_string(),
495            found: format!("{:?}", key),
496        }),
497    }
498}
499
500fn try_cbor_eq<T: AsRef<[u8]> + Debug>(
501    actual_body: T,
502    expected_body: &str,
503) -> Result<(), ProtocolTestFailure> {
504    let decoded = base64_simd::STANDARD
505        .decode_to_vec(expected_body)
506        .expect("smithy protocol test `body` property is not properly base64 encoded");
507    let expected_cbor_value: ciborium::value::Value =
508        ciborium::de::from_reader(decoded.as_slice()).expect("expected value must be valid CBOR");
509    let actual_cbor_value: ciborium::value::Value = ciborium::de::from_reader(actual_body.as_ref())
510        .map_err(|e| ProtocolTestFailure::InvalidBodyFormat {
511            expected: "cbor".to_owned(),
512            found: format!("{} {:?}", e, actual_body),
513        })?;
514    let actual_body_base64 = base64_simd::STANDARD.encode_to_string(&actual_body);
515
516    if !cbor_values_equal(&expected_cbor_value, &actual_cbor_value)? {
517        let expected_body_annotated_hex: String = cbor_diag::parse_bytes(&decoded)
518            .expect("smithy protocol test `body` property is not valid CBOR")
519            .to_hex();
520        let expected_body_diag: String = cbor_diag::parse_bytes(&decoded)
521            .expect("smithy protocol test `body` property is not valid CBOR")
522            .to_diag_pretty();
523        let actual_body_annotated_hex: String = cbor_diag::parse_bytes(&actual_body)
524            .expect("actual body is not valid CBOR")
525            .to_hex();
526        let actual_body_diag: String = cbor_diag::parse_bytes(&actual_body)
527            .expect("actual body is not valid CBOR")
528            .to_diag_pretty();
529
530        Err(ProtocolTestFailure::BodyDidNotMatch {
531            comparison: PrettyString(format!(
532                "{}",
533                Comparison::new(&expected_cbor_value, &actual_cbor_value)
534            )),
535            // The last newline is important because the panic message ends with a `.`
536            hint: format!(
537                "expected body in diagnostic format:
538{}
539actual body in diagnostic format:
540{}
541expected body in annotated hex:
542{}
543actual body in annotated hex:
544{}
545actual body in base64 (useful to update the protocol test):
546{}
547",
548                expected_body_diag,
549                actual_body_diag,
550                expected_body_annotated_hex,
551                actual_body_annotated_hex,
552                actual_body_base64,
553            ),
554        })
555    } else {
556        Ok(())
557    }
558}
559
560pub fn decode_body_data(body: &[u8], media_type: MediaType) -> Cow<'_, [u8]> {
561    match media_type {
562        MediaType::Cbor => Cow::Owned(
563            base64_simd::STANDARD
564                .decode_to_vec(body)
565                .expect("smithy protocol test `body` property is not properly base64 encoded"),
566        ),
567        _ => Cow::Borrowed(body),
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use crate::{
574        forbid_headers, forbid_query_params, require_headers, require_query_params, validate_body,
575        validate_headers, validate_query_string, FloatEquals, MediaType, ProtocolTestFailure,
576    };
577    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
578    use aws_smithy_runtime_api::http::Headers;
579
580    fn make_request(uri: &str) -> HttpRequest {
581        let mut req = HttpRequest::empty();
582        req.set_uri(uri).unwrap();
583        req
584    }
585
586    #[test]
587    fn test_validate_empty_query_string() {
588        let request = HttpRequest::empty();
589        validate_query_string(&request, &[]).expect("no required params should pass");
590        validate_query_string(&request, &["a"]).expect_err("no params provided");
591    }
592
593    #[test]
594    fn test_validate_query_string() {
595        let request = make_request("/foo?a=b&c&d=efg&hello=a%20b");
596        validate_query_string(&request, &["a=b"]).expect("a=b is in the query string");
597        validate_query_string(&request, &["c", "a=b"])
598            .expect("both params are in the query string");
599        validate_query_string(&request, &["a=b", "c", "d=efg", "hello=a%20b"])
600            .expect("all params are in the query string");
601        validate_query_string(&request, &[]).expect("no required params should pass");
602
603        validate_query_string(&request, &["a"]).expect_err("no parameter should match");
604        validate_query_string(&request, &["a=bc"]).expect_err("no parameter should match");
605        validate_query_string(&request, &["a=bc"]).expect_err("no parameter should match");
606        validate_query_string(&request, &["hell=a%20"]).expect_err("no parameter should match");
607    }
608
609    #[test]
610    fn test_forbid_query_param() {
611        let request = make_request("/foo?a=b&c&d=efg&hello=a%20b");
612        forbid_query_params(&request, &["a"]).expect_err("a is a query param");
613        forbid_query_params(&request, &["not_included"]).expect("query param not included");
614        forbid_query_params(&request, &["a=b"]).expect_err("if there is an `=`, match against KV");
615        forbid_query_params(&request, &["c"]).expect_err("c is a query param");
616        forbid_query_params(&request, &["a=c"]).expect("there is no a=c query param set");
617    }
618
619    #[test]
620    fn test_require_query_param() {
621        let request = make_request("/foo?a=b&c&d=efg&hello=a%20b");
622        require_query_params(&request, &["a"]).expect("a is a query param");
623        require_query_params(&request, &["not_included"]).expect_err("query param not included");
624        require_query_params(&request, &["a=b"]).expect_err("should be matching against keys");
625        require_query_params(&request, &["c"]).expect("c is a query param");
626    }
627
628    #[test]
629    fn test_validate_headers() {
630        let mut headers = Headers::new();
631        headers.append("x-foo", "foo");
632        headers.append("x-foo-list", "foo");
633        headers.append("x-foo-list", "bar");
634        headers.append("x-inline", "inline, other");
635
636        validate_headers(&headers, [("X-Foo", "foo")]).expect("header present");
637        validate_headers(&headers, [("X-Foo", "Foo")]).expect_err("case sensitive");
638        validate_headers(&headers, [("x-foo-list", "foo, bar")]).expect("list concat");
639        validate_headers(&headers, [("X-Foo-List", "foo")])
640            .expect_err("all list members must be specified");
641        validate_headers(&headers, [("X-Inline", "inline, other")])
642            .expect("inline header lists also work");
643        assert_eq!(
644            validate_headers(&headers, [("missing", "value")]),
645            Err(ProtocolTestFailure::MissingHeader {
646                expected: "missing".to_owned()
647            })
648        );
649    }
650
651    #[test]
652    fn test_forbidden_headers() {
653        let mut headers = Headers::new();
654        headers.append("x-foo", "foo");
655        assert_eq!(
656            forbid_headers(&headers, &["X-Foo"]).expect_err("should be error"),
657            ProtocolTestFailure::ForbiddenHeader {
658                forbidden: "X-Foo".to_string(),
659                found: "X-Foo: foo".to_string()
660            }
661        );
662        forbid_headers(&headers, &["X-Bar"]).expect("header not present");
663    }
664
665    #[test]
666    fn test_required_headers() {
667        let mut headers = Headers::new();
668        headers.append("x-foo", "foo");
669        require_headers(&headers, &["X-Foo"]).expect("header present");
670        require_headers(&headers, &["X-Bar"]).expect_err("header not present");
671    }
672
673    #[test]
674    fn test_validate_json_body() {
675        let expected = r#"{"abc": 5 }"#;
676        let actual = r#"   {"abc":   5 }"#;
677        validate_body(actual, expected, MediaType::Json).expect("inputs matched as JSON");
678
679        let expected = r#"{"abc": 5 }"#;
680        let actual = r#"   {"abc":   6 }"#;
681        validate_body(actual, expected, MediaType::Json).expect_err("bodies do not match");
682    }
683
684    #[test]
685    fn test_validate_cbor_body() {
686        let base64_encode = |v: &[u8]| base64_simd::STANDARD.encode_to_string(v);
687
688        // The following is the CBOR representation of `{"abc": 5 }`.
689        let actual = [0xbf, 0x63, 0x61, 0x62, 0x63, 0x05, 0xff];
690        // The following is the base64-encoded CBOR representation of `{"abc": 5 }` using a definite length map.
691        let expected_base64 = base64_encode(&[0xA1, 0x63, 0x61, 0x62, 0x63, 0x05]);
692
693        validate_body(actual, expected_base64.as_str(), MediaType::Cbor)
694            .expect("unexpected mismatch between CBOR definite and indefinite map encodings");
695
696        // The following is the CBOR representation of `{"a":1, "b":2}`.
697        let actual = [0xBF, 0x61, 0x61, 0x01, 0x61, 0x62, 0x02, 0xFF];
698        // The following is the base64-encoded CBOR representation of `{"b":2, "a":1}`.
699        let expected_base64 = base64_encode(&[0xBF, 0x61, 0x62, 0x02, 0x61, 0x61, 0x01, 0xFF]);
700        validate_body(actual, expected_base64.as_str(), MediaType::Cbor)
701            .expect("different ordering in CBOR decoded maps do not match");
702
703        // The following is the CBOR representation of `{"a":[1,2,{"b":3, "c":4}]}`.
704        let actual = [
705            0xBF, 0x61, 0x61, 0x9F, 0x01, 0x02, 0xBF, 0x61, 0x62, 0x03, 0x61, 0x63, 0x04, 0xFF,
706            0xFF, 0xFF,
707        ];
708        // The following is the base64-encoded CBOR representation of `{"a":[1,2,{"c":4, "b":3}]}`.
709        let expected_base64 = base64_encode(&[
710            0xBF, 0x61, 0x61, 0x9F, 0x01, 0x02, 0xBF, 0x61, 0x63, 0x04, 0x61, 0x62, 0x03, 0xFF,
711            0xFF, 0xFF,
712        ]);
713        validate_body(actual, expected_base64.as_str(), MediaType::Cbor)
714            .expect("different ordering in CBOR decoded maps do not match");
715
716        // The following is the CBOR representation of `{"a":[1,2]}`.
717        let actual = [0xBF, 0x61, 0x61, 0x9F, 0x01, 0x02, 0xFF, 0xFF];
718        // The following is the CBOR representation of `{"a":[2,1]}`.
719        let expected_base64 = base64_encode(&[0xBF, 0x61, 0x61, 0x9F, 0x02, 0x01, 0xFF, 0xFF]);
720        validate_body(actual, expected_base64.as_str(), MediaType::Cbor)
721            .expect_err("arrays in CBOR should follow strict ordering");
722    }
723
724    #[test]
725    fn test_validate_xml_body() {
726        let expected = r#"<a>
727        hello123
728        </a>"#;
729        let actual = "<a>hello123</a>";
730        validate_body(actual, expected, MediaType::Xml).expect("inputs match as XML");
731        let expected = r#"<a>
732        hello123
733        </a>"#;
734        let actual = "<a>hello124</a>";
735        validate_body(actual, expected, MediaType::Xml).expect_err("inputs are different");
736    }
737
738    #[test]
739    fn test_validate_non_json_body() {
740        let expected = r#"asdf"#;
741        let actual = r#"asdf "#;
742        validate_body(actual, expected, MediaType::from("something/else"))
743            .expect_err("bodies do not match");
744
745        validate_body(expected, expected, MediaType::from("something/else"))
746            .expect("inputs matched exactly")
747    }
748
749    #[test]
750    fn test_validate_headers_http0x() {
751        let request = http::Request::builder().header("a", "b").body(()).unwrap();
752        validate_headers(request.headers(), [("a", "b")]).unwrap()
753    }
754
755    #[test]
756    fn test_float_equals() {
757        let a = f64::NAN;
758        let b = f64::NAN;
759        assert_ne!(a, b);
760        assert!(a.float_equals(&b));
761        assert!(!a.float_equals(&5_f64));
762
763        assert!(5.0.float_equals(&5.0));
764        assert!(!5.0.float_equals(&5.1));
765
766        assert!(f64::INFINITY.float_equals(&f64::INFINITY));
767        assert!(!f64::INFINITY.float_equals(&f64::NEG_INFINITY));
768        assert!(f64::NEG_INFINITY.float_equals(&f64::NEG_INFINITY));
769    }
770}