1use anyhow::Result;
4use serde_json::Value;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::sync::{LazyLock, Mutex};
8
9use crate::registry::AssertionTiming;
11
12use jaq_core::{Compiler, Ctx, Vars, data, load, unwrap_valr};
14use jaq_json::{Map as JaqMap, Num as JaqNum, Rc as JaqRc, Val as JaqVal};
15
16use super::operators;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum AssertionResult {
22 Pass,
23 Fail {
24 message: String,
25 expected: Option<String>,
26 actual: Option<String>,
27 },
28 Error(String),
29}
30
31impl AssertionResult {
32 pub fn fail(message: impl Into<String>) -> Self {
33 Self::Fail {
34 message: message.into(),
35 expected: None,
36 actual: None,
37 }
38 }
39
40 pub fn fail_with_diff(
41 message: impl Into<String>,
42 expected: impl Into<String>,
43 actual: impl Into<String>,
44 ) -> Self {
45 Self::Fail {
46 message: message.into(),
47 expected: Some(expected.into()),
48 actual: Some(actual.into()),
49 }
50 }
51
52 pub fn negate(self) -> Self {
53 match self {
54 Self::Pass => Self::fail("Negated assertion passed (expected false)"),
55 Self::Fail { .. } => Self::Pass,
56 Self::Error(e) => Self::Error(e),
57 }
58 }
59}
60
61pub struct AssertionEngine {
63 plugin_registry: Arc<dyn crate::registry::PluginRegistry>,
64}
65
66type JaqFilter = jaq_core::Filter<data::JustLut<JaqVal>>;
67
68static JAQ_FILTER_CACHE: LazyLock<Mutex<HashMap<String, Arc<JaqFilter>>>> =
72 LazyLock::new(|| Mutex::new(HashMap::new()));
73
74impl AssertionEngine {
75 pub fn new() -> Self {
77 Self {
78 plugin_registry: Arc::new(crate::registry::NoopPluginRegistry),
79 }
80 }
81
82 pub fn with_registry(registry: Arc<dyn crate::registry::PluginRegistry>) -> Self {
84 Self {
85 plugin_registry: registry,
86 }
87 }
88
89 pub fn evaluate(
91 &self,
92 assertion: &str,
93 response: &Value,
94 headers: Option<&HashMap<String, String>>,
95 trailers: Option<&HashMap<String, String>>,
96 ) -> Result<AssertionResult> {
97 self.evaluate_with_timing(assertion, response, headers, trailers, None)
98 }
99
100 pub fn evaluate_with_timing(
101 &self,
102 assertion: &str,
103 response: &Value,
104 headers: Option<&HashMap<String, String>>,
105 trailers: Option<&HashMap<String, String>>,
106 timing: Option<&AssertionTiming>,
107 ) -> Result<AssertionResult> {
108 let trimmed = assertion.trim();
109
110 match operators::evaluate_assertion(
112 &*self.plugin_registry,
113 trimmed,
114 response,
115 headers,
116 trailers,
117 timing,
118 ) {
119 Ok(Some(result)) => Ok(result),
120 Ok(None) => {
121 self.evaluate_jaq(trimmed, response)
123 }
124 Err(e) => Err(e),
125 }
126 }
127
128 pub fn query(&self, expr: &str, input: &Value) -> Result<Vec<Value>> {
130 let values = self.run_jaq(expr, input)?;
131 Ok(values.iter().map(jaq_to_json).collect())
132 }
133
134 fn evaluate_jaq(&self, expr: &str, response: &Value) -> Result<AssertionResult> {
135 let out = match self.run_jaq(expr, response) {
136 Ok(out) => out,
137 Err(e) => return Ok(AssertionResult::Error(format!("JQ Parse Error: {}", e))),
138 };
139
140 let mut passed = false;
141 let mut seen_false = false;
142
143 for val in out {
144 if matches!(val, JaqVal::Bool(true)) {
145 passed = true;
146 } else {
147 seen_false = true;
148 }
149 }
150
151 if seen_false {
152 return Ok(AssertionResult::fail(format!(
153 "JQ assertion evaluated to false: {}",
154 expr
155 )));
156 }
157
158 if passed {
159 Ok(AssertionResult::Pass)
160 } else {
161 Ok(AssertionResult::fail(format!(
162 "JQ assertion produced no output (falsey): {}",
163 expr
164 )))
165 }
166 }
167
168 fn run_jaq(&self, expr: &str, input: &Value) -> Result<Vec<JaqVal>> {
169 let filter = Self::get_or_compile_jaq_filter(expr)?;
170
171 let input = json_to_jaq(input);
172
173 let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
174 let out = filter.id.run((ctx, input)).map(unwrap_valr);
175
176 let mut values = Vec::new();
177 for item in out {
178 match item {
179 Ok(v) => values.push(v),
180 Err(e) => return Err(anyhow::anyhow!("JQ Runtime Error: {}", e)),
181 }
182 }
183
184 Ok(values)
185 }
186
187 fn get_or_compile_jaq_filter(expr: &str) -> Result<Arc<JaqFilter>> {
188 use jaq_core::defs as core_defs;
189 use jaq_core::funs as core_funs;
190
191 if let Some(cached) = JAQ_FILTER_CACHE
192 .lock()
193 .unwrap_or_else(|e| e.into_inner())
194 .get(expr)
195 .cloned()
196 {
197 return Ok(cached);
198 }
199
200 let arena = load::Arena::default();
201 let defs = core_defs().chain(jaq_std::defs()).chain(jaq_json::defs());
202 let funs = core_funs().chain(jaq_std::funs()).chain(jaq_json::funs());
203 let loader = load::Loader::new(defs);
204 let program = load::File {
205 code: expr,
206 path: (),
207 };
208
209 let modules = loader
210 .load(&arena, program)
211 .map_err(|errs| anyhow::anyhow!("Failed to parse JQ expression: {:?}", errs))?;
212
213 let filter = Compiler::default()
214 .with_funs(funs)
215 .compile(modules)
216 .map_err(|errs| anyhow::anyhow!("Failed to compile JQ expression: {:?}", errs))?;
217
218 let filter = Arc::new(filter);
219 JAQ_FILTER_CACHE
220 .lock()
221 .unwrap_or_else(|e| e.into_inner())
222 .insert(expr.to_string(), Arc::clone(&filter));
223
224 Ok(filter)
225 }
226
227 pub(super) fn eval_jaq_one(expr: &str, input: &Value) -> anyhow::Result<Value> {
230 let filter = Self::get_or_compile_jaq_filter(expr)?;
231 let jaq_input = json_to_jaq(input);
232 let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
233 let mut out = filter.id.run((ctx, jaq_input)).map(unwrap_valr);
234 if let Some(Ok(val)) = out.next() {
235 Ok(jaq_to_json(&val))
236 } else {
237 Err(anyhow::anyhow!("JQ produced no output for: {}", expr))
238 }
239 }
240
241 #[must_use]
243 pub fn has_failures(&self, results: &[AssertionResult]) -> bool {
244 results
245 .iter()
246 .any(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
247 }
248
249 pub fn get_failures<'a>(&self, results: &'a [AssertionResult]) -> Vec<&'a AssertionResult> {
251 results
252 .iter()
253 .filter(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
254 .collect()
255 }
256
257 pub fn evaluate_all(
259 &self,
260 assertions: &[String],
261 response: &serde_json::Value,
262 headers: Option<&HashMap<String, String>>,
263 trailers: Option<&HashMap<String, String>>,
264 ) -> Vec<AssertionResult> {
265 self.evaluate_all_with_timing(assertions, response, headers, trailers, None)
266 }
267
268 pub fn evaluate_all_with_timing(
269 &self,
270 assertions: &[String],
271 response: &serde_json::Value,
272 headers: Option<&HashMap<String, String>>,
273 trailers: Option<&HashMap<String, String>>,
274 timing: Option<&AssertionTiming>,
275 ) -> Vec<AssertionResult> {
276 assertions
277 .iter()
278 .map(|assertion| {
279 self.evaluate_with_timing(assertion, response, headers, trailers, timing)
280 .unwrap_or_else(|e| AssertionResult::Error(format!("Internal error: {}", e)))
281 })
282 .collect()
283 }
284}
285
286fn json_to_jaq(value: &Value) -> JaqVal {
287 match value {
288 Value::Null => JaqVal::Null,
289 Value::Bool(v) => JaqVal::Bool(*v),
290 Value::Number(n) => {
291 if let Some(i) = n.as_i64() {
292 JaqVal::Num(JaqNum::from_integral(i))
293 } else if let Some(u) = n.as_u64() {
294 JaqVal::Num(JaqNum::from_integral(u))
295 } else if let Some(f) = n.as_f64() {
296 JaqVal::Num(JaqNum::Float(f))
297 } else {
298 JaqVal::Null
299 }
300 }
301 Value::String(s) => JaqVal::utf8_str(s.clone()),
302 Value::Array(items) => JaqVal::Arr(JaqRc::new(items.iter().map(json_to_jaq).collect())),
303 Value::Object(obj) => {
304 let map: JaqMap = obj
305 .iter()
306 .map(|(k, v)| (JaqVal::utf8_str(k.clone()), json_to_jaq(v)))
307 .collect();
308 JaqVal::Obj(JaqRc::new(map))
309 }
310 }
311}
312
313fn jaq_to_json(value: &JaqVal) -> Value {
314 match value {
315 JaqVal::Null => Value::Null,
316 JaqVal::Bool(v) => Value::Bool(*v),
317 JaqVal::Num(n) => match n {
318 JaqNum::Int(v) => Value::Number(serde_json::Number::from(*v)),
319 JaqNum::Float(v) => serde_json::Number::from_f64(*v)
320 .map(Value::Number)
321 .unwrap_or(Value::Null),
322 JaqNum::BigInt(bi) => {
323 if let Some(i) = n.as_isize() {
325 Value::Number(serde_json::Number::from(i))
326 } else {
327 let s = bi.to_string();
329 if let Ok(i) = s.parse::<i64>() {
330 Value::Number(serde_json::Number::from(i))
331 } else if let Ok(u) = s.parse::<u64>() {
332 Value::Number(serde_json::Number::from(u))
333 } else {
334 Value::Null
335 }
336 }
337 }
338 JaqNum::Dec(s) => {
339 s.parse::<f64>()
341 .ok()
342 .and_then(serde_json::Number::from_f64)
343 .map(Value::Number)
344 .unwrap_or(Value::Null)
345 }
346 },
347 JaqVal::TStr(s) | JaqVal::BStr(s) => {
348 match std::str::from_utf8(s.as_ref()) {
349 Ok(v) => Value::String(v.to_string()),
350 Err(_) => Value::Null, }
352 }
353 JaqVal::Arr(items) => Value::Array(items.iter().map(jaq_to_json).collect()),
354 JaqVal::Obj(obj) => {
355 let map: serde_json::Map<String, Value> = obj
356 .iter()
357 .filter_map(|(k, v)| {
358 let key = match k {
359 JaqVal::TStr(s) | JaqVal::BStr(s) => {
360 std::str::from_utf8(s.as_ref()).ok().map(str::to_owned)
361 }
362 _ => None,
363 }?;
364 Some((key, jaq_to_json(v)))
365 })
366 .collect();
367 Value::Object(map)
368 }
369 }
370}
371
372impl Default for AssertionEngine {
373 fn default() -> Self {
374 Self::new()
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use serde_json::json;
382
383 fn create_test_response() -> Value {
384 json!({
385 "id": 123,
386 "name": "test",
387 "email": "test@example.com",
388 "active": true,
389 "tags": ["a", "b", "c"],
390 "nested": {
391 "value": 42
392 }
393 })
394 }
395
396 #[test]
397 fn test_assertion_result_fail() {
398 let result = AssertionResult::fail("test message");
399 if let AssertionResult::Fail { message, .. } = result {
400 assert_eq!(message, "test message");
401 } else {
402 panic!("Expected Fail result");
403 }
404 }
405
406 #[test]
407 fn test_assertion_result_fail_with_diff() {
408 let result = AssertionResult::fail_with_diff("mismatch", "expected", "actual");
409 if let AssertionResult::Fail {
410 message,
411 expected,
412 actual,
413 } = result
414 {
415 assert_eq!(message, "mismatch");
416 assert_eq!(expected, Some("expected".to_string()));
417 assert_eq!(actual, Some("actual".to_string()));
418 } else {
419 panic!("Expected Fail result");
420 }
421 }
422
423 #[test]
424 fn test_assertion_result_debug() {
425 let result = AssertionResult::Pass;
426 let debug_str = format!("{:?}", result);
427 assert!(debug_str.contains("Pass"));
428 }
429
430 #[test]
431 fn test_evaluate_equality_operator() {
432 let engine = AssertionEngine::new();
433 let response = create_test_response();
434
435 let result = engine
436 .evaluate(".id == 123", &response, None, None)
437 .unwrap();
438 if let AssertionResult::Pass = result {
439 } else {
441 panic!("Expected Pass for equality check");
442 }
443 }
444
445 #[test]
446 fn test_evaluate_bracket_index_assertion() {
447 let engine = AssertionEngine::new();
448 let response = serde_json::json!({
449 "ipsToDecorations": {
450 "10.0.0.1": {
451 "decoration": "web-frontend",
452 "environment": "production"
453 }
454 }
455 });
456
457 let result1 = engine
459 .evaluate(
460 ".ipsToDecorations[\"10.0.0.1\"].environment == \"production\"",
461 &response,
462 None,
463 None,
464 )
465 .unwrap();
466 assert!(
467 matches!(result1, AssertionResult::Pass),
468 "Expected Pass for correct value, got: {:?}",
469 result1
470 );
471
472 let result2 = engine
474 .evaluate(
475 ".ipsToDecorations[\"10.0.0.1\"].environment == \"production1\"",
476 &response,
477 None,
478 None,
479 )
480 .unwrap();
481 assert!(
482 matches!(result2, AssertionResult::Fail { .. }),
483 "Expected Fail for wrong value, got: {:?}",
484 result2
485 );
486 }
487
488 #[test]
489 fn test_evaluate_equality_operator_fail() {
490 let engine = AssertionEngine::new();
491 let response = create_test_response();
492
493 let result = engine
494 .evaluate(".id == 456", &response, None, None)
495 .unwrap();
496 if let AssertionResult::Fail { .. } = result {
497 } else {
499 panic!("Expected Fail for equality check");
500 }
501 }
502
503 #[test]
504 fn test_evaluate_inequality_operator() {
505 let engine = AssertionEngine::new();
506 let response = create_test_response();
507
508 let result = engine
509 .evaluate(".id != 456", &response, None, None)
510 .unwrap();
511 if let AssertionResult::Pass = result {
512 } else {
514 panic!("Expected Pass for inequality check");
515 }
516 }
517
518 #[test]
519 fn test_evaluate_contains_operator() {
520 let engine = AssertionEngine::new();
521 let response = create_test_response();
522
523 let result = engine
524 .evaluate(".name contains \"test\"", &response, None, None)
525 .unwrap();
526 if let AssertionResult::Pass = result {
527 } else {
529 panic!("Expected Pass for contains check");
530 }
531 }
532
533 #[test]
534 fn test_evaluate_contains_operator_array() {
535 let engine = AssertionEngine::new();
536 let response = create_test_response();
537
538 let result = engine
539 .evaluate(".tags contains \"a\"", &response, None, None)
540 .unwrap();
541 if let AssertionResult::Pass = result {
542 } else {
544 panic!("Expected Pass for array contains check");
545 }
546 }
547
548 #[test]
549 fn test_evaluate_starts_with_operator() {
550 let engine = AssertionEngine::new();
551 let response = create_test_response();
552
553 let result = engine
554 .evaluate(".name startsWith \"te\"", &response, None, None)
555 .unwrap();
556 if let AssertionResult::Pass = result {
557 } else {
559 panic!("Expected Pass for startsWith check");
560 }
561 }
562
563 #[test]
564 fn test_evaluate_ends_with_operator() {
565 let engine = AssertionEngine::new();
566 let response = create_test_response();
567
568 let result = engine
569 .evaluate(".name endsWith \"st\"", &response, None, None)
570 .unwrap();
571 if let AssertionResult::Pass = result {
572 } else {
574 panic!("Expected Pass for endsWith check");
575 }
576 }
577
578 #[test]
579 fn test_evaluate_numeric_greater_than() {
580 let engine = AssertionEngine::new();
581 let response = create_test_response();
582
583 let result = engine.evaluate(".id > 100", &response, None, None).unwrap();
584 if let AssertionResult::Pass = result {
585 } else {
587 panic!("Expected Pass for greater than check");
588 }
589 }
590
591 #[test]
592 fn test_evaluate_numeric_less_than() {
593 let engine = AssertionEngine::new();
594 let response = create_test_response();
595
596 let result = engine.evaluate(".id < 200", &response, None, None).unwrap();
597 if let AssertionResult::Pass = result {
598 } else {
600 panic!("Expected Pass for less than check");
601 }
602 }
603
604 #[test]
605 fn test_evaluate_numeric_gte() {
606 let engine = AssertionEngine::new();
607 let response = create_test_response();
608
609 let result = engine
610 .evaluate(".id >= 123", &response, None, None)
611 .unwrap();
612 if let AssertionResult::Pass = result {
613 } else {
615 panic!("Expected Pass for gte check");
616 }
617 }
618
619 #[test]
620 fn test_evaluate_numeric_lte() {
621 let engine = AssertionEngine::new();
622 let response = create_test_response();
623
624 let result = engine
625 .evaluate(".id <= 123", &response, None, None)
626 .unwrap();
627 if let AssertionResult::Pass = result {
628 } else {
630 panic!("Expected Pass for lte check");
631 }
632 }
633
634 #[test]
635 fn test_evaluate_matches_regex() {
636 let engine = AssertionEngine::new();
637 let response = create_test_response();
638
639 let result = engine
640 .evaluate(".name matches \"^te.*t$\"", &response, None, None)
641 .unwrap();
642 if let AssertionResult::Pass = result {
643 } else {
645 panic!("Expected Pass for regex match");
646 }
647 }
648
649 #[test]
650 fn test_evaluate_matches_regex_fail() {
651 let engine = AssertionEngine::new();
652 let response = create_test_response();
653
654 let result = engine
655 .evaluate(".name matches \"^xyz\"", &response, None, None)
656 .unwrap();
657 if let AssertionResult::Fail { .. } = result {
658 } else {
660 panic!("Expected Fail for regex match");
661 }
662 }
663
664 #[test]
665 fn test_evaluate_nested_path() {
666 let engine = AssertionEngine::new();
667 let response = create_test_response();
668
669 let result = engine
670 .evaluate(".nested.value == 42", &response, None, None)
671 .unwrap();
672 if let AssertionResult::Pass = result {
673 } else {
675 panic!("Expected Pass for nested path check");
676 }
677 }
678
679 #[test]
680 fn test_evaluate_boolean_path() {
681 let engine = AssertionEngine::new();
682 let response = create_test_response();
683
684 let result = engine
685 .evaluate(".active == true", &response, None, None)
686 .unwrap();
687 if let AssertionResult::Pass = result {
688 } else {
690 panic!("Expected Pass for boolean check");
691 }
692 }
693
694 #[test]
695 fn test_evaluate_array_index() {
696 let engine = AssertionEngine::new();
697 let response = create_test_response();
698
699 let result = engine
700 .evaluate(".tags[0] == \"a\"", &response, None, None)
701 .unwrap();
702 if let AssertionResult::Pass = result {
703 } else {
705 panic!("Expected Pass for array index check");
706 }
707 }
708
709 #[test]
710 fn test_evaluate_unsupported_syntax() {
711 let engine = AssertionEngine::new();
712 let response = create_test_response();
713
714 let result = engine.evaluate("some_unknown_function()", &response, None, None);
716 assert!(result.is_ok());
718 }
719
720 #[test]
721 fn test_evaluate_all() {
722 let engine = AssertionEngine::new();
723 let response = create_test_response();
724
725 let assertions = vec![".id == 123".to_string(), ".name == \"test\"".to_string()];
726
727 let results = engine.evaluate_all(&assertions, &response, None, None);
728 assert_eq!(results.len(), 2);
729 assert!(results.iter().all(|r| matches!(r, AssertionResult::Pass)));
730 }
731
732 #[test]
733 fn test_evaluate_all_with_failure() {
734 let engine = AssertionEngine::new();
735 let response = create_test_response();
736
737 let assertions = vec![".id == 123".to_string(), ".id == 999".to_string()];
738
739 let results = engine.evaluate_all(&assertions, &response, None, None);
740 assert_eq!(results.len(), 2);
741 assert!(matches!(&results[0], AssertionResult::Pass));
742 assert!(matches!(&results[1], AssertionResult::Fail { .. }));
743 }
744
745 #[test]
746 fn test_evaluate_type_cast_number() {
747 let engine = AssertionEngine::new();
748 let response = json!({
749 "price": 42
750 });
751
752 let result = engine.evaluate(".price:number >= 0", &response, None, None);
753 assert!(
754 matches!(result, Ok(AssertionResult::Pass)),
755 "Expected Pass, got: {:?}",
756 result
757 );
758
759 let result = engine.evaluate(".price:number < 0", &response, None, None);
760 assert!(
761 matches!(result, Ok(AssertionResult::Fail { .. })),
762 "Expected Fail, got: {:?}",
763 result
764 );
765 }
766
767 #[test]
768 fn test_evaluate_type_cast_string() {
769 let engine = AssertionEngine::new();
770 let response = json!({
771 "name": "hello world"
772 });
773
774 let result = engine.evaluate(".name:string contains \"hello\"", &response, None, None);
775 assert!(
776 matches!(result, Ok(AssertionResult::Pass)),
777 "Expected Pass, got: {:?}",
778 result
779 );
780
781 let result = engine.evaluate(".name:string startsWith \"he\"", &response, None, None);
782 assert!(
783 matches!(result, Ok(AssertionResult::Pass)),
784 "Expected Pass, got: {:?}",
785 result
786 );
787 }
788
789 #[test]
790 fn test_evaluate_type_cast_is_noop() {
791 let engine = AssertionEngine::new();
792 let response = json!({
793 "value": 123
794 });
795
796 let without_cast = engine.evaluate(".value == 123", &response, None, None);
798 let with_cast = engine.evaluate(".value:number == 123", &response, None, None);
799 assert_eq!(
800 matches!(without_cast, Ok(AssertionResult::Pass)),
801 matches!(with_cast, Ok(AssertionResult::Pass)),
802 "Type cast should not change evaluation result"
803 );
804 }
805
806 #[test]
807 fn test_query_jq_simple() {
808 let engine = AssertionEngine::new();
809 let response = create_test_response();
810
811 let results = engine.query(".id", &response).unwrap();
812 assert_eq!(results.len(), 1);
813 assert_eq!(results[0], json!(123));
814 }
815
816 #[test]
817 fn test_query_jq_nested() {
818 let engine = AssertionEngine::new();
819 let response = create_test_response();
820
821 let results = engine.query(".nested.value", &response).unwrap();
822 assert_eq!(results.len(), 1);
823 assert_eq!(results[0], json!(42));
824 }
825
826 #[test]
827 fn test_query_jq_array() {
828 let engine = AssertionEngine::new();
829 let response = create_test_response();
830
831 let results = engine.query(".tags[]", &response).unwrap();
832 assert_eq!(results.len(), 3);
833 assert_eq!(results[0], json!("a"));
834 assert_eq!(results[1], json!("b"));
835 assert_eq!(results[2], json!("c"));
836 }
837
838 #[test]
839 fn test_query_jq_filter() {
840 let engine = AssertionEngine::new();
841 let response = json!([1, 2, 3, 4, 5]);
842
843 let results = engine.query(".[] | select(. > 3)", &response).unwrap();
844 assert_eq!(results.len(), 2);
845 assert_eq!(results[0], json!(4));
846 assert_eq!(results[1], json!(5));
847 }
848
849 #[test]
850 fn test_query_jq_length() {
851 let engine = AssertionEngine::new();
852 let response = create_test_response();
853
854 let results = engine.query(".tags | length", &response).unwrap();
855 assert_eq!(results.len(), 1);
856 assert_eq!(results[0], json!(3));
857 }
858
859 #[test]
860 fn test_query_invalid_expression() {
861 let engine = AssertionEngine::new();
862 let response = create_test_response();
863
864 let results = engine.query("invalid[[[", &response);
865 assert!(results.is_err());
866 }
867
868 #[test]
869 fn test_jaq_to_json_dec_number() {
870 let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("2.5".to_string())));
871 assert_eq!(jaq_to_json(&dec), json!(2.5));
872 }
873
874 #[test]
875 fn test_jaq_to_json_invalid_dec_number() {
876 let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("not-a-number".to_string())));
877 assert_eq!(jaq_to_json(&dec), Value::Null);
878 }
879
880 #[test]
881 fn test_json_to_jaq_null() {
882 let result = json_to_jaq(&json!(null));
883 assert!(matches!(result, JaqVal::Null));
884 }
885
886 #[test]
887 fn test_json_to_jaq_bool() {
888 let result = json_to_jaq(&json!(true));
889 assert!(matches!(result, JaqVal::Bool(true)));
890 }
891
892 #[test]
893 fn test_json_to_jaq_number_int() {
894 let result = json_to_jaq(&json!(42));
895 assert!(matches!(result, JaqVal::Num(JaqNum::Int(42))));
896 }
897
898 #[test]
899 fn test_json_to_jaq_number_float() {
900 let result = json_to_jaq(&json!(4.14));
901 assert!(matches!(result, JaqVal::Num(JaqNum::Float(f)) if (f - 4.14).abs() < 0.001));
902 }
903
904 #[test]
905 fn test_json_to_jaq_string() {
906 let result = json_to_jaq(&json!("hello"));
907 assert!(matches!(result, JaqVal::TStr(_)));
908 }
909
910 #[test]
911 fn test_json_to_jaq_array() {
912 let result = json_to_jaq(&json!([1, 2, 3]));
913 assert!(matches!(result, JaqVal::Arr(_)));
914 }
915
916 #[test]
917 fn test_json_to_jaq_object() {
918 let result = json_to_jaq(&json!({"key": "value"}));
919 assert!(matches!(result, JaqVal::Obj(_)));
920 }
921
922 #[test]
923 fn test_jaq_filter_cache_returns_same_arc() {
924 let expr = ".__cache_test_sentinel__";
925 let first = AssertionEngine::get_or_compile_jaq_filter(expr).unwrap();
926 let second = AssertionEngine::get_or_compile_jaq_filter(expr).unwrap();
927 assert!(Arc::ptr_eq(&first, &second));
928 }
929 #[test]
930 fn test_assertion_result_negate() {
931 let pass = AssertionResult::Pass;
932 assert!(matches!(pass.negate(), AssertionResult::Fail { .. }));
933
934 let fail = AssertionResult::fail("msg");
935 assert!(matches!(fail.negate(), AssertionResult::Pass));
936
937 let error = AssertionResult::Error("err".into());
938 assert!(matches!(error.negate(), AssertionResult::Error(_)));
939 }
940
941 #[test]
942 fn test_assertion_engine_get_failures() {
943 let engine = AssertionEngine::new();
944 let response = create_test_response();
945 let assertions = vec![".id == 123".to_string(), ".id == 999".to_string()];
946 let results = engine.evaluate_all(&assertions, &response, None, None);
947 let failures = engine.get_failures(&results);
948 assert_eq!(failures.len(), 1);
949 }
950
951 #[test]
952 fn test_assertion_engine_has_failures() {
953 let engine = AssertionEngine::new();
954 let response = create_test_response();
955 let result = engine.evaluate_all(&[".id == 999".to_string()], &response, None, None);
956 assert!(engine.has_failures(&result));
957 }
958
959 #[test]
960 fn test_assertion_engine_no_failures() {
961 let engine = AssertionEngine::new();
962 let response = create_test_response();
963 let result = engine.evaluate_all(&[".id == 123".to_string()], &response, None, None);
964 assert!(!engine.has_failures(&result));
965 }
966
967 #[test]
968 fn test_assertion_engine_default() {
969 let engine = AssertionEngine::default();
970 let response = create_test_response();
971 let result = engine
972 .evaluate(".id == 123", &response, None, None)
973 .unwrap();
974 assert!(matches!(result, AssertionResult::Pass));
975 }
976
977 #[test]
978 fn test_assertion_result_fail_with_diff_fields() {
979 let result = AssertionResult::fail_with_diff("mismatch", "{\"a\":1}", "{\"a\":2}");
980 match result {
981 AssertionResult::Fail {
982 message,
983 expected,
984 actual,
985 } => {
986 assert_eq!(message, "mismatch");
987 assert_eq!(expected.unwrap(), "{\"a\":1}");
988 assert_eq!(actual.unwrap(), "{\"a\":2}");
989 }
990 _ => panic!("Expected Fail"),
991 }
992 }
993
994 #[test]
995 fn test_evaluate_url_scheme_parse_only() {
996 use apif_ast::assertion_ast::{AssertionExpr, assertion_to_string, parse_assertion};
997 let expr = parse_assertion("@url.scheme(\"https://example.com\") == \"https\"");
998 assert!(
999 !matches!(&expr, AssertionExpr::Raw(_)),
1000 "Expression should be parsed, not Raw: {:?}",
1001 expr
1002 );
1003 let s = assertion_to_string(&expr);
1004 assert_eq!(
1005 s, "@url.scheme(\"https://example.com\") == \"https\"",
1006 "Roundtrip failed"
1007 );
1008 }
1009}