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