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 None,
360 )
361 }
362
363 #[allow(clippy::too_many_arguments)]
364 pub fn evaluate_with_timing(
365 &self,
366 assertion: &str,
367 response: &Value,
368 headers: Option<&HashMap<String, String>>,
369 trailers: Option<&HashMap<String, String>>,
370 timing: Option<&AssertionTiming>,
371 variables: &HashMap<String, Value>,
372 protocol: Option<&str>,
373 ) -> Result<AssertionResult> {
374 let trimmed = assertion.trim();
375
376 let ctx = operators::EvalCtx::new(response, variables)
377 .with_headers(headers)
378 .with_trailers(trailers)
379 .with_timing(timing)
380 .with_protocol(protocol);
381
382 match operators::evaluate_assertion(&*self.plugin_registry, trimmed, &ctx) {
383 Ok(Some(result)) => Ok(result),
384 Ok(None) => {
385 if let Some(pos) = find_lone_equals(trimmed) {
390 return Ok(AssertionResult::fail(format!(
391 "Assertion uses `=` at position {} — did you mean `==`? \
392 (`=` is not a comparison operator): {}",
393 pos, trimmed
394 )));
395 }
396 self.evaluate_jaq(trimmed, response)
397 }
398 Err(e) => Err(e),
399 }
400 }
401
402 pub fn query(&self, expr: &str, input: &Value) -> Result<Vec<Value>> {
404 let values = self.run_jaq(expr, input)?;
405 Ok(values.iter().map(jaq_to_json).collect())
406 }
407
408 fn evaluate_jaq(&self, expr: &str, response: &Value) -> Result<AssertionResult> {
409 let out = match self.run_jaq(expr, response) {
410 Ok(out) => out,
411 Err(e) => return Ok(AssertionResult::Error(format!("JQ Parse Error: {}", e))),
412 };
413
414 for val in &out {
417 if matches!(val, JaqVal::Bool(false) | JaqVal::Null) {
418 let rendered = serde_json::to_string(&jaq_to_json(val))
419 .unwrap_or_else(|_| "<unprintable>".to_string());
420 return Ok(AssertionResult::fail(format!(
421 "JQ assertion evaluated to falsy value {}: {}",
422 rendered, expr
423 )));
424 }
425 }
426
427 if out.is_empty() {
428 Ok(AssertionResult::fail(format!(
429 "JQ assertion produced no output (falsey): {}",
430 expr
431 )))
432 } else {
433 Ok(AssertionResult::Pass)
434 }
435 }
436
437 fn run_jaq(&self, expr: &str, input: &Value) -> Result<Vec<JaqVal>> {
438 let rewritten = rewrite_plugin_calls(expr)?;
441 let filter = Self::get_or_compile_jaq_filter(&rewritten)?;
442
443 let input = json_to_jaq(input);
444
445 let _registry_guard = PluginRegistryGuard::set(self.plugin_registry.clone());
447
448 let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
449 let out = filter.id.run((ctx, input)).map(unwrap_valr);
450
451 let mut values = Vec::new();
452 for item in out {
453 match item {
454 Ok(v) => values.push(v),
455 Err(e) => return Err(anyhow::anyhow!("JQ Runtime Error: {}", e)),
456 }
457 }
458
459 Ok(values)
460 }
461
462 fn get_or_compile_jaq_filter(expr: &str) -> Result<Arc<JaqFilter>> {
463 use jaq_core::defs as core_defs;
464 use jaq_core::funs as core_funs;
465
466 if let Some(cached) = JAQ_FILTER_CACHE
467 .lock()
468 .unwrap_or_else(|e| e.into_inner())
469 .get(expr)
470 .cloned()
471 {
472 return Ok(cached);
473 }
474
475 let cleaned = strip_numeric_underscores(expr);
476
477 let arena = load::Arena::default();
478 let defs = core_defs().chain(jaq_std::defs()).chain(jaq_json::defs());
479 let funs = core_funs()
480 .chain(jaq_std::funs())
481 .chain(jaq_json::funs())
482 .chain(std::iter::once(jaq_plugin_fun()));
483 let loader = load::Loader::new(defs);
484 let program = load::File {
485 code: cleaned.as_str(),
486 path: (),
487 };
488
489 let modules = loader
490 .load(&arena, program)
491 .map_err(|errs| anyhow::anyhow!("Failed to parse JQ expression: {:?}", errs))?;
492
493 let filter = Compiler::default()
494 .with_funs(funs)
495 .compile(modules)
496 .map_err(|errs| anyhow::anyhow!("Failed to compile JQ expression: {:?}", errs))?;
497
498 let filter = Arc::new(filter);
499 JAQ_FILTER_CACHE
500 .lock()
501 .unwrap_or_else(|e| e.into_inner())
502 .insert(expr.to_string(), Arc::clone(&filter));
503
504 Ok(filter)
505 }
506
507 pub(super) fn eval_jaq_one(expr: &str, input: &Value) -> anyhow::Result<Value> {
510 let filter = Self::get_or_compile_jaq_filter(expr)?;
511 let jaq_input = json_to_jaq(input);
512 let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
513 let mut out = filter.id.run((ctx, jaq_input)).map(unwrap_valr);
514 if let Some(Ok(val)) = out.next() {
515 Ok(jaq_to_json(&val))
516 } else {
517 Err(anyhow::anyhow!("JQ produced no output for: {}", expr))
518 }
519 }
520
521 #[must_use]
522 pub fn has_failures(&self, results: &[AssertionResult]) -> bool {
523 results
524 .iter()
525 .any(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
526 }
527
528 pub fn get_failures<'a>(&self, results: &'a [AssertionResult]) -> Vec<&'a AssertionResult> {
529 results
530 .iter()
531 .filter(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
532 .collect()
533 }
534
535 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 None,
550 )
551 }
552
553 #[allow(clippy::too_many_arguments)]
554 pub fn evaluate_all_with_timing(
555 &self,
556 assertions: &[String],
557 response: &serde_json::Value,
558 headers: Option<&HashMap<String, String>>,
559 trailers: Option<&HashMap<String, String>>,
560 timing: Option<&AssertionTiming>,
561 variables: &HashMap<String, Value>,
562 protocol: Option<&str>,
563 ) -> Vec<AssertionResult> {
564 self.evaluate_all_with_records(
565 assertions, response, headers, trailers, timing, variables, protocol,
566 )
567 .into_iter()
568 .map(|(result, _elapsed_ms)| result)
569 .collect()
570 }
571
572 #[allow(clippy::too_many_arguments)]
576 pub fn evaluate_all_with_records(
577 &self,
578 assertions: &[String],
579 response: &serde_json::Value,
580 headers: Option<&HashMap<String, String>>,
581 trailers: Option<&HashMap<String, String>>,
582 timing: Option<&AssertionTiming>,
583 variables: &HashMap<String, Value>,
584 protocol: Option<&str>,
585 ) -> Vec<(AssertionResult, u64)> {
586 assertions
587 .iter()
588 .map(|assertion| {
589 let start = std::time::Instant::now();
590 let result = self
591 .evaluate_with_timing(
592 assertion, response, headers, trailers, timing, variables, protocol,
593 )
594 .unwrap_or_else(|e| AssertionResult::Error(format!("Internal error: {}", e)));
595 tracing::trace!("assertion: {assertion} -> {result:?}");
596 (result, start.elapsed().as_millis() as u64)
597 })
598 .collect()
599 }
600}
601
602fn strip_numeric_underscores(expr: &str) -> String {
605 let mut out = String::with_capacity(expr.len());
606 let mut chars = expr.chars().peekable();
607
608 while let Some(c) = chars.next() {
609 if c == '"' {
610 out.push(c);
611 while let Some(next) = chars.next() {
612 out.push(next);
613 if next == '\\' {
614 if let Some(escaped) = chars.next() {
615 out.push(escaped);
616 }
617 } else if next == '"' {
618 break;
619 }
620 }
621 } else {
622 let is_digit_separator = c == '_'
623 && out.chars().next_back().is_some_and(|p| p.is_ascii_digit())
624 && chars.peek().is_some_and(|n| n.is_ascii_digit());
625 if !is_digit_separator {
626 out.push(c);
627 }
628 }
629 }
630
631 out
632}
633
634fn json_to_jaq(value: &Value) -> JaqVal {
635 match value {
636 Value::Null => JaqVal::Null,
637 Value::Bool(v) => JaqVal::Bool(*v),
638 Value::Number(n) => {
639 if let Some(i) = n.as_i64() {
640 JaqVal::Num(JaqNum::from_integral(i))
641 } else if let Some(u) = n.as_u64() {
642 JaqVal::Num(JaqNum::from_integral(u))
643 } else if let Some(f) = n.as_f64() {
644 JaqVal::Num(JaqNum::Float(f))
645 } else {
646 JaqVal::Null
647 }
648 }
649 Value::String(s) => JaqVal::utf8_str(s.clone()),
650 Value::Array(items) => JaqVal::Arr(JaqRc::new(items.iter().map(json_to_jaq).collect())),
651 Value::Object(obj) => {
652 let map: JaqMap = obj
653 .iter()
654 .map(|(k, v)| (JaqVal::utf8_str(k.clone()), json_to_jaq(v)))
655 .collect();
656 JaqVal::Obj(JaqRc::new(map))
657 }
658 }
659}
660
661fn jaq_to_json(value: &JaqVal) -> Value {
662 match value {
663 JaqVal::Null => Value::Null,
664 JaqVal::Bool(v) => Value::Bool(*v),
665 JaqVal::Num(n) => match n {
666 JaqNum::Int(v) => Value::Number(serde_json::Number::from(*v)),
667 JaqNum::Float(v) => serde_json::Number::from_f64(*v)
668 .map(Value::Number)
669 .unwrap_or(Value::Null),
670 JaqNum::BigInt(bi) => {
671 if let Some(i) = n.as_isize() {
673 Value::Number(serde_json::Number::from(i))
674 } else {
675 let s = bi.to_string();
677 if let Ok(i) = s.parse::<i64>() {
678 Value::Number(serde_json::Number::from(i))
679 } else if let Ok(u) = s.parse::<u64>() {
680 Value::Number(serde_json::Number::from(u))
681 } else {
682 Value::Null
683 }
684 }
685 }
686 JaqNum::Dec(s) => {
687 s.parse::<f64>()
689 .ok()
690 .and_then(serde_json::Number::from_f64)
691 .map(Value::Number)
692 .unwrap_or(Value::Null)
693 }
694 },
695 JaqVal::TStr(s) | JaqVal::BStr(s) => {
696 match std::str::from_utf8(s.as_ref()) {
697 Ok(v) => Value::String(v.to_string()),
698 Err(_) => Value::Null, }
700 }
701 JaqVal::Arr(items) => Value::Array(items.iter().map(jaq_to_json).collect()),
702 JaqVal::Obj(obj) => {
703 let map: serde_json::Map<String, Value> = obj
704 .iter()
705 .filter_map(|(k, v)| {
706 let key = match k {
707 JaqVal::TStr(s) | JaqVal::BStr(s) => {
708 std::str::from_utf8(s.as_ref()).ok().map(str::to_owned)
709 }
710 _ => None,
711 }?;
712 Some((key, jaq_to_json(v)))
713 })
714 .collect();
715 Value::Object(map)
716 }
717 }
718}
719
720impl Default for AssertionEngine {
721 fn default() -> Self {
722 Self::new()
723 }
724}
725
726fn find_lone_equals(expr: &str) -> Option<usize> {
730 let bytes = expr.as_bytes();
731 let mut in_string: Option<u8> = None; let mut i = 0;
733 while i < bytes.len() {
734 let b = bytes[i];
735 match in_string {
736 Some(q) => {
737 if b == b'\\' {
738 i += 2; continue;
740 }
741 if b == q {
742 in_string = None;
743 }
744 }
745 None => match b {
746 b'"' | b'\'' => in_string = Some(b),
747 b'=' => {
748 let prev = if i > 0 { bytes[i - 1] } else { 0 };
749 let next = if i + 1 < bytes.len() { bytes[i + 1] } else { 0 };
750 let is_double = next == b'=' || prev == b'=';
752 let is_compound = matches!(prev, b'!' | b'<' | b'>');
753 if !is_double && !is_compound {
754 return Some(i);
755 }
756 }
757 _ => {}
758 },
759 }
760 i += 1;
761 }
762 None
763}
764
765#[cfg(test)]
766mod tests {
767 use super::*;
768 use serde_json::json;
769
770 fn create_test_response() -> Value {
771 json!({
772 "id": 123,
773 "name": "test",
774 "email": "test@example.com",
775 "active": true,
776 "tags": ["a", "b", "c"],
777 "nested": {
778 "value": 42
779 }
780 })
781 }
782
783 #[test]
784 fn strip_numeric_underscores_merges_digit_separators_outside_strings() {
785 assert_eq!(
786 strip_numeric_underscores(".amount == 1_000_000"),
787 ".amount == 1000000"
788 );
789 assert_eq!(
790 strip_numeric_underscores(".price == 1_234.567_89"),
791 ".price == 1234.56789"
792 );
793 assert_eq!(strip_numeric_underscores(".foo_bar == 1"), ".foo_bar == 1");
796 assert_eq!(
798 strip_numeric_underscores(r#".id == "a_1_2_3""#),
799 r#".id == "a_1_2_3""#
800 );
801 }
802
803 #[test]
804 fn assertion_with_numeric_digit_separators_matches_the_plain_number() {
805 let engine = AssertionEngine::new();
806 let response = json!({"amount": 1_000_000});
807 let result = engine
808 .evaluate(".amount == 1_000_000", &response, None, None)
809 .unwrap();
810 assert_eq!(result, AssertionResult::Pass);
811 }
812
813 #[test]
814 fn test_find_lone_equals_detects_typo() {
815 assert_eq!(find_lone_equals(".x = 5"), Some(3));
816 assert_eq!(find_lone_equals(".name = \"a\""), Some(6));
817 }
818
819 #[test]
820 fn test_find_lone_equals_ignores_comparisons() {
821 assert_eq!(find_lone_equals(".x == 5"), None);
822 assert_eq!(find_lone_equals(".x != 5"), None);
823 assert_eq!(find_lone_equals(".x <= 5"), None);
824 assert_eq!(find_lone_equals(".x >= 5"), None);
825 }
826
827 #[test]
828 fn test_find_lone_equals_ignores_string_contents() {
829 assert_eq!(find_lone_equals(".x == \"a=b\""), None);
831 assert_eq!(find_lone_equals(".x == \"a\\\"=b\""), None);
832 }
833
834 #[test]
835 fn test_lone_equals_assertion_fails_not_passes() {
836 let engine = AssertionEngine::new();
837 let response = create_test_response();
838 let result = engine.evaluate(".id = 123", &response, None, None).unwrap();
841 assert!(
842 matches!(result, AssertionResult::Fail { .. }),
843 "lone `=` must fail, got {:?}",
844 result
845 );
846 }
847
848 #[test]
849 fn test_assertion_result_fail() {
850 let result = AssertionResult::fail("test message");
851 if let AssertionResult::Fail { message, .. } = result {
852 assert_eq!(message, "test message");
853 } else {
854 panic!("Expected Fail result");
855 }
856 }
857
858 #[test]
859 fn test_assertion_result_fail_with_diff() {
860 let result = AssertionResult::fail_with_diff("mismatch", "expected", "actual");
861 if let AssertionResult::Fail {
862 message,
863 expected,
864 actual,
865 } = result
866 {
867 assert_eq!(message, "mismatch");
868 assert_eq!(expected, Some("expected".to_string()));
869 assert_eq!(actual, Some("actual".to_string()));
870 } else {
871 panic!("Expected Fail result");
872 }
873 }
874
875 #[test]
876 fn test_assertion_result_debug() {
877 let result = AssertionResult::Pass;
878 let debug_str = format!("{:?}", result);
879 assert!(debug_str.contains("Pass"));
880 }
881
882 #[test]
883 fn test_evaluate_equality_operator() {
884 let engine = AssertionEngine::new();
885 let response = create_test_response();
886
887 let result = engine
888 .evaluate(".id == 123", &response, None, None)
889 .unwrap();
890 if let AssertionResult::Pass = result {
891 } else {
893 panic!("Expected Pass for equality check");
894 }
895 }
896
897 #[test]
898 fn test_evaluate_bracket_index_assertion() {
899 let engine = AssertionEngine::new();
900 let response = serde_json::json!({
901 "ipsToDecorations": {
902 "10.0.0.1": {
903 "decoration": "web-frontend",
904 "environment": "production"
905 }
906 }
907 });
908
909 let result1 = engine
911 .evaluate(
912 ".ipsToDecorations[\"10.0.0.1\"].environment == \"production\"",
913 &response,
914 None,
915 None,
916 )
917 .unwrap();
918 assert!(
919 matches!(result1, AssertionResult::Pass),
920 "Expected Pass for correct value, got: {:?}",
921 result1
922 );
923
924 let result2 = engine
926 .evaluate(
927 ".ipsToDecorations[\"10.0.0.1\"].environment == \"production1\"",
928 &response,
929 None,
930 None,
931 )
932 .unwrap();
933 assert!(
934 matches!(result2, AssertionResult::Fail { .. }),
935 "Expected Fail for wrong value, got: {:?}",
936 result2
937 );
938 }
939
940 #[test]
941 fn test_evaluate_equality_operator_fail() {
942 let engine = AssertionEngine::new();
943 let response = create_test_response();
944
945 let result = engine
946 .evaluate(".id == 456", &response, None, None)
947 .unwrap();
948 if let AssertionResult::Fail { .. } = result {
949 } else {
951 panic!("Expected Fail for equality check");
952 }
953 }
954
955 #[test]
956 fn test_evaluate_inequality_operator() {
957 let engine = AssertionEngine::new();
958 let response = create_test_response();
959
960 let result = engine
961 .evaluate(".id != 456", &response, None, None)
962 .unwrap();
963 if let AssertionResult::Pass = result {
964 } else {
966 panic!("Expected Pass for inequality check");
967 }
968 }
969
970 #[test]
971 fn test_evaluate_contains_operator() {
972 let engine = AssertionEngine::new();
973 let response = create_test_response();
974
975 let result = engine
976 .evaluate(".name contains \"test\"", &response, None, None)
977 .unwrap();
978 if let AssertionResult::Pass = result {
979 } else {
981 panic!("Expected Pass for contains check");
982 }
983 }
984
985 #[test]
986 fn test_evaluate_contains_operator_array() {
987 let engine = AssertionEngine::new();
988 let response = create_test_response();
989
990 let result = engine
991 .evaluate(".tags contains \"a\"", &response, None, None)
992 .unwrap();
993 if let AssertionResult::Pass = result {
994 } else {
996 panic!("Expected Pass for array contains check");
997 }
998 }
999
1000 #[test]
1001 fn test_evaluate_starts_with_operator() {
1002 let engine = AssertionEngine::new();
1003 let response = create_test_response();
1004
1005 let result = engine
1006 .evaluate(".name startsWith \"te\"", &response, None, None)
1007 .unwrap();
1008 if let AssertionResult::Pass = result {
1009 } else {
1011 panic!("Expected Pass for startsWith check");
1012 }
1013 }
1014
1015 #[test]
1016 fn test_evaluate_ends_with_operator() {
1017 let engine = AssertionEngine::new();
1018 let response = create_test_response();
1019
1020 let result = engine
1021 .evaluate(".name endsWith \"st\"", &response, None, None)
1022 .unwrap();
1023 if let AssertionResult::Pass = result {
1024 } else {
1026 panic!("Expected Pass for endsWith check");
1027 }
1028 }
1029
1030 #[test]
1031 fn test_evaluate_numeric_greater_than() {
1032 let engine = AssertionEngine::new();
1033 let response = create_test_response();
1034
1035 let result = engine.evaluate(".id > 100", &response, None, None).unwrap();
1036 if let AssertionResult::Pass = result {
1037 } else {
1039 panic!("Expected Pass for greater than check");
1040 }
1041 }
1042
1043 #[test]
1044 fn test_evaluate_numeric_less_than() {
1045 let engine = AssertionEngine::new();
1046 let response = create_test_response();
1047
1048 let result = engine.evaluate(".id < 200", &response, None, None).unwrap();
1049 if let AssertionResult::Pass = result {
1050 } else {
1052 panic!("Expected Pass for less than check");
1053 }
1054 }
1055
1056 #[test]
1057 fn test_evaluate_numeric_gte() {
1058 let engine = AssertionEngine::new();
1059 let response = create_test_response();
1060
1061 let result = engine
1062 .evaluate(".id >= 123", &response, None, None)
1063 .unwrap();
1064 if let AssertionResult::Pass = result {
1065 } else {
1067 panic!("Expected Pass for gte check");
1068 }
1069 }
1070
1071 #[test]
1072 fn test_evaluate_numeric_lte() {
1073 let engine = AssertionEngine::new();
1074 let response = create_test_response();
1075
1076 let result = engine
1077 .evaluate(".id <= 123", &response, None, None)
1078 .unwrap();
1079 if let AssertionResult::Pass = result {
1080 } else {
1082 panic!("Expected Pass for lte check");
1083 }
1084 }
1085
1086 #[test]
1087 fn test_evaluate_matches_regex() {
1088 let engine = AssertionEngine::new();
1089 let response = create_test_response();
1090
1091 let result = engine
1092 .evaluate(".name matches \"^te.*t$\"", &response, None, None)
1093 .unwrap();
1094 if let AssertionResult::Pass = result {
1095 } else {
1097 panic!("Expected Pass for regex match");
1098 }
1099 }
1100
1101 #[test]
1102 fn test_evaluate_matches_regex_fail() {
1103 let engine = AssertionEngine::new();
1104 let response = create_test_response();
1105
1106 let result = engine
1107 .evaluate(".name matches \"^xyz\"", &response, None, None)
1108 .unwrap();
1109 if let AssertionResult::Fail { .. } = result {
1110 } else {
1112 panic!("Expected Fail for regex match");
1113 }
1114 }
1115
1116 #[test]
1117 fn test_evaluate_nested_path() {
1118 let engine = AssertionEngine::new();
1119 let response = create_test_response();
1120
1121 let result = engine
1122 .evaluate(".nested.value == 42", &response, None, None)
1123 .unwrap();
1124 if let AssertionResult::Pass = result {
1125 } else {
1127 panic!("Expected Pass for nested path check");
1128 }
1129 }
1130
1131 #[test]
1132 fn test_evaluate_boolean_path() {
1133 let engine = AssertionEngine::new();
1134 let response = create_test_response();
1135
1136 let result = engine
1137 .evaluate(".active == true", &response, None, None)
1138 .unwrap();
1139 if let AssertionResult::Pass = result {
1140 } else {
1142 panic!("Expected Pass for boolean check");
1143 }
1144 }
1145
1146 #[test]
1147 fn test_evaluate_array_index() {
1148 let engine = AssertionEngine::new();
1149 let response = create_test_response();
1150
1151 let result = engine
1152 .evaluate(".tags[0] == \"a\"", &response, None, None)
1153 .unwrap();
1154 if let AssertionResult::Pass = result {
1155 } else {
1157 panic!("Expected Pass for array index check");
1158 }
1159 }
1160
1161 #[test]
1162 fn test_evaluate_unsupported_syntax() {
1163 let engine = AssertionEngine::new();
1164 let response = create_test_response();
1165
1166 let result = engine.evaluate("some_unknown_function()", &response, None, None);
1168 assert!(result.is_ok());
1170 }
1171
1172 #[test]
1173 fn test_evaluate_all() {
1174 let engine = AssertionEngine::new();
1175 let response = create_test_response();
1176
1177 let assertions = vec![".id == 123".to_string(), ".name == \"test\"".to_string()];
1178
1179 let results = engine.evaluate_all(&assertions, &response, None, None);
1180 assert_eq!(results.len(), 2);
1181 assert!(results.iter().all(|r| matches!(r, AssertionResult::Pass)));
1182 }
1183
1184 #[test]
1185 fn test_evaluate_all_with_failure() {
1186 let engine = AssertionEngine::new();
1187 let response = create_test_response();
1188
1189 let assertions = vec![".id == 123".to_string(), ".id == 999".to_string()];
1190
1191 let results = engine.evaluate_all(&assertions, &response, None, None);
1192 assert_eq!(results.len(), 2);
1193 assert!(matches!(&results[0], AssertionResult::Pass));
1194 assert!(matches!(&results[1], AssertionResult::Fail { .. }));
1195 }
1196
1197 #[test]
1198 fn test_evaluate_type_cast_number() {
1199 let engine = AssertionEngine::new();
1200 let response = json!({
1201 "price": 42
1202 });
1203
1204 let result = engine.evaluate(".price:number >= 0", &response, None, None);
1205 assert!(
1206 matches!(result, Ok(AssertionResult::Pass)),
1207 "Expected Pass, got: {:?}",
1208 result
1209 );
1210
1211 let result = engine.evaluate(".price:number < 0", &response, None, None);
1212 assert!(
1213 matches!(result, Ok(AssertionResult::Fail { .. })),
1214 "Expected Fail, got: {:?}",
1215 result
1216 );
1217 }
1218
1219 #[test]
1220 fn test_evaluate_type_cast_string() {
1221 let engine = AssertionEngine::new();
1222 let response = json!({
1223 "name": "hello world"
1224 });
1225
1226 let result = engine.evaluate(".name:string contains \"hello\"", &response, None, None);
1227 assert!(
1228 matches!(result, Ok(AssertionResult::Pass)),
1229 "Expected Pass, got: {:?}",
1230 result
1231 );
1232
1233 let result = engine.evaluate(".name:string startsWith \"he\"", &response, None, None);
1234 assert!(
1235 matches!(result, Ok(AssertionResult::Pass)),
1236 "Expected Pass, got: {:?}",
1237 result
1238 );
1239 }
1240
1241 #[test]
1242 fn test_evaluate_type_cast_is_noop() {
1243 let engine = AssertionEngine::new();
1244 let response = json!({
1245 "value": 123
1246 });
1247
1248 let without_cast = engine.evaluate(".value == 123", &response, None, None);
1250 let with_cast = engine.evaluate(".value:number == 123", &response, None, None);
1251 assert_eq!(
1252 matches!(without_cast, Ok(AssertionResult::Pass)),
1253 matches!(with_cast, Ok(AssertionResult::Pass)),
1254 "Type cast should not change evaluation result"
1255 );
1256 }
1257
1258 #[test]
1259 fn test_jq_fallback_truthy_non_bool_output() {
1260 let engine = AssertionEngine::new();
1263 let response = create_test_response();
1264
1265 let result = engine
1266 .evaluate(".tags | length", &response, None, None)
1267 .unwrap();
1268 assert!(
1269 matches!(result, AssertionResult::Pass),
1270 "Expected Pass, got: {:?}",
1271 result
1272 );
1273 }
1274
1275 #[test]
1276 fn test_jq_fallback_false_output_shows_value() {
1277 let engine = AssertionEngine::new();
1278 let response = create_test_response();
1279
1280 let result = engine
1282 .evaluate(".tags | length > 10", &response, None, None)
1283 .unwrap();
1284 if let AssertionResult::Fail { message, .. } = result {
1285 assert!(message.contains("false"), "message: {}", message);
1286 } else {
1287 panic!("Expected Fail, got: {:?}", result);
1288 }
1289 }
1290
1291 #[test]
1292 fn test_jq_fallback_null_output_fails() {
1293 let engine = AssertionEngine::new();
1294 let response = create_test_response();
1295
1296 let result = engine
1298 .evaluate(".missing_key | .", &response, None, None)
1299 .unwrap();
1300 assert!(
1301 matches!(result, AssertionResult::Fail { .. }),
1302 "Expected Fail, got: {:?}",
1303 result
1304 );
1305 }
1306
1307 #[test]
1308 fn test_query_jq_simple() {
1309 let engine = AssertionEngine::new();
1310 let response = create_test_response();
1311
1312 let results = engine.query(".id", &response).unwrap();
1313 assert_eq!(results.len(), 1);
1314 assert_eq!(results[0], json!(123));
1315 }
1316
1317 #[test]
1318 fn test_query_jq_nested() {
1319 let engine = AssertionEngine::new();
1320 let response = create_test_response();
1321
1322 let results = engine.query(".nested.value", &response).unwrap();
1323 assert_eq!(results.len(), 1);
1324 assert_eq!(results[0], json!(42));
1325 }
1326
1327 #[test]
1328 fn test_query_jq_array() {
1329 let engine = AssertionEngine::new();
1330 let response = create_test_response();
1331
1332 let results = engine.query(".tags[]", &response).unwrap();
1333 assert_eq!(results.len(), 3);
1334 assert_eq!(results[0], json!("a"));
1335 assert_eq!(results[1], json!("b"));
1336 assert_eq!(results[2], json!("c"));
1337 }
1338
1339 #[test]
1340 fn test_query_jq_filter() {
1341 let engine = AssertionEngine::new();
1342 let response = json!([1, 2, 3, 4, 5]);
1343
1344 let results = engine.query(".[] | select(. > 3)", &response).unwrap();
1345 assert_eq!(results.len(), 2);
1346 assert_eq!(results[0], json!(4));
1347 assert_eq!(results[1], json!(5));
1348 }
1349
1350 #[test]
1351 fn test_query_jq_length() {
1352 let engine = AssertionEngine::new();
1353 let response = create_test_response();
1354
1355 let results = engine.query(".tags | length", &response).unwrap();
1356 assert_eq!(results.len(), 1);
1357 assert_eq!(results[0], json!(3));
1358 }
1359
1360 #[test]
1361 fn test_query_invalid_expression() {
1362 let engine = AssertionEngine::new();
1363 let response = create_test_response();
1364
1365 let results = engine.query("invalid[[[", &response);
1366 assert!(results.is_err());
1367 }
1368
1369 #[test]
1370 fn test_jaq_to_json_dec_number() {
1371 let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("2.5".to_string())));
1372 assert_eq!(jaq_to_json(&dec), json!(2.5));
1373 }
1374
1375 #[test]
1376 fn test_jaq_to_json_invalid_dec_number() {
1377 let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("not-a-number".to_string())));
1378 assert_eq!(jaq_to_json(&dec), Value::Null);
1379 }
1380
1381 #[test]
1382 fn test_json_to_jaq_null() {
1383 let result = json_to_jaq(&json!(null));
1384 assert!(matches!(result, JaqVal::Null));
1385 }
1386
1387 #[test]
1388 fn test_json_to_jaq_bool() {
1389 let result = json_to_jaq(&json!(true));
1390 assert!(matches!(result, JaqVal::Bool(true)));
1391 }
1392
1393 #[test]
1394 fn test_json_to_jaq_number_int() {
1395 let result = json_to_jaq(&json!(42));
1396 assert!(matches!(result, JaqVal::Num(JaqNum::Int(42))));
1397 }
1398
1399 #[test]
1400 fn test_json_to_jaq_number_float() {
1401 let result = json_to_jaq(&json!(4.14));
1402 assert!(matches!(result, JaqVal::Num(JaqNum::Float(f)) if (f - 4.14).abs() < 0.001));
1403 }
1404
1405 #[test]
1406 fn test_json_to_jaq_string() {
1407 let result = json_to_jaq(&json!("hello"));
1408 assert!(matches!(result, JaqVal::TStr(_)));
1409 }
1410
1411 #[test]
1412 fn test_json_to_jaq_array() {
1413 let result = json_to_jaq(&json!([1, 2, 3]));
1414 assert!(matches!(result, JaqVal::Arr(_)));
1415 }
1416
1417 #[test]
1418 fn test_json_to_jaq_object() {
1419 let result = json_to_jaq(&json!({"key": "value"}));
1420 assert!(matches!(result, JaqVal::Obj(_)));
1421 }
1422
1423 #[test]
1424 fn test_jaq_filter_cache_returns_same_arc() {
1425 let expr = ".__cache_test_sentinel__";
1426 let first = AssertionEngine::get_or_compile_jaq_filter(expr).unwrap();
1427 let second = AssertionEngine::get_or_compile_jaq_filter(expr).unwrap();
1428 assert!(Arc::ptr_eq(&first, &second));
1429 }
1430 #[test]
1431 fn test_assertion_result_negate() {
1432 let pass = AssertionResult::Pass;
1433 assert!(matches!(pass.negate(), AssertionResult::Fail { .. }));
1434
1435 let fail = AssertionResult::fail("msg");
1436 assert!(matches!(fail.negate(), AssertionResult::Pass));
1437
1438 let error = AssertionResult::Error("err".into());
1439 assert!(matches!(error.negate(), AssertionResult::Error(_)));
1440 }
1441
1442 #[test]
1443 fn test_assertion_engine_get_failures() {
1444 let engine = AssertionEngine::new();
1445 let response = create_test_response();
1446 let assertions = vec![".id == 123".to_string(), ".id == 999".to_string()];
1447 let results = engine.evaluate_all(&assertions, &response, None, None);
1448 let failures = engine.get_failures(&results);
1449 assert_eq!(failures.len(), 1);
1450 }
1451
1452 #[test]
1453 fn test_assertion_engine_has_failures() {
1454 let engine = AssertionEngine::new();
1455 let response = create_test_response();
1456 let result = engine.evaluate_all(&[".id == 999".to_string()], &response, None, None);
1457 assert!(engine.has_failures(&result));
1458 }
1459
1460 #[test]
1461 fn test_assertion_engine_no_failures() {
1462 let engine = AssertionEngine::new();
1463 let response = create_test_response();
1464 let result = engine.evaluate_all(&[".id == 123".to_string()], &response, None, None);
1465 assert!(!engine.has_failures(&result));
1466 }
1467
1468 #[test]
1469 fn test_assertion_engine_default() {
1470 let engine = AssertionEngine::default();
1471 let response = create_test_response();
1472 let result = engine
1473 .evaluate(".id == 123", &response, None, None)
1474 .unwrap();
1475 assert!(matches!(result, AssertionResult::Pass));
1476 }
1477
1478 #[test]
1479 fn test_assertion_result_fail_with_diff_fields() {
1480 let result = AssertionResult::fail_with_diff("mismatch", "{\"a\":1}", "{\"a\":2}");
1481 match result {
1482 AssertionResult::Fail {
1483 message,
1484 expected,
1485 actual,
1486 } => {
1487 assert_eq!(message, "mismatch");
1488 assert_eq!(expected.unwrap(), "{\"a\":1}");
1489 assert_eq!(actual.unwrap(), "{\"a\":2}");
1490 }
1491 _ => panic!("Expected Fail"),
1492 }
1493 }
1494
1495 #[test]
1496 fn test_evaluate_url_scheme_parse_only() {
1497 use apif_ast::assertion_ast::{AssertionExpr, assertion_to_string, parse_assertion};
1498 let expr = parse_assertion("@url.scheme(\"https://example.com\") == \"https\"");
1499 assert!(
1500 !matches!(&expr, AssertionExpr::Raw(_)),
1501 "Expression should be parsed, not Raw: {:?}",
1502 expr
1503 );
1504 let s = assertion_to_string(&expr);
1505 assert_eq!(
1506 s, "@url.scheme(\"https://example.com\") == \"https\"",
1507 "Roundtrip failed"
1508 );
1509 }
1510
1511 #[test]
1512 fn test_rewrite_plugin_calls_basic() {
1513 assert_eq!(
1514 rewrite_plugin_calls("@len(.items) == .n").unwrap(),
1515 "__plugin(\"len\"; [.items]) == .n"
1516 );
1517 }
1518
1519 #[test]
1520 fn test_rewrite_plugin_calls_multiple_args() {
1521 assert_eq!(
1522 rewrite_plugin_calls("@regex(.name, \"^A\")").unwrap(),
1523 "__plugin(\"regex\"; [.name, \"^A\"])"
1524 );
1525 }
1526
1527 #[test]
1528 fn test_rewrite_plugin_calls_nested() {
1529 assert_eq!(
1530 rewrite_plugin_calls(".x | map(@is_email(.)) | all").unwrap(),
1531 ".x | map(__plugin(\"is_email\"; [.])) | all"
1532 );
1533 }
1534
1535 #[test]
1536 fn test_rewrite_plugin_calls_leaves_format_strings() {
1537 assert_eq!(
1539 rewrite_plugin_calls(".x | @base64").unwrap(),
1540 ".x | @base64"
1541 );
1542 }
1543
1544 #[test]
1545 fn test_rewrite_plugin_calls_ignores_at_in_string() {
1546 assert_eq!(
1548 rewrite_plugin_calls(".x == \"@len(a)\"").unwrap(),
1549 ".x == \"@len(a)\""
1550 );
1551 }
1552
1553 #[test]
1554 fn test_rewrite_plugin_calls_rejects_context_plugin() {
1555 let err = rewrite_plugin_calls("@header(\"x\") | length").unwrap_err();
1556 assert!(
1557 err.to_string().contains("not available in jq expressions"),
1558 "unexpected error: {}",
1559 err
1560 );
1561 }
1562
1563 #[test]
1564 fn test_jaq_context_plugin_reports_clear_error() {
1565 let engine = AssertionEngine::new();
1568 let response = json!({"x": 1});
1569 let result = engine
1570 .evaluate(".list | map(@header(\"y\")) | all", &response, None, None)
1571 .unwrap();
1572 let msg = match result {
1573 AssertionResult::Error(m) => m,
1574 AssertionResult::Fail { message, .. } => message,
1575 other => panic!("expected error/fail, got {:?}", other),
1576 };
1577 assert!(
1578 msg.contains("not available in jq expressions"),
1579 "unexpected message: {}",
1580 msg
1581 );
1582 }
1583}