1use std::collections::BTreeMap;
84
85use regex::Regex;
86use serde::{Deserialize, Serialize};
87
88use crate::Response;
89
90mod json;
91
92#[cfg(test)]
93mod test_support;
94
95use json::check_json_path;
96
97#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
110#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
111#[serde(deny_unknown_fields)]
112pub struct Assertions {
113 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub status: Option<u16>,
121
122 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub status_in: Option<Vec<u16>>,
130
131 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
149 pub headers: BTreeMap<String, Option<String>>,
150
151 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub body_contains: Option<String>,
155
156 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub body_matches: Option<String>,
173
174 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub elapsed_ms_under: Option<u64>,
184
185 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
232 pub json: BTreeMap<String, serde_json::Value>,
233
234 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub not: Option<NotAssertions>,
241}
242
243#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
251#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
252#[serde(deny_unknown_fields)]
253pub struct NotAssertions {
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub status: Option<u16>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub status_in: Option<Vec<u16>>,
258 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
259 pub headers: BTreeMap<String, Option<String>>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub body_contains: Option<String>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub body_matches: Option<String>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub elapsed_ms_under: Option<u64>,
266 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
267 pub json: BTreeMap<String, serde_json::Value>,
268}
269
270impl NotAssertions {
271 fn is_empty(&self) -> bool {
272 self.status.is_none()
273 && self.status_in.is_none()
274 && self.headers.is_empty()
275 && self.body_contains.is_none()
276 && self.body_matches.is_none()
277 && self.elapsed_ms_under.is_none()
278 && self.json.is_empty()
279 }
280
281 fn fields(&self) -> Fields<'_> {
282 Fields {
283 status: self.status,
284 status_in: self.status_in.as_deref(),
285 headers: &self.headers,
286 body_contains: self.body_contains.as_deref(),
287 body_matches: self.body_matches.as_deref(),
288 elapsed_ms_under: self.elapsed_ms_under,
289 json: &self.json,
290 }
291 }
292}
293
294struct Fields<'a> {
299 status: Option<u16>,
300 status_in: Option<&'a [u16]>,
301 headers: &'a BTreeMap<String, Option<String>>,
302 body_contains: Option<&'a str>,
303 body_matches: Option<&'a str>,
304 elapsed_ms_under: Option<u64>,
305 json: &'a BTreeMap<String, serde_json::Value>,
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum AssertionKind {
317 Status,
318 StatusIn,
319 Header,
320 BodyContains,
321 BodyMatches,
322 ElapsedMsUnder,
323 JsonPath,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct AssertionResult {
334 pub kind: AssertionKind,
335
336 pub expectation: String,
338
339 pub failure: Option<String>,
344}
345
346impl AssertionResult {
347 pub fn passed(&self) -> bool {
348 self.failure.is_none()
349 }
350
351 fn pass(kind: AssertionKind, expectation: String) -> Self {
352 Self {
353 kind,
354 expectation,
355 failure: None,
356 }
357 }
358
359 fn fail(kind: AssertionKind, expectation: String, failure: String) -> Self {
360 Self {
361 kind,
362 expectation,
363 failure: Some(failure),
364 }
365 }
366}
367
368fn expectation_text(positive: String, negative: String, negate: bool) -> String {
374 if negate {
375 negative
376 } else {
377 positive
378 }
379}
380
381fn finish(
392 kind: AssertionKind,
393 holds: bool,
394 expectation: String,
395 negate: bool,
396 detail_if_false: String,
397 detail_if_true: String,
398) -> AssertionResult {
399 let failed = if negate { holds } else { !holds };
400 if !failed {
401 AssertionResult::pass(kind, expectation)
402 } else {
403 let detail = if negate {
404 detail_if_true
405 } else {
406 detail_if_false
407 };
408 AssertionResult::fail(kind, expectation, detail)
409 }
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, Default)]
420pub struct AssertionReport {
421 results: Vec<AssertionResult>,
422}
423
424impl AssertionReport {
425 pub fn results(&self) -> &[AssertionResult] {
426 &self.results
427 }
428
429 pub fn is_empty(&self) -> bool {
436 self.results.is_empty()
437 }
438
439 pub fn len(&self) -> usize {
440 self.results.len()
441 }
442
443 pub fn passed(&self) -> bool {
445 self.results.iter().all(AssertionResult::passed)
446 }
447
448 pub fn passed_count(&self) -> usize {
449 self.results.iter().filter(|result| result.passed()).count()
450 }
451
452 pub fn failed_count(&self) -> usize {
453 self.results.len() - self.passed_count()
454 }
455
456 pub fn failures(&self) -> impl Iterator<Item = &AssertionResult> {
458 self.results.iter().filter(|result| !result.passed())
459 }
460}
461
462impl Assertions {
463 pub fn is_empty(&self) -> bool {
466 self.status.is_none()
467 && self.status_in.is_none()
468 && self.headers.is_empty()
469 && self.body_contains.is_none()
470 && self.body_matches.is_none()
471 && self.elapsed_ms_under.is_none()
472 && self.json.is_empty()
473 && self.not.as_ref().is_none_or(NotAssertions::is_empty)
474 }
475
476 fn fields(&self) -> Fields<'_> {
477 Fields {
478 status: self.status,
479 status_in: self.status_in.as_deref(),
480 headers: &self.headers,
481 body_contains: self.body_contains.as_deref(),
482 body_matches: self.body_matches.as_deref(),
483 elapsed_ms_under: self.elapsed_ms_under,
484 json: &self.json,
485 }
486 }
487
488 pub fn evaluate(&self, response: &Response) -> AssertionReport {
494 let mut results = Vec::new();
495 push_checks(&mut results, self.fields(), false, response);
496 if let Some(not) = &self.not {
497 push_checks(&mut results, not.fields(), true, response);
498 }
499 AssertionReport { results }
500 }
501}
502
503fn push_checks(
508 results: &mut Vec<AssertionResult>,
509 fields: Fields<'_>,
510 negate: bool,
511 response: &Response,
512) {
513 if let Some(expected) = fields.status {
514 results.push(check_status(expected, response, negate));
515 }
516
517 if let Some(allowed) = fields.status_in {
518 results.push(check_status_in(allowed, response, negate));
519 }
520
521 for (name, expected) in fields.headers {
522 results.push(check_header(name, expected.as_deref(), response, negate));
523 }
524
525 if let Some(needle) = fields.body_contains {
526 results.push(check_body_contains(needle, response, negate));
527 }
528
529 if let Some(pattern) = fields.body_matches {
530 results.push(check_body_matches(pattern, response, negate));
531 }
532
533 if let Some(threshold_ms) = fields.elapsed_ms_under {
534 results.push(check_elapsed_ms_under(threshold_ms, response, negate));
535 }
536
537 if !fields.json.is_empty() {
538 let body = serde_json::from_str::<serde_json::Value>(&response.body);
543 for (path, expected) in fields.json {
544 results.push(check_json_path(
545 path,
546 expected,
547 body.as_ref(),
548 response,
549 negate,
550 ));
551 }
552 }
553}
554
555fn check_status(expected: u16, response: &Response, negate: bool) -> AssertionResult {
556 let holds = response.status == expected;
557 let expectation = expectation_text(
558 format!("status is {expected}"),
559 format!("status is not {expected}"),
560 negate,
561 );
562 let detail = format!("got {}", response.status);
563 finish(
564 AssertionKind::Status,
565 holds,
566 expectation,
567 negate,
568 detail.clone(),
569 detail,
570 )
571}
572
573fn check_status_in(allowed: &[u16], response: &Response, negate: bool) -> AssertionResult {
574 let holds = allowed.contains(&response.status);
575 let list = allowed
576 .iter()
577 .map(u16::to_string)
578 .collect::<Vec<_>>()
579 .join(", ");
580 let expectation = expectation_text(
581 format!("status is one of [{list}]"),
582 format!("status is not one of [{list}]"),
583 negate,
584 );
585 let detail = format!("got {}", response.status);
586 finish(
587 AssertionKind::StatusIn,
588 holds,
589 expectation,
590 negate,
591 detail.clone(),
592 detail,
593 )
594}
595
596fn check_header(
597 name: &str,
598 expected: Option<&str>,
599 response: &Response,
600 negate: bool,
601) -> AssertionResult {
602 let expectation = expectation_text(
603 match expected {
604 Some(value) => format!("header `{name}` is `{value}`"),
605 None => format!("header `{name}` is present"),
606 },
607 match expected {
608 Some(value) => format!("header `{name}` is not `{value}`"),
609 None => format!("header `{name}` is not present"),
610 },
611 negate,
612 );
613
614 let seen: Vec<&str> = response
617 .headers
618 .iter()
619 .filter(|(header, _)| header.eq_ignore_ascii_case(name))
620 .map(|(_, value)| value.as_str())
621 .collect();
622
623 let holds = match expected {
624 None => !seen.is_empty(),
625 Some(expected) => seen.contains(&expected),
626 };
627
628 let detail_if_false = if seen.is_empty() {
629 let present = response
633 .headers
634 .iter()
635 .map(|(header, _)| header.as_str())
636 .collect::<Vec<_>>()
637 .join(", ");
638 if present.is_empty() {
639 "the response carries no headers at all".to_string()
640 } else {
641 format!("not present (the response has: {present})")
642 }
643 } else {
644 format!(
645 "got {}",
646 seen.iter()
647 .map(|value| format!("`{value}`"))
648 .collect::<Vec<_>>()
649 .join(", ")
650 )
651 };
652 let detail_if_true = format!(
656 "got {}",
657 seen.iter()
658 .map(|value| format!("`{value}`"))
659 .collect::<Vec<_>>()
660 .join(", ")
661 );
662
663 finish(
664 AssertionKind::Header,
665 holds,
666 expectation,
667 negate,
668 detail_if_false,
669 detail_if_true,
670 )
671}
672
673fn check_body_contains(needle: &str, response: &Response, negate: bool) -> AssertionResult {
674 let holds = response.body.contains(needle);
675 let expectation = expectation_text(
676 format!("body contains `{needle}`"),
677 format!("body does not contain `{needle}`"),
678 negate,
679 );
680 finish(
681 AssertionKind::BodyContains,
682 holds,
683 expectation,
684 negate,
685 format!("not found in the {}-byte body", response.body.len()),
686 format!("found in the {}-byte body", response.body.len()),
687 )
688}
689
690fn check_body_matches(pattern: &str, response: &Response, negate: bool) -> AssertionResult {
691 let expectation = expectation_text(
692 format!("body matches `{pattern}`"),
693 format!("body does not match `{pattern}`"),
694 negate,
695 );
696
697 let regex = match Regex::new(pattern) {
702 Ok(regex) => regex,
703 Err(err) => {
704 return AssertionResult::fail(
705 AssertionKind::BodyMatches,
706 expectation,
707 format!("not a valid regular expression: {err}"),
708 );
709 }
710 };
711
712 let holds = regex.is_match(&response.body);
713 finish(
714 AssertionKind::BodyMatches,
715 holds,
716 expectation,
717 negate,
718 format!("no match in the {}-byte body", response.body.len()),
719 format!("matched in the {}-byte body", response.body.len()),
720 )
721}
722
723fn check_elapsed_ms_under(threshold_ms: u64, response: &Response, negate: bool) -> AssertionResult {
724 let elapsed_ms = response.elapsed.as_millis();
725 let holds = elapsed_ms < u128::from(threshold_ms);
726 let expectation = expectation_text(
727 format!("elapsed time is under {threshold_ms}ms"),
728 format!("elapsed time is not under {threshold_ms}ms"),
729 negate,
730 );
731 let detail = format!("took {elapsed_ms}ms");
732 finish(
733 AssertionKind::ElapsedMsUnder,
734 holds,
735 expectation,
736 negate,
737 detail.clone(),
738 detail,
739 )
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745 use test_support::{assertions, json_response, only_failure, response};
746
747 #[test]
748 fn a_matching_status_passes() {
749 let report = assertions("status: 200").evaluate(&json_response());
750 assert!(report.passed(), "{report:?}");
751 assert_eq!(report.len(), 1);
752 assert_eq!(report.results()[0].expectation, "status is 200");
753 }
754
755 #[test]
756 fn a_different_status_fails_and_says_what_it_got() {
757 let report = assertions("status: 200").evaluate(&response(404, &[], ""));
758 assert!(!report.passed());
759 let failure = only_failure(&report);
760 assert_eq!(failure.kind, AssertionKind::Status);
761 assert_eq!(failure.expectation, "status is 200");
762 assert_eq!(failure.failure.as_deref(), Some("got 404"));
763 }
764
765 #[test]
766 fn a_header_value_match_passes_regardless_of_name_casing() {
767 let report =
770 assertions("headers:\n Content-Type: application/json\n").evaluate(&json_response());
771 assert!(report.passed(), "{report:?}");
772 }
773
774 #[test]
775 fn a_header_with_a_null_value_asserts_only_presence() {
776 let report = assertions("headers:\n content-type:\n").evaluate(&json_response());
777 assert!(report.passed(), "{report:?}");
778 assert_eq!(
779 report.results()[0].expectation,
780 "header `content-type` is present"
781 );
782 }
783
784 #[test]
785 fn a_missing_header_fails_and_lists_the_ones_that_are_there() {
786 let report = assertions("headers:\n x-request-id:\n").evaluate(&json_response());
787 let failure = only_failure(&report);
788 assert_eq!(failure.kind, AssertionKind::Header);
789 let detail = failure.failure.as_deref().unwrap();
790 assert!(detail.contains("not present"), "got {detail}");
791 assert!(detail.contains("content-type"), "got {detail}");
792 }
793
794 #[test]
795 fn a_header_with_the_wrong_value_fails_and_shows_the_value_it_found() {
796 let report = assertions("headers:\n content-type: text/html\n").evaluate(&json_response());
797 let failure = only_failure(&report);
798 assert_eq!(
799 failure.failure.as_deref(),
800 Some("got `application/json`"),
801 "the value seen is the whole point of the message"
802 );
803 }
804
805 #[test]
806 fn a_header_value_is_matched_exactly_not_by_prefix() {
807 let decorated = response(
811 200,
812 &[("content-type", "application/json; charset=utf-8")],
813 "",
814 );
815 let report =
816 assertions("headers:\n content-type: application/json\n").evaluate(&decorated);
817 assert!(!report.passed(), "a prefix must not count as a match");
818 }
819
820 #[test]
821 fn a_repeated_header_passes_if_any_value_matches() {
822 let repeated = response(200, &[("set-cookie", "a=1"), ("set-cookie", "b=2")], "");
823 let report = assertions("headers:\n set-cookie: b=2\n").evaluate(&repeated);
824 assert!(report.passed(), "{report:?}");
825
826 let report = assertions("headers:\n set-cookie: c=3\n").evaluate(&repeated);
827 let detail = only_failure(&report).failure.clone().unwrap();
828 assert_eq!(detail, "got `a=1`, `b=2`", "both values should be shown");
829 }
830
831 #[test]
832 fn body_contains_passes_on_a_substring_and_fails_otherwise() {
833 let response = response(200, &[], "the operation was a success");
834
835 let report = assertions("body_contains: success").evaluate(&response);
836 assert!(report.passed(), "{report:?}");
837
838 let report = assertions("body_contains: failure").evaluate(&response);
839 let failure = only_failure(&report);
840 assert_eq!(failure.kind, AssertionKind::BodyContains);
841 assert_eq!(failure.expectation, "body contains `failure`");
842 assert!(
843 failure.failure.as_deref().unwrap().contains("27-byte body"),
844 "got {failure:?}"
845 );
846 }
847
848 #[test]
849 fn body_contains_is_case_sensitive() {
850 let report = assertions("body_contains: SUCCESS").evaluate(&response(200, &[], "success"));
851 assert!(!report.passed(), "matching is on the bytes as they arrived");
852 }
853
854 #[test]
855 fn an_unknown_assertion_key_is_a_parse_error() {
856 let err = serde_yaml::from_str::<Assertions>("body_contain: success\n")
859 .expect_err("a typo must not be silently ignored");
860 assert!(err.to_string().contains("body_contain"), "got {err}");
861 }
862
863 #[test]
864 fn an_expected_json_value_with_no_json_equivalent_is_a_parse_error() {
865 let err = serde_yaml::from_str::<Assertions>("json:\n $.a:\n ? [x, y]\n : one\n")
870 .expect_err("a sequence key has no JSON equivalent");
871 assert!(!err.to_string().is_empty());
872 }
873
874 #[test]
875 fn a_scalar_key_in_an_expected_value_is_read_as_the_string_json_would_use() {
876 let assertions = assertions("json:\n $.a:\n 1: one\n");
880 assert_eq!(
881 assertions.json["$.a"],
882 serde_json::json!({"1": "one"}),
883 "a scalar key becomes its string form"
884 );
885 }
886
887 #[test]
890 fn status_in_passes_when_the_status_is_one_of_the_list() {
891 let report = assertions("status_in: [200, 201, 204]").evaluate(&response(201, &[], ""));
892 assert!(report.passed(), "{report:?}");
893 assert_eq!(
894 report.results()[0].expectation,
895 "status is one of [200, 201, 204]"
896 );
897 }
898
899 #[test]
900 fn status_in_fails_and_says_what_it_got_when_the_status_is_not_listed() {
901 let report = assertions("status_in: [200, 201, 204]").evaluate(&response(404, &[], ""));
902 let failure = only_failure(&report);
903 assert_eq!(failure.kind, AssertionKind::StatusIn);
904 assert_eq!(failure.failure.as_deref(), Some("got 404"));
905 }
906
907 #[test]
910 fn body_matches_passes_on_a_regex_match_and_fails_otherwise() {
911 let body = response(200, &[], "request id: 4471");
912
913 let report = assertions(r"body_matches: 'id:\s*\d+'").evaluate(&body);
914 assert!(report.passed(), "{report:?}");
915
916 let report = assertions(r"body_matches: 'id:\s*[a-z]+'").evaluate(&body);
917 let failure = only_failure(&report);
918 assert_eq!(failure.kind, AssertionKind::BodyMatches);
919 assert!(
920 failure.failure.as_deref().unwrap().contains("16-byte body"),
921 "{failure:?}"
922 );
923 }
924
925 #[test]
926 fn an_invalid_regex_is_a_failed_assertion_not_a_panic() {
927 let report = assertions("body_matches: '['").evaluate(&response(200, &[], "anything"));
928 let failure = only_failure(&report);
929 assert!(
930 failure
931 .failure
932 .as_deref()
933 .unwrap()
934 .contains("not a valid regular expression"),
935 "{failure:?}"
936 );
937 }
938
939 #[test]
942 fn elapsed_ms_under_passes_when_faster_than_the_threshold() {
943 let mut fast = response(200, &[], "");
944 fast.elapsed = std::time::Duration::from_millis(10);
945 let report = assertions("elapsed_ms_under: 1000").evaluate(&fast);
946 assert!(report.passed(), "{report:?}");
947 assert_eq!(
948 report.results()[0].expectation,
949 "elapsed time is under 1000ms"
950 );
951 }
952
953 #[test]
954 fn elapsed_ms_under_fails_when_slower_than_the_threshold() {
955 let mut slow = response(200, &[], "");
956 slow.elapsed = std::time::Duration::from_millis(1500);
957 let report = assertions("elapsed_ms_under: 1000").evaluate(&slow);
958 let failure = only_failure(&report);
959 assert_eq!(failure.kind, AssertionKind::ElapsedMsUnder);
960 assert_eq!(failure.failure.as_deref(), Some("took 1500ms"));
961 }
962
963 #[test]
964 fn every_assertion_is_reported_not_just_the_first_failure() {
965 let report = assertions(
968 "\
969status: 201
970headers:
971 content-type: application/json
972 x-missing: whatever
973body_contains: ada
974json:
975 $.user.id: 42
976 $.user.name: grace
977",
978 )
979 .evaluate(&json_response());
980
981 assert_eq!(report.len(), 6);
982 assert_eq!(report.passed_count(), 3);
983 assert_eq!(report.failed_count(), 3);
984 assert!(!report.passed());
985
986 let expectations: Vec<&str> = report
988 .results()
989 .iter()
990 .map(|result| result.expectation.as_str())
991 .collect();
992 assert_eq!(
993 expectations,
994 vec![
995 "status is 201",
996 "header `content-type` is `application/json`",
997 "header `x-missing` is `whatever`",
998 "body contains `ada`",
999 "`$.user.id` is 42",
1000 "`$.user.name` is \"grace\"",
1001 ]
1002 );
1003
1004 let failed: Vec<&str> = report
1005 .failures()
1006 .map(|result| result.expectation.as_str())
1007 .collect();
1008 assert_eq!(
1009 failed,
1010 vec![
1011 "status is 201",
1012 "header `x-missing` is `whatever`",
1013 "`$.user.name` is \"grace\"",
1014 ],
1015 "the passing assertions must not hide the failing ones, or vice versa"
1016 );
1017 }
1018
1019 #[test]
1020 fn an_empty_report_is_vacuously_passing_and_knows_it_is_empty() {
1021 let report = Assertions::default().evaluate(&json_response());
1022 assert!(report.is_empty(), "nothing was asserted");
1023 assert!(report.passed(), "and so nothing failed");
1024 assert_eq!(report.failed_count(), 0);
1025 }
1026
1027 #[test]
1030 fn not_status_passes_when_the_status_differs_and_fails_when_it_matches() {
1031 let report = assertions("not:\n status: 404\n").evaluate(&response(200, &[], ""));
1032 assert!(report.passed(), "{report:?}");
1033 assert_eq!(report.results()[0].expectation, "status is not 404");
1034
1035 let report = assertions("not:\n status: 404\n").evaluate(&response(404, &[], ""));
1036 let failure = only_failure(&report);
1037 assert_eq!(failure.kind, AssertionKind::Status);
1038 assert_eq!(failure.expectation, "status is not 404");
1039 assert_eq!(failure.failure.as_deref(), Some("got 404"));
1040 }
1041
1042 #[test]
1043 fn not_body_contains_passes_when_absent_and_fails_when_present() {
1044 let ok = response(200, &[], "all good");
1045 let report = assertions("not:\n body_contains: error\n").evaluate(&ok);
1046 assert!(report.passed(), "{report:?}");
1047 assert_eq!(
1048 report.results()[0].expectation,
1049 "body does not contain `error`"
1050 );
1051
1052 let bad = response(200, &[], "an error occurred");
1053 let report = assertions("not:\n body_contains: error\n").evaluate(&bad);
1054 let failure = only_failure(&report);
1055 assert_eq!(failure.expectation, "body does not contain `error`");
1056 assert!(
1057 failure.failure.as_deref().unwrap().contains("found in the"),
1058 "{failure:?}"
1059 );
1060 }
1061
1062 #[test]
1063 fn not_json_path_negates_equality() {
1064 let report = assertions("not:\n json:\n $.user.id: 7\n").evaluate(&json_response());
1065 assert!(report.passed(), "{report:?}");
1066 assert_eq!(report.results()[0].expectation, "`$.user.id` is not 7");
1067
1068 let report = assertions("not:\n json:\n $.user.id: 42\n").evaluate(&json_response());
1069 let failure = only_failure(&report);
1070 assert_eq!(failure.expectation, "`$.user.id` is not 42");
1071 assert_eq!(failure.failure.as_deref(), Some("got 42"));
1072 }
1073
1074 #[test]
1075 fn a_hard_error_under_not_still_fails_rather_than_being_negated_into_a_pass() {
1076 let report = assertions("not:\n json:\n $.user.name: {greater_than: 5}\n")
1080 .evaluate(&json_response());
1081 assert!(
1082 !report.passed(),
1083 "a type mismatch must still fail under `not:`"
1084 );
1085 let failure = only_failure(&report);
1086 assert!(
1087 failure
1088 .failure
1089 .as_deref()
1090 .unwrap()
1091 .contains("is not a number"),
1092 "{failure:?}"
1093 );
1094 }
1095
1096 #[test]
1097 fn a_malformed_json_path_under_not_still_fails() {
1098 let report = assertions("not:\n json:\n '$.[': 1\n").evaluate(&json_response());
1099 assert!(!report.passed());
1100 let failure = only_failure(&report);
1101 assert!(
1102 failure
1103 .failure
1104 .as_deref()
1105 .unwrap()
1106 .contains("not a valid JSON path"),
1107 "{failure:?}"
1108 );
1109 }
1110
1111 #[test]
1112 fn not_and_the_plain_block_can_be_combined_and_both_are_reported() {
1113 let report = assertions(
1114 "\
1115status: 200
1116not:
1117 body_contains: error
1118",
1119 )
1120 .evaluate(&response(200, &[], "all good"));
1121 assert!(report.passed(), "{report:?}");
1122 assert_eq!(report.len(), 2);
1123 }
1124
1125 #[test]
1126 fn a_nested_not_inside_not_is_a_parse_error() {
1127 let err = serde_yaml::from_str::<Assertions>("not:\n not:\n status: 200\n")
1128 .expect_err("double negation is not part of the schema");
1129 assert!(err.to_string().contains("not"), "{err}");
1130 }
1131
1132 #[test]
1133 fn an_empty_not_block_counts_as_no_assertions() {
1134 let assertions = assertions("not: {}");
1135 assert!(assertions.is_empty());
1136 }
1137}