1use anyhow::Result;
4use serde_json::Value;
5use std::cell::RefCell;
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::sync::{LazyLock, Mutex};
9
10use crate::registry::AssertionTiming;
11
12use jaq_core::{
13 Bind, Compiler, Ctx, Cv, Error as JaqError, Vars, data, load, native::bome, unwrap_valr,
14};
15use jaq_json::{Map as JaqMap, Num as JaqNum, Rc as JaqRc, Val as JaqVal};
16
17use super::operators;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum AssertionResult {
21 Pass,
22 Fail {
23 message: String,
24 expected: Option<String>,
25 actual: Option<String>,
26 },
27 Error(String),
28}
29
30impl AssertionResult {
31 pub fn fail(message: impl Into<String>) -> Self {
32 Self::Fail {
33 message: message.into(),
34 expected: None,
35 actual: None,
36 }
37 }
38
39 pub fn fail_with_diff(
40 message: impl Into<String>,
41 expected: impl Into<String>,
42 actual: impl Into<String>,
43 ) -> Self {
44 Self::Fail {
45 message: message.into(),
46 expected: Some(expected.into()),
47 actual: Some(actual.into()),
48 }
49 }
50
51 pub fn negate(self) -> Self {
52 match self {
53 Self::Pass => Self::fail("Negated assertion passed (expected false)"),
54 Self::Fail { .. } => Self::Pass,
55 Self::Error(e) => Self::Error(e),
56 }
57 }
58}
59
60pub struct AssertionEngine {
61 plugin_registry: Arc<dyn crate::registry::PluginRegistry>,
62}
63
64type JaqFilter = jaq_core::Filter<data::JustLut<JaqVal>>;
65
66static JAQ_FILTER_CACHE: LazyLock<Mutex<HashMap<String, Arc<JaqFilter>>>> =
70 LazyLock::new(|| Mutex::new(HashMap::new()));
71
72const JAQ_CONTEXT_ONLY_PLUGINS: &[&str] = &[
77 "header",
78 "has_header",
79 "trailer",
80 "has_trailer",
81 "elapsed_ms",
82 "total_elapsed_ms",
83 "env",
84 "scope.message_count",
85 "scope.index",
86 "scope_message_count",
87 "scope_index",
88];
89
90thread_local! {
91 static JAQ_PLUGIN_REGISTRY: RefCell<Option<Arc<dyn crate::registry::PluginRegistry>>> =
96 const { RefCell::new(None) };
97}
98
99struct PluginRegistryGuard(Option<Arc<dyn crate::registry::PluginRegistry>>);
102
103impl PluginRegistryGuard {
104 fn set(registry: Arc<dyn crate::registry::PluginRegistry>) -> Self {
105 let prev = JAQ_PLUGIN_REGISTRY.with(|cell| cell.borrow_mut().replace(registry));
106 Self(prev)
107 }
108}
109
110impl Drop for PluginRegistryGuard {
111 fn drop(&mut self) {
112 let prev = self.0.take();
113 JAQ_PLUGIN_REGISTRY.with(|cell| *cell.borrow_mut() = prev);
114 }
115}
116
117fn dispatch_jaq_plugin(name: &str, args: &[Value]) -> std::result::Result<JaqVal, String> {
122 let registry = JAQ_PLUGIN_REGISTRY.with(|cell| cell.borrow().clone());
123 let registry =
124 registry.ok_or_else(|| format!("plugin '@{}' is not available in this context", name))?;
125 let plugin = registry
126 .get_plugin(name)
127 .ok_or_else(|| format!("unknown plugin '@{}' in jq expression", name))?;
128
129 let null = Value::Null;
130 let ctx = crate::registry::PluginContext::new(&null);
131 match plugin
132 .execute(args, &ctx)
133 .map_err(|e| format!("plugin '@{}' error: {}", name, e))?
134 {
135 crate::registry::PluginResult::Value(v) => Ok(json_to_jaq(&v)),
136 crate::registry::PluginResult::Assertion(AssertionResult::Pass) => Ok(JaqVal::Bool(true)),
137 crate::registry::PluginResult::Assertion(AssertionResult::Fail { .. }) => {
138 Ok(JaqVal::Bool(false))
139 }
140 crate::registry::PluginResult::Assertion(AssertionResult::Error(e)) => {
141 Err(format!("plugin '@{}' error: {}", name, e))
142 }
143 }
144}
145
146fn jaq_plugin_fun<D>() -> jaq_core::native::Fun<D>
155where
156 D: for<'a> jaq_core::DataT<V<'a> = JaqVal>,
157{
158 jaq_core::native::run((
159 "__plugin",
160 Box::new([Bind::Fun(()), Bind::Fun(())]),
161 |mut cv: Cv<D>| {
162 let input = cv.1.clone();
163 let (args_id, args_ctx) = cv.0.pop_fun();
165 let (name_id, name_ctx) = cv.0.pop_fun();
166
167 let name = match name_id
168 .run((name_ctx, input.clone()))
169 .map(unwrap_valr)
170 .next()
171 {
172 Some(Ok(v)) => v,
173 Some(Err(e)) => return bome(Err(e)),
174 None => return bome(Err(JaqError::str("plugin call produced no name"))),
175 };
176 let name = match jaq_to_json(&name) {
177 Value::String(s) => s,
178 other => {
179 return bome(Err(JaqError::str(format!(
180 "plugin name must be a string, got {}",
181 other
182 ))));
183 }
184 };
185
186 let args_val = match args_id.run((args_ctx, input)).map(unwrap_valr).next() {
187 Some(Ok(v)) => v,
188 Some(Err(e)) => return bome(Err(e)),
189 None => {
190 return bome(Err(JaqError::str(format!(
191 "plugin '@{}' produced no arguments",
192 name
193 ))));
194 }
195 };
196 let args_json = match jaq_to_json(&args_val) {
197 Value::Array(items) => items,
198 other => vec![other],
199 };
200
201 match dispatch_jaq_plugin(&name, &args_json) {
202 Ok(v) => bome(Ok(v)),
203 Err(e) => bome(Err(JaqError::str(e))),
204 }
205 },
206 ))
207}
208
209fn rewrite_plugin_calls(expr: &str) -> Result<String> {
214 let bytes = expr.as_bytes();
215 let mut out = String::with_capacity(expr.len() + 16);
216 let mut i = 0;
217 while i < bytes.len() {
218 let b = bytes[i];
219 match b {
220 b'"' | b'\'' => {
221 let start = i;
223 i += 1;
224 while i < bytes.len() {
225 if bytes[i] == b'\\' {
226 i += 2;
227 continue;
228 }
229 let end = bytes[i] == b;
230 i += 1;
231 if end {
232 break;
233 }
234 }
235 out.push_str(&expr[start..i.min(bytes.len())]);
236 }
237 b'@' => {
238 let name_start = i + 1;
239 let mut j = name_start;
240 while j < bytes.len()
241 && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_' || bytes[j] == b'.')
242 {
243 j += 1;
244 }
245 if j > name_start && j < bytes.len() && bytes[j] == b'(' {
246 let name = &expr[name_start..j];
247 if JAQ_CONTEXT_ONLY_PLUGINS.contains(&name) {
248 return Err(anyhow::anyhow!(
249 "@{} is not available in jq expressions: it needs response \
250 header/trailer/timing/env context; use it as a standalone assertion",
251 name
252 ));
253 }
254 let close = find_matching_paren(bytes, j).ok_or_else(|| {
255 anyhow::anyhow!("unbalanced parentheses in plugin call @{}", name)
256 })?;
257 let inner = rewrite_plugin_calls(&expr[j + 1..close])?;
258 out.push_str("__plugin(\"");
259 out.push_str(name);
260 out.push_str("\"; [");
261 out.push_str(&inner);
262 out.push_str("])");
263 i = close + 1;
264 } else {
265 out.push('@');
266 i += 1;
267 }
268 }
269 _ => {
270 let len = utf8_char_len(b);
271 out.push_str(&expr[i..(i + len).min(bytes.len())]);
272 i += len;
273 }
274 }
275 }
276 Ok(out)
277}
278
279fn utf8_char_len(b: u8) -> usize {
281 if b < 0x80 {
282 1
283 } else if b >> 5 == 0b110 {
284 2
285 } else if b >> 4 == 0b1110 {
286 3
287 } else if b >> 3 == 0b11110 {
288 4
289 } else {
290 1
291 }
292}
293
294fn find_matching_paren(bytes: &[u8], open: usize) -> Option<usize> {
297 let mut depth = 0usize;
298 let mut i = open;
299 let mut in_string: Option<u8> = None;
300 while i < bytes.len() {
301 let b = bytes[i];
302 match in_string {
303 Some(q) => {
304 if b == b'\\' {
305 i += 2;
306 continue;
307 }
308 if b == q {
309 in_string = None;
310 }
311 }
312 None => match b {
313 b'"' | b'\'' => in_string = Some(b),
314 b'(' | b'[' | b'{' => depth += 1,
315 b')' | b']' | b'}' => {
316 depth -= 1;
317 if depth == 0 {
318 return Some(i);
319 }
320 }
321 _ => {}
322 },
323 }
324 i += 1;
325 }
326 None
327}
328
329impl AssertionEngine {
330 pub fn new() -> Self {
332 Self {
333 plugin_registry: Arc::new(crate::registry::NoopPluginRegistry),
334 }
335 }
336
337 pub fn with_registry(registry: Arc<dyn crate::registry::PluginRegistry>) -> Self {
339 Self {
340 plugin_registry: registry,
341 }
342 }
343
344 pub fn evaluate(
346 &self,
347 assertion: &str,
348 response: &Value,
349 headers: Option<&HashMap<String, String>>,
350 trailers: Option<&HashMap<String, String>>,
351 ) -> Result<AssertionResult> {
352 self.evaluate_with_timing(
353 assertion,
354 response,
355 headers,
356 trailers,
357 None,
358 &HashMap::new(),
359 )
360 }
361
362 pub fn evaluate_with_timing(
363 &self,
364 assertion: &str,
365 response: &Value,
366 headers: Option<&HashMap<String, String>>,
367 trailers: Option<&HashMap<String, String>>,
368 timing: Option<&AssertionTiming>,
369 variables: &HashMap<String, Value>,
370 ) -> Result<AssertionResult> {
371 let trimmed = assertion.trim();
372
373 match operators::evaluate_assertion(
374 &*self.plugin_registry,
375 trimmed,
376 response,
377 headers,
378 trailers,
379 timing,
380 variables,
381 ) {
382 Ok(Some(result)) => Ok(result),
383 Ok(None) => {
384 if let Some(pos) = find_lone_equals(trimmed) {
389 return Ok(AssertionResult::fail(format!(
390 "Assertion uses `=` at position {} — did you mean `==`? \
391 (`=` is not a comparison operator): {}",
392 pos, trimmed
393 )));
394 }
395 self.evaluate_jaq(trimmed, response)
396 }
397 Err(e) => Err(e),
398 }
399 }
400
401 pub fn query(&self, expr: &str, input: &Value) -> Result<Vec<Value>> {
403 let values = self.run_jaq(expr, input)?;
404 Ok(values.iter().map(jaq_to_json).collect())
405 }
406
407 fn evaluate_jaq(&self, expr: &str, response: &Value) -> Result<AssertionResult> {
408 let out = match self.run_jaq(expr, response) {
409 Ok(out) => out,
410 Err(e) => return Ok(AssertionResult::Error(format!("JQ Parse Error: {}", e))),
411 };
412
413 for val in &out {
416 if matches!(val, JaqVal::Bool(false) | JaqVal::Null) {
417 let rendered = serde_json::to_string(&jaq_to_json(val))
418 .unwrap_or_else(|_| "<unprintable>".to_string());
419 return Ok(AssertionResult::fail(format!(
420 "JQ assertion evaluated to falsy value {}: {}",
421 rendered, expr
422 )));
423 }
424 }
425
426 if out.is_empty() {
427 Ok(AssertionResult::fail(format!(
428 "JQ assertion produced no output (falsey): {}",
429 expr
430 )))
431 } else {
432 Ok(AssertionResult::Pass)
433 }
434 }
435
436 fn run_jaq(&self, expr: &str, input: &Value) -> Result<Vec<JaqVal>> {
437 let rewritten = rewrite_plugin_calls(expr)?;
440 let filter = Self::get_or_compile_jaq_filter(&rewritten)?;
441
442 let input = json_to_jaq(input);
443
444 let _registry_guard = PluginRegistryGuard::set(self.plugin_registry.clone());
446
447 let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
448 let out = filter.id.run((ctx, input)).map(unwrap_valr);
449
450 let mut values = Vec::new();
451 for item in out {
452 match item {
453 Ok(v) => values.push(v),
454 Err(e) => return Err(anyhow::anyhow!("JQ Runtime Error: {}", e)),
455 }
456 }
457
458 Ok(values)
459 }
460
461 fn get_or_compile_jaq_filter(expr: &str) -> Result<Arc<JaqFilter>> {
462 use jaq_core::defs as core_defs;
463 use jaq_core::funs as core_funs;
464
465 if let Some(cached) = JAQ_FILTER_CACHE
466 .lock()
467 .unwrap_or_else(|e| e.into_inner())
468 .get(expr)
469 .cloned()
470 {
471 return Ok(cached);
472 }
473
474 let arena = load::Arena::default();
475 let defs = core_defs().chain(jaq_std::defs()).chain(jaq_json::defs());
476 let funs = core_funs()
477 .chain(jaq_std::funs())
478 .chain(jaq_json::funs())
479 .chain(std::iter::once(jaq_plugin_fun()));
480 let loader = load::Loader::new(defs);
481 let program = load::File {
482 code: expr,
483 path: (),
484 };
485
486 let modules = loader
487 .load(&arena, program)
488 .map_err(|errs| anyhow::anyhow!("Failed to parse JQ expression: {:?}", errs))?;
489
490 let filter = Compiler::default()
491 .with_funs(funs)
492 .compile(modules)
493 .map_err(|errs| anyhow::anyhow!("Failed to compile JQ expression: {:?}", errs))?;
494
495 let filter = Arc::new(filter);
496 JAQ_FILTER_CACHE
497 .lock()
498 .unwrap_or_else(|e| e.into_inner())
499 .insert(expr.to_string(), Arc::clone(&filter));
500
501 Ok(filter)
502 }
503
504 pub(super) fn eval_jaq_one(expr: &str, input: &Value) -> anyhow::Result<Value> {
507 let filter = Self::get_or_compile_jaq_filter(expr)?;
508 let jaq_input = json_to_jaq(input);
509 let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
510 let mut out = filter.id.run((ctx, jaq_input)).map(unwrap_valr);
511 if let Some(Ok(val)) = out.next() {
512 Ok(jaq_to_json(&val))
513 } else {
514 Err(anyhow::anyhow!("JQ produced no output for: {}", expr))
515 }
516 }
517
518 #[must_use]
520 pub fn has_failures(&self, results: &[AssertionResult]) -> bool {
521 results
522 .iter()
523 .any(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
524 }
525
526 pub fn get_failures<'a>(&self, results: &'a [AssertionResult]) -> Vec<&'a AssertionResult> {
528 results
529 .iter()
530 .filter(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
531 .collect()
532 }
533
534 pub fn evaluate_all(
536 &self,
537 assertions: &[String],
538 response: &serde_json::Value,
539 headers: Option<&HashMap<String, String>>,
540 trailers: Option<&HashMap<String, String>>,
541 ) -> Vec<AssertionResult> {
542 self.evaluate_all_with_timing(
543 assertions,
544 response,
545 headers,
546 trailers,
547 None,
548 &HashMap::new(),
549 )
550 }
551
552 pub fn evaluate_all_with_timing(
553 &self,
554 assertions: &[String],
555 response: &serde_json::Value,
556 headers: Option<&HashMap<String, String>>,
557 trailers: Option<&HashMap<String, String>>,
558 timing: Option<&AssertionTiming>,
559 variables: &HashMap<String, Value>,
560 ) -> Vec<AssertionResult> {
561 assertions
562 .iter()
563 .map(|assertion| {
564 self.evaluate_with_timing(assertion, response, headers, trailers, timing, variables)
565 .unwrap_or_else(|e| AssertionResult::Error(format!("Internal error: {}", e)))
566 })
567 .collect()
568 }
569}
570
571fn json_to_jaq(value: &Value) -> JaqVal {
572 match value {
573 Value::Null => JaqVal::Null,
574 Value::Bool(v) => JaqVal::Bool(*v),
575 Value::Number(n) => {
576 if let Some(i) = n.as_i64() {
577 JaqVal::Num(JaqNum::from_integral(i))
578 } else if let Some(u) = n.as_u64() {
579 JaqVal::Num(JaqNum::from_integral(u))
580 } else if let Some(f) = n.as_f64() {
581 JaqVal::Num(JaqNum::Float(f))
582 } else {
583 JaqVal::Null
584 }
585 }
586 Value::String(s) => JaqVal::utf8_str(s.clone()),
587 Value::Array(items) => JaqVal::Arr(JaqRc::new(items.iter().map(json_to_jaq).collect())),
588 Value::Object(obj) => {
589 let map: JaqMap = obj
590 .iter()
591 .map(|(k, v)| (JaqVal::utf8_str(k.clone()), json_to_jaq(v)))
592 .collect();
593 JaqVal::Obj(JaqRc::new(map))
594 }
595 }
596}
597
598fn jaq_to_json(value: &JaqVal) -> Value {
599 match value {
600 JaqVal::Null => Value::Null,
601 JaqVal::Bool(v) => Value::Bool(*v),
602 JaqVal::Num(n) => match n {
603 JaqNum::Int(v) => Value::Number(serde_json::Number::from(*v)),
604 JaqNum::Float(v) => serde_json::Number::from_f64(*v)
605 .map(Value::Number)
606 .unwrap_or(Value::Null),
607 JaqNum::BigInt(bi) => {
608 if let Some(i) = n.as_isize() {
610 Value::Number(serde_json::Number::from(i))
611 } else {
612 let s = bi.to_string();
614 if let Ok(i) = s.parse::<i64>() {
615 Value::Number(serde_json::Number::from(i))
616 } else if let Ok(u) = s.parse::<u64>() {
617 Value::Number(serde_json::Number::from(u))
618 } else {
619 Value::Null
620 }
621 }
622 }
623 JaqNum::Dec(s) => {
624 s.parse::<f64>()
626 .ok()
627 .and_then(serde_json::Number::from_f64)
628 .map(Value::Number)
629 .unwrap_or(Value::Null)
630 }
631 },
632 JaqVal::TStr(s) | JaqVal::BStr(s) => {
633 match std::str::from_utf8(s.as_ref()) {
634 Ok(v) => Value::String(v.to_string()),
635 Err(_) => Value::Null, }
637 }
638 JaqVal::Arr(items) => Value::Array(items.iter().map(jaq_to_json).collect()),
639 JaqVal::Obj(obj) => {
640 let map: serde_json::Map<String, Value> = obj
641 .iter()
642 .filter_map(|(k, v)| {
643 let key = match k {
644 JaqVal::TStr(s) | JaqVal::BStr(s) => {
645 std::str::from_utf8(s.as_ref()).ok().map(str::to_owned)
646 }
647 _ => None,
648 }?;
649 Some((key, jaq_to_json(v)))
650 })
651 .collect();
652 Value::Object(map)
653 }
654 }
655}
656
657impl Default for AssertionEngine {
658 fn default() -> Self {
659 Self::new()
660 }
661}
662
663fn find_lone_equals(expr: &str) -> Option<usize> {
667 let bytes = expr.as_bytes();
668 let mut in_string: Option<u8> = None; let mut i = 0;
670 while i < bytes.len() {
671 let b = bytes[i];
672 match in_string {
673 Some(q) => {
674 if b == b'\\' {
675 i += 2; continue;
677 }
678 if b == q {
679 in_string = None;
680 }
681 }
682 None => match b {
683 b'"' | b'\'' => in_string = Some(b),
684 b'=' => {
685 let prev = if i > 0 { bytes[i - 1] } else { 0 };
686 let next = if i + 1 < bytes.len() { bytes[i + 1] } else { 0 };
687 let is_double = next == b'=' || prev == b'=';
689 let is_compound = matches!(prev, b'!' | b'<' | b'>');
690 if !is_double && !is_compound {
691 return Some(i);
692 }
693 }
694 _ => {}
695 },
696 }
697 i += 1;
698 }
699 None
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705 use serde_json::json;
706
707 fn create_test_response() -> Value {
708 json!({
709 "id": 123,
710 "name": "test",
711 "email": "test@example.com",
712 "active": true,
713 "tags": ["a", "b", "c"],
714 "nested": {
715 "value": 42
716 }
717 })
718 }
719
720 #[test]
721 fn test_find_lone_equals_detects_typo() {
722 assert_eq!(find_lone_equals(".x = 5"), Some(3));
723 assert_eq!(find_lone_equals(".name = \"a\""), Some(6));
724 }
725
726 #[test]
727 fn test_find_lone_equals_ignores_comparisons() {
728 assert_eq!(find_lone_equals(".x == 5"), None);
729 assert_eq!(find_lone_equals(".x != 5"), None);
730 assert_eq!(find_lone_equals(".x <= 5"), None);
731 assert_eq!(find_lone_equals(".x >= 5"), None);
732 }
733
734 #[test]
735 fn test_find_lone_equals_ignores_string_contents() {
736 assert_eq!(find_lone_equals(".x == \"a=b\""), None);
738 assert_eq!(find_lone_equals(".x == \"a\\\"=b\""), None);
739 }
740
741 #[test]
742 fn test_lone_equals_assertion_fails_not_passes() {
743 let engine = AssertionEngine::new();
744 let response = create_test_response();
745 let result = engine.evaluate(".id = 123", &response, None, None).unwrap();
748 assert!(
749 matches!(result, AssertionResult::Fail { .. }),
750 "lone `=` must fail, got {:?}",
751 result
752 );
753 }
754
755 #[test]
756 fn test_assertion_result_fail() {
757 let result = AssertionResult::fail("test message");
758 if let AssertionResult::Fail { message, .. } = result {
759 assert_eq!(message, "test message");
760 } else {
761 panic!("Expected Fail result");
762 }
763 }
764
765 #[test]
766 fn test_assertion_result_fail_with_diff() {
767 let result = AssertionResult::fail_with_diff("mismatch", "expected", "actual");
768 if let AssertionResult::Fail {
769 message,
770 expected,
771 actual,
772 } = result
773 {
774 assert_eq!(message, "mismatch");
775 assert_eq!(expected, Some("expected".to_string()));
776 assert_eq!(actual, Some("actual".to_string()));
777 } else {
778 panic!("Expected Fail result");
779 }
780 }
781
782 #[test]
783 fn test_assertion_result_debug() {
784 let result = AssertionResult::Pass;
785 let debug_str = format!("{:?}", result);
786 assert!(debug_str.contains("Pass"));
787 }
788
789 #[test]
790 fn test_evaluate_equality_operator() {
791 let engine = AssertionEngine::new();
792 let response = create_test_response();
793
794 let result = engine
795 .evaluate(".id == 123", &response, None, None)
796 .unwrap();
797 if let AssertionResult::Pass = result {
798 } else {
800 panic!("Expected Pass for equality check");
801 }
802 }
803
804 #[test]
805 fn test_evaluate_bracket_index_assertion() {
806 let engine = AssertionEngine::new();
807 let response = serde_json::json!({
808 "ipsToDecorations": {
809 "10.0.0.1": {
810 "decoration": "web-frontend",
811 "environment": "production"
812 }
813 }
814 });
815
816 let result1 = engine
818 .evaluate(
819 ".ipsToDecorations[\"10.0.0.1\"].environment == \"production\"",
820 &response,
821 None,
822 None,
823 )
824 .unwrap();
825 assert!(
826 matches!(result1, AssertionResult::Pass),
827 "Expected Pass for correct value, got: {:?}",
828 result1
829 );
830
831 let result2 = engine
833 .evaluate(
834 ".ipsToDecorations[\"10.0.0.1\"].environment == \"production1\"",
835 &response,
836 None,
837 None,
838 )
839 .unwrap();
840 assert!(
841 matches!(result2, AssertionResult::Fail { .. }),
842 "Expected Fail for wrong value, got: {:?}",
843 result2
844 );
845 }
846
847 #[test]
848 fn test_evaluate_equality_operator_fail() {
849 let engine = AssertionEngine::new();
850 let response = create_test_response();
851
852 let result = engine
853 .evaluate(".id == 456", &response, None, None)
854 .unwrap();
855 if let AssertionResult::Fail { .. } = result {
856 } else {
858 panic!("Expected Fail for equality check");
859 }
860 }
861
862 #[test]
863 fn test_evaluate_inequality_operator() {
864 let engine = AssertionEngine::new();
865 let response = create_test_response();
866
867 let result = engine
868 .evaluate(".id != 456", &response, None, None)
869 .unwrap();
870 if let AssertionResult::Pass = result {
871 } else {
873 panic!("Expected Pass for inequality check");
874 }
875 }
876
877 #[test]
878 fn test_evaluate_contains_operator() {
879 let engine = AssertionEngine::new();
880 let response = create_test_response();
881
882 let result = engine
883 .evaluate(".name contains \"test\"", &response, None, None)
884 .unwrap();
885 if let AssertionResult::Pass = result {
886 } else {
888 panic!("Expected Pass for contains check");
889 }
890 }
891
892 #[test]
893 fn test_evaluate_contains_operator_array() {
894 let engine = AssertionEngine::new();
895 let response = create_test_response();
896
897 let result = engine
898 .evaluate(".tags contains \"a\"", &response, None, None)
899 .unwrap();
900 if let AssertionResult::Pass = result {
901 } else {
903 panic!("Expected Pass for array contains check");
904 }
905 }
906
907 #[test]
908 fn test_evaluate_starts_with_operator() {
909 let engine = AssertionEngine::new();
910 let response = create_test_response();
911
912 let result = engine
913 .evaluate(".name startsWith \"te\"", &response, None, None)
914 .unwrap();
915 if let AssertionResult::Pass = result {
916 } else {
918 panic!("Expected Pass for startsWith check");
919 }
920 }
921
922 #[test]
923 fn test_evaluate_ends_with_operator() {
924 let engine = AssertionEngine::new();
925 let response = create_test_response();
926
927 let result = engine
928 .evaluate(".name endsWith \"st\"", &response, None, None)
929 .unwrap();
930 if let AssertionResult::Pass = result {
931 } else {
933 panic!("Expected Pass for endsWith check");
934 }
935 }
936
937 #[test]
938 fn test_evaluate_numeric_greater_than() {
939 let engine = AssertionEngine::new();
940 let response = create_test_response();
941
942 let result = engine.evaluate(".id > 100", &response, None, None).unwrap();
943 if let AssertionResult::Pass = result {
944 } else {
946 panic!("Expected Pass for greater than check");
947 }
948 }
949
950 #[test]
951 fn test_evaluate_numeric_less_than() {
952 let engine = AssertionEngine::new();
953 let response = create_test_response();
954
955 let result = engine.evaluate(".id < 200", &response, None, None).unwrap();
956 if let AssertionResult::Pass = result {
957 } else {
959 panic!("Expected Pass for less than check");
960 }
961 }
962
963 #[test]
964 fn test_evaluate_numeric_gte() {
965 let engine = AssertionEngine::new();
966 let response = create_test_response();
967
968 let result = engine
969 .evaluate(".id >= 123", &response, None, None)
970 .unwrap();
971 if let AssertionResult::Pass = result {
972 } else {
974 panic!("Expected Pass for gte check");
975 }
976 }
977
978 #[test]
979 fn test_evaluate_numeric_lte() {
980 let engine = AssertionEngine::new();
981 let response = create_test_response();
982
983 let result = engine
984 .evaluate(".id <= 123", &response, None, None)
985 .unwrap();
986 if let AssertionResult::Pass = result {
987 } else {
989 panic!("Expected Pass for lte check");
990 }
991 }
992
993 #[test]
994 fn test_evaluate_matches_regex() {
995 let engine = AssertionEngine::new();
996 let response = create_test_response();
997
998 let result = engine
999 .evaluate(".name matches \"^te.*t$\"", &response, None, None)
1000 .unwrap();
1001 if let AssertionResult::Pass = result {
1002 } else {
1004 panic!("Expected Pass for regex match");
1005 }
1006 }
1007
1008 #[test]
1009 fn test_evaluate_matches_regex_fail() {
1010 let engine = AssertionEngine::new();
1011 let response = create_test_response();
1012
1013 let result = engine
1014 .evaluate(".name matches \"^xyz\"", &response, None, None)
1015 .unwrap();
1016 if let AssertionResult::Fail { .. } = result {
1017 } else {
1019 panic!("Expected Fail for regex match");
1020 }
1021 }
1022
1023 #[test]
1024 fn test_evaluate_nested_path() {
1025 let engine = AssertionEngine::new();
1026 let response = create_test_response();
1027
1028 let result = engine
1029 .evaluate(".nested.value == 42", &response, None, None)
1030 .unwrap();
1031 if let AssertionResult::Pass = result {
1032 } else {
1034 panic!("Expected Pass for nested path check");
1035 }
1036 }
1037
1038 #[test]
1039 fn test_evaluate_boolean_path() {
1040 let engine = AssertionEngine::new();
1041 let response = create_test_response();
1042
1043 let result = engine
1044 .evaluate(".active == true", &response, None, None)
1045 .unwrap();
1046 if let AssertionResult::Pass = result {
1047 } else {
1049 panic!("Expected Pass for boolean check");
1050 }
1051 }
1052
1053 #[test]
1054 fn test_evaluate_array_index() {
1055 let engine = AssertionEngine::new();
1056 let response = create_test_response();
1057
1058 let result = engine
1059 .evaluate(".tags[0] == \"a\"", &response, None, None)
1060 .unwrap();
1061 if let AssertionResult::Pass = result {
1062 } else {
1064 panic!("Expected Pass for array index check");
1065 }
1066 }
1067
1068 #[test]
1069 fn test_evaluate_unsupported_syntax() {
1070 let engine = AssertionEngine::new();
1071 let response = create_test_response();
1072
1073 let result = engine.evaluate("some_unknown_function()", &response, None, None);
1075 assert!(result.is_ok());
1077 }
1078
1079 #[test]
1080 fn test_evaluate_all() {
1081 let engine = AssertionEngine::new();
1082 let response = create_test_response();
1083
1084 let assertions = vec![".id == 123".to_string(), ".name == \"test\"".to_string()];
1085
1086 let results = engine.evaluate_all(&assertions, &response, None, None);
1087 assert_eq!(results.len(), 2);
1088 assert!(results.iter().all(|r| matches!(r, AssertionResult::Pass)));
1089 }
1090
1091 #[test]
1092 fn test_evaluate_all_with_failure() {
1093 let engine = AssertionEngine::new();
1094 let response = create_test_response();
1095
1096 let assertions = vec![".id == 123".to_string(), ".id == 999".to_string()];
1097
1098 let results = engine.evaluate_all(&assertions, &response, None, None);
1099 assert_eq!(results.len(), 2);
1100 assert!(matches!(&results[0], AssertionResult::Pass));
1101 assert!(matches!(&results[1], AssertionResult::Fail { .. }));
1102 }
1103
1104 #[test]
1105 fn test_evaluate_type_cast_number() {
1106 let engine = AssertionEngine::new();
1107 let response = json!({
1108 "price": 42
1109 });
1110
1111 let result = engine.evaluate(".price:number >= 0", &response, None, None);
1112 assert!(
1113 matches!(result, Ok(AssertionResult::Pass)),
1114 "Expected Pass, got: {:?}",
1115 result
1116 );
1117
1118 let result = engine.evaluate(".price:number < 0", &response, None, None);
1119 assert!(
1120 matches!(result, Ok(AssertionResult::Fail { .. })),
1121 "Expected Fail, got: {:?}",
1122 result
1123 );
1124 }
1125
1126 #[test]
1127 fn test_evaluate_type_cast_string() {
1128 let engine = AssertionEngine::new();
1129 let response = json!({
1130 "name": "hello world"
1131 });
1132
1133 let result = engine.evaluate(".name:string contains \"hello\"", &response, None, None);
1134 assert!(
1135 matches!(result, Ok(AssertionResult::Pass)),
1136 "Expected Pass, got: {:?}",
1137 result
1138 );
1139
1140 let result = engine.evaluate(".name:string startsWith \"he\"", &response, None, None);
1141 assert!(
1142 matches!(result, Ok(AssertionResult::Pass)),
1143 "Expected Pass, got: {:?}",
1144 result
1145 );
1146 }
1147
1148 #[test]
1149 fn test_evaluate_type_cast_is_noop() {
1150 let engine = AssertionEngine::new();
1151 let response = json!({
1152 "value": 123
1153 });
1154
1155 let without_cast = engine.evaluate(".value == 123", &response, None, None);
1157 let with_cast = engine.evaluate(".value:number == 123", &response, None, None);
1158 assert_eq!(
1159 matches!(without_cast, Ok(AssertionResult::Pass)),
1160 matches!(with_cast, Ok(AssertionResult::Pass)),
1161 "Type cast should not change evaluation result"
1162 );
1163 }
1164
1165 #[test]
1166 fn test_jq_fallback_truthy_non_bool_output() {
1167 let engine = AssertionEngine::new();
1170 let response = create_test_response();
1171
1172 let result = engine
1173 .evaluate(".tags | length", &response, None, None)
1174 .unwrap();
1175 assert!(
1176 matches!(result, AssertionResult::Pass),
1177 "Expected Pass, got: {:?}",
1178 result
1179 );
1180 }
1181
1182 #[test]
1183 fn test_jq_fallback_false_output_shows_value() {
1184 let engine = AssertionEngine::new();
1185 let response = create_test_response();
1186
1187 let result = engine
1189 .evaluate(".tags | length > 10", &response, None, None)
1190 .unwrap();
1191 if let AssertionResult::Fail { message, .. } = result {
1192 assert!(message.contains("false"), "message: {}", message);
1193 } else {
1194 panic!("Expected Fail, got: {:?}", result);
1195 }
1196 }
1197
1198 #[test]
1199 fn test_jq_fallback_null_output_fails() {
1200 let engine = AssertionEngine::new();
1201 let response = create_test_response();
1202
1203 let result = engine
1205 .evaluate(".missing_key | .", &response, None, None)
1206 .unwrap();
1207 assert!(
1208 matches!(result, AssertionResult::Fail { .. }),
1209 "Expected Fail, got: {:?}",
1210 result
1211 );
1212 }
1213
1214 #[test]
1215 fn test_query_jq_simple() {
1216 let engine = AssertionEngine::new();
1217 let response = create_test_response();
1218
1219 let results = engine.query(".id", &response).unwrap();
1220 assert_eq!(results.len(), 1);
1221 assert_eq!(results[0], json!(123));
1222 }
1223
1224 #[test]
1225 fn test_query_jq_nested() {
1226 let engine = AssertionEngine::new();
1227 let response = create_test_response();
1228
1229 let results = engine.query(".nested.value", &response).unwrap();
1230 assert_eq!(results.len(), 1);
1231 assert_eq!(results[0], json!(42));
1232 }
1233
1234 #[test]
1235 fn test_query_jq_array() {
1236 let engine = AssertionEngine::new();
1237 let response = create_test_response();
1238
1239 let results = engine.query(".tags[]", &response).unwrap();
1240 assert_eq!(results.len(), 3);
1241 assert_eq!(results[0], json!("a"));
1242 assert_eq!(results[1], json!("b"));
1243 assert_eq!(results[2], json!("c"));
1244 }
1245
1246 #[test]
1247 fn test_query_jq_filter() {
1248 let engine = AssertionEngine::new();
1249 let response = json!([1, 2, 3, 4, 5]);
1250
1251 let results = engine.query(".[] | select(. > 3)", &response).unwrap();
1252 assert_eq!(results.len(), 2);
1253 assert_eq!(results[0], json!(4));
1254 assert_eq!(results[1], json!(5));
1255 }
1256
1257 #[test]
1258 fn test_query_jq_length() {
1259 let engine = AssertionEngine::new();
1260 let response = create_test_response();
1261
1262 let results = engine.query(".tags | length", &response).unwrap();
1263 assert_eq!(results.len(), 1);
1264 assert_eq!(results[0], json!(3));
1265 }
1266
1267 #[test]
1268 fn test_query_invalid_expression() {
1269 let engine = AssertionEngine::new();
1270 let response = create_test_response();
1271
1272 let results = engine.query("invalid[[[", &response);
1273 assert!(results.is_err());
1274 }
1275
1276 #[test]
1277 fn test_jaq_to_json_dec_number() {
1278 let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("2.5".to_string())));
1279 assert_eq!(jaq_to_json(&dec), json!(2.5));
1280 }
1281
1282 #[test]
1283 fn test_jaq_to_json_invalid_dec_number() {
1284 let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("not-a-number".to_string())));
1285 assert_eq!(jaq_to_json(&dec), Value::Null);
1286 }
1287
1288 #[test]
1289 fn test_json_to_jaq_null() {
1290 let result = json_to_jaq(&json!(null));
1291 assert!(matches!(result, JaqVal::Null));
1292 }
1293
1294 #[test]
1295 fn test_json_to_jaq_bool() {
1296 let result = json_to_jaq(&json!(true));
1297 assert!(matches!(result, JaqVal::Bool(true)));
1298 }
1299
1300 #[test]
1301 fn test_json_to_jaq_number_int() {
1302 let result = json_to_jaq(&json!(42));
1303 assert!(matches!(result, JaqVal::Num(JaqNum::Int(42))));
1304 }
1305
1306 #[test]
1307 fn test_json_to_jaq_number_float() {
1308 let result = json_to_jaq(&json!(4.14));
1309 assert!(matches!(result, JaqVal::Num(JaqNum::Float(f)) if (f - 4.14).abs() < 0.001));
1310 }
1311
1312 #[test]
1313 fn test_json_to_jaq_string() {
1314 let result = json_to_jaq(&json!("hello"));
1315 assert!(matches!(result, JaqVal::TStr(_)));
1316 }
1317
1318 #[test]
1319 fn test_json_to_jaq_array() {
1320 let result = json_to_jaq(&json!([1, 2, 3]));
1321 assert!(matches!(result, JaqVal::Arr(_)));
1322 }
1323
1324 #[test]
1325 fn test_json_to_jaq_object() {
1326 let result = json_to_jaq(&json!({"key": "value"}));
1327 assert!(matches!(result, JaqVal::Obj(_)));
1328 }
1329
1330 #[test]
1331 fn test_jaq_filter_cache_returns_same_arc() {
1332 let expr = ".__cache_test_sentinel__";
1333 let first = AssertionEngine::get_or_compile_jaq_filter(expr).unwrap();
1334 let second = AssertionEngine::get_or_compile_jaq_filter(expr).unwrap();
1335 assert!(Arc::ptr_eq(&first, &second));
1336 }
1337 #[test]
1338 fn test_assertion_result_negate() {
1339 let pass = AssertionResult::Pass;
1340 assert!(matches!(pass.negate(), AssertionResult::Fail { .. }));
1341
1342 let fail = AssertionResult::fail("msg");
1343 assert!(matches!(fail.negate(), AssertionResult::Pass));
1344
1345 let error = AssertionResult::Error("err".into());
1346 assert!(matches!(error.negate(), AssertionResult::Error(_)));
1347 }
1348
1349 #[test]
1350 fn test_assertion_engine_get_failures() {
1351 let engine = AssertionEngine::new();
1352 let response = create_test_response();
1353 let assertions = vec![".id == 123".to_string(), ".id == 999".to_string()];
1354 let results = engine.evaluate_all(&assertions, &response, None, None);
1355 let failures = engine.get_failures(&results);
1356 assert_eq!(failures.len(), 1);
1357 }
1358
1359 #[test]
1360 fn test_assertion_engine_has_failures() {
1361 let engine = AssertionEngine::new();
1362 let response = create_test_response();
1363 let result = engine.evaluate_all(&[".id == 999".to_string()], &response, None, None);
1364 assert!(engine.has_failures(&result));
1365 }
1366
1367 #[test]
1368 fn test_assertion_engine_no_failures() {
1369 let engine = AssertionEngine::new();
1370 let response = create_test_response();
1371 let result = engine.evaluate_all(&[".id == 123".to_string()], &response, None, None);
1372 assert!(!engine.has_failures(&result));
1373 }
1374
1375 #[test]
1376 fn test_assertion_engine_default() {
1377 let engine = AssertionEngine::default();
1378 let response = create_test_response();
1379 let result = engine
1380 .evaluate(".id == 123", &response, None, None)
1381 .unwrap();
1382 assert!(matches!(result, AssertionResult::Pass));
1383 }
1384
1385 #[test]
1386 fn test_assertion_result_fail_with_diff_fields() {
1387 let result = AssertionResult::fail_with_diff("mismatch", "{\"a\":1}", "{\"a\":2}");
1388 match result {
1389 AssertionResult::Fail {
1390 message,
1391 expected,
1392 actual,
1393 } => {
1394 assert_eq!(message, "mismatch");
1395 assert_eq!(expected.unwrap(), "{\"a\":1}");
1396 assert_eq!(actual.unwrap(), "{\"a\":2}");
1397 }
1398 _ => panic!("Expected Fail"),
1399 }
1400 }
1401
1402 #[test]
1403 fn test_evaluate_url_scheme_parse_only() {
1404 use apif_ast::assertion_ast::{AssertionExpr, assertion_to_string, parse_assertion};
1405 let expr = parse_assertion("@url.scheme(\"https://example.com\") == \"https\"");
1406 assert!(
1407 !matches!(&expr, AssertionExpr::Raw(_)),
1408 "Expression should be parsed, not Raw: {:?}",
1409 expr
1410 );
1411 let s = assertion_to_string(&expr);
1412 assert_eq!(
1413 s, "@url.scheme(\"https://example.com\") == \"https\"",
1414 "Roundtrip failed"
1415 );
1416 }
1417
1418 #[test]
1419 fn test_rewrite_plugin_calls_basic() {
1420 assert_eq!(
1421 rewrite_plugin_calls("@len(.items) == .n").unwrap(),
1422 "__plugin(\"len\"; [.items]) == .n"
1423 );
1424 }
1425
1426 #[test]
1427 fn test_rewrite_plugin_calls_multiple_args() {
1428 assert_eq!(
1429 rewrite_plugin_calls("@regex(.name, \"^A\")").unwrap(),
1430 "__plugin(\"regex\"; [.name, \"^A\"])"
1431 );
1432 }
1433
1434 #[test]
1435 fn test_rewrite_plugin_calls_nested() {
1436 assert_eq!(
1437 rewrite_plugin_calls(".x | map(@is_email(.)) | all").unwrap(),
1438 ".x | map(__plugin(\"is_email\"; [.])) | all"
1439 );
1440 }
1441
1442 #[test]
1443 fn test_rewrite_plugin_calls_leaves_format_strings() {
1444 assert_eq!(
1446 rewrite_plugin_calls(".x | @base64").unwrap(),
1447 ".x | @base64"
1448 );
1449 }
1450
1451 #[test]
1452 fn test_rewrite_plugin_calls_ignores_at_in_string() {
1453 assert_eq!(
1455 rewrite_plugin_calls(".x == \"@len(a)\"").unwrap(),
1456 ".x == \"@len(a)\""
1457 );
1458 }
1459
1460 #[test]
1461 fn test_rewrite_plugin_calls_rejects_context_plugin() {
1462 let err = rewrite_plugin_calls("@header(\"x\") | length").unwrap_err();
1463 assert!(
1464 err.to_string().contains("not available in jq expressions"),
1465 "unexpected error: {}",
1466 err
1467 );
1468 }
1469
1470 #[test]
1471 fn test_jaq_context_plugin_reports_clear_error() {
1472 let engine = AssertionEngine::new();
1475 let response = json!({"x": 1});
1476 let result = engine
1477 .evaluate(".list | map(@header(\"y\")) | all", &response, None, None)
1478 .unwrap();
1479 let msg = match result {
1480 AssertionResult::Error(m) => m,
1481 AssertionResult::Fail { message, .. } => message,
1482 other => panic!("expected error/fail, got {:?}", other),
1483 };
1484 assert!(
1485 msg.contains("not available in jq expressions"),
1486 "unexpected message: {}",
1487 msg
1488 );
1489 }
1490}