1#![cfg_attr(docsrs, feature(doc_cfg))]
8#![warn(
10 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
31pub 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 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#[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 #[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 actual_params.contains(&parsed) {
197 return Err(ProtocolTestFailure::ForbiddenQueryParam {
198 expected: param.to_string(),
199 });
200 }
201 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#[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#[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 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 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,
342 Xml,
344 Cbor,
346 UrlEncodedForm,
348 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 (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
449fn 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 if a_array.len() != b_array.len() {
462 return Ok(false);
463 }
464 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 (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 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
504fn 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
517fn 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 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 let actual = [0xbf, 0x63, 0x61, 0x62, 0x63, 0x05, 0xff];
716 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 let actual = [0xBF, 0x61, 0x61, 0x01, 0x61, 0x62, 0x02, 0xFF];
724 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 let actual = [
731 0xBF, 0x61, 0x61, 0x9F, 0x01, 0x02, 0xBF, 0x61, 0x62, 0x03, 0x61, 0x63, 0x04, 0xFF,
732 0xFF, 0xFF,
733 ];
734 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 let actual = [0xBF, 0x61, 0x61, 0x9F, 0x01, 0x02, 0xFF, 0xFF];
744 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}