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