Skip to main content

apif_assert/
engine.rs

1// Assertion engine using embedded jaq and operators fallback
2
3use anyhow::Result;
4use serde_json::Value;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::sync::{LazyLock, Mutex};
8
9// Plugin imports
10use crate::registry::AssertionTiming;
11
12// Jaq imports
13use 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
16// Operators module
17use super::operators;
18
19/// Assertion result
20#[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
61/// Assertion engine
62pub struct AssertionEngine {
63    plugin_registry: Arc<dyn crate::registry::PluginRegistry>,
64}
65
66type JaqFilter = jaq_core::Filter<data::JustLut<JaqVal>>;
67
68/// Thread-safe cache for compiled JQ filters.
69/// Uses `Mutex` instead of `thread_local!` + `RefCell` to be safe with
70/// tokio's work-stealing runtime where futures can migrate across threads.
71static JAQ_FILTER_CACHE: LazyLock<Mutex<HashMap<String, Arc<JaqFilter>>>> =
72    LazyLock::new(|| Mutex::new(HashMap::new()));
73
74impl AssertionEngine {
75    /// Create a new assertion engine with default plugins
76    pub fn new() -> Self {
77        Self {
78            plugin_registry: Arc::new(crate::registry::NoopPluginRegistry),
79        }
80    }
81
82    /// Create a new assertion engine with a custom plugin registry
83    pub fn with_registry(registry: Arc<dyn crate::registry::PluginRegistry>) -> Self {
84        Self {
85            plugin_registry: registry,
86        }
87    }
88
89    /// Evaluate a single assertion
90    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        // 1. Try AST-based operator engine
111        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                // AST could not parse it — fall through to JQ
122                self.evaluate_jaq(trimmed, response)
123            }
124            Err(e) => Err(e),
125        }
126    }
127
128    /// Execute a JQ query and return the result(s)
129    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    /// Evaluate a JQ expression against `input`, returning the first output value.
228    /// Uses `JAQ_FILTER_CACHE` to avoid recompilation on repeated calls.
229    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    // Check if any assertion failed (re-exported wrapper)
242    #[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    // Get failed assertions (re-exported wrapper)
250    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    // Evaluate multiple assertions (re-exported wrapper)
258    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                // Try to fit in isize first (public API), then fall back to string parse
324                if let Some(i) = n.as_isize() {
325                    Value::Number(serde_json::Number::from(i))
326                } else {
327                    // BigInt too large for isize — avoid JSON parser on hot path
328                    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                // Dec is a string like "3.14" — parse as f64 directly, no JSON parser
340                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, // non-UTF8 bytes can't be represented in JSON
351            }
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            // Pass
440        } 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        // Correct value - should PASS
458        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        // Wrong value - should FAIL
473        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            // Pass
498        } 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            // Pass
513        } 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            // Pass
528        } 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            // Pass
543        } 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            // Pass
558        } 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            // Pass
573        } 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            // Pass
586        } 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            // Pass
599        } 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            // Pass
614        } 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            // Pass
629        } 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            // Pass
644        } 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            // Pass
659        } 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            // Pass
674        } 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            // Pass
689        } 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            // Pass
704        } 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        // This should fall through to JQ evaluation
715        let result = engine.evaluate("some_unknown_function()", &response, None, None);
716        // Should not panic, should return Error or handle gracefully
717        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        // Type cast should not affect evaluation result
797        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}