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 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
32pub 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 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#[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 actual_params.contains(&parsed) {
185 return Err(ProtocolTestFailure::ForbiddenQueryParam {
186 expected: param.to_string(),
187 });
188 }
189 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 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 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,
310 Xml,
312 Cbor,
314 UrlEncodedForm,
316 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 (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
417fn 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 if a_array.len() != b_array.len() {
430 return Ok(false);
431 }
432 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 (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 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
472fn 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
485fn 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 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 let actual = [0xbf, 0x63, 0x61, 0x62, 0x63, 0x05, 0xff];
684 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 let actual = [0xBF, 0x61, 0x61, 0x01, 0x61, 0x62, 0x02, 0xFF];
692 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 let actual = [
699 0xBF, 0x61, 0x61, 0x9F, 0x01, 0x02, 0xBF, 0x61, 0x62, 0x03, 0x61, 0x63, 0x04, 0xFF,
700 0xFF, 0xFF,
701 ];
702 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 let actual = [0xBF, 0x61, 0x61, 0x9F, 0x01, 0x02, 0xFF, 0xFF];
712 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}