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