1use anyhow::Result;
5use regex::Regex;
6use serde_json::Value;
7use std::cell::RefCell;
8use std::collections::HashMap;
9use std::rc::Rc;
10
11use crate::engine::AssertionResult;
12use crate::registry::{AssertionTiming, PluginContext, PluginRegistry, PluginResult};
13use apif_ast::assertion_ast::{AssertionExpr, BinaryOp, Expr, Literal, parse_assertion};
14fn normalize_plugin_name(name: &str) -> &str {
15 let trimmed = name.trim();
16 trimmed.strip_prefix('@').unwrap_or(trimmed)
17}
18
19type ValueResult = std::result::Result<Value, String>;
23
24fn regex_with_flags(pattern: &str, flags: &str) -> String {
27 let supported: String = flags
28 .chars()
29 .filter(|c| matches!(c, 'i' | 'm' | 's' | 'x' | 'u' | 'U'))
30 .collect();
31 if supported.is_empty() {
32 pattern.to_string()
33 } else {
34 format!("(?{}){}", supported, pattern)
35 }
36}
37
38thread_local! {
39 static REGEX_CACHE: RefCell<HashMap<String, std::result::Result<Rc<Regex>, String>>> =
40 RefCell::new(HashMap::new());
41}
42
43fn cached_regex(pattern: &str) -> std::result::Result<Rc<Regex>, String> {
44 if let Some(cached) = REGEX_CACHE.with(|cache| cache.borrow().get(pattern).cloned()) {
45 return cached;
46 }
47
48 let compiled = Regex::new(pattern)
49 .map(Rc::new)
50 .map_err(|err| err.to_string());
51
52 REGEX_CACHE.with(|cache| {
53 cache
54 .borrow_mut()
55 .insert(pattern.to_string(), compiled.clone());
56 });
57
58 compiled
59}
60
61pub fn evaluate_assertion(
65 registry: &dyn PluginRegistry,
66 assertion: &str,
67 response: &Value,
68 headers: Option<&HashMap<String, String>>,
69 trailers: Option<&HashMap<String, String>>,
70 timing: Option<&AssertionTiming>,
71 variables: &HashMap<String, Value>,
72) -> Result<Option<AssertionResult>> {
73 let trimmed = assertion.trim();
74 if trimmed.is_empty() {
75 return Ok(None);
76 }
77
78 let ast = parse_assertion(trimmed);
79 match &ast {
80 AssertionExpr::Raw(_) => Ok(None),
81 _ => evaluate_ast(
82 registry, &ast, response, headers, trailers, timing, variables,
83 )
84 .map(Some),
85 }
86}
87
88fn evaluate_ast(
89 pm: &dyn PluginRegistry,
90 expr: &AssertionExpr,
91 response: &Value,
92 headers: Option<&HashMap<String, String>>,
93 trailers: Option<&HashMap<String, String>>,
94 timing: Option<&AssertionTiming>,
95 variables: &HashMap<String, Value>,
96) -> Result<AssertionResult> {
97 match expr {
98 AssertionExpr::Not(inner) => {
99 let r = evaluate_ast(pm, inner, response, headers, trailers, timing, variables)?;
100 Ok(negate(r))
101 }
102 AssertionExpr::NotNot(inner) => {
103 evaluate_ast(pm, inner, response, headers, trailers, timing, variables)
104 }
105 AssertionExpr::And { left, right } => {
106 let lr = evaluate_ast(pm, left, response, headers, trailers, timing, variables)?;
107 if !is_pass(&lr) {
108 return Ok(AssertionResult::fail(format!(
109 "Left of 'and' failed: {}",
110 fmt_result_short(&lr)
111 )));
112 }
113 let rr = evaluate_ast(pm, right, response, headers, trailers, timing, variables)?;
114 if !is_pass(&rr) {
115 return Ok(AssertionResult::fail(format!(
116 "Right of 'and' failed: {}",
117 fmt_result_short(&rr)
118 )));
119 }
120 Ok(AssertionResult::Pass)
121 }
122 AssertionExpr::Or { left, right } => {
123 let lr = evaluate_ast(pm, left, response, headers, trailers, timing, variables)?;
124 if is_pass(&lr) {
125 return Ok(AssertionResult::Pass);
126 }
127 let rr = evaluate_ast(pm, right, response, headers, trailers, timing, variables)?;
128 if is_pass(&rr) {
129 return Ok(AssertionResult::Pass);
130 }
131 Ok(AssertionResult::fail(format!(
132 "Both sides of 'or' failed: left={}, right={}",
133 fmt_result_short(&lr),
134 fmt_result_short(&rr)
135 )))
136 }
137 AssertionExpr::Xor { left, right } => {
138 let lr = evaluate_ast(pm, left, response, headers, trailers, timing, variables)?;
139 let rr = evaluate_ast(pm, right, response, headers, trailers, timing, variables)?;
140 let lp = is_pass(&lr);
141 let rp = is_pass(&rr);
142 if lp != rp {
143 Ok(AssertionResult::Pass)
144 } else {
145 Ok(AssertionResult::fail(format!(
146 "Xor expects exactly one true, got left={} right={}",
147 lp, rp
148 )))
149 }
150 }
151 AssertionExpr::Binary { op, left, right } => {
152 let lhs = match eval_value(pm, left, response, headers, trailers, timing, variables) {
153 Ok(v) => v,
154 Err(e) => return Ok(AssertionResult::Error(e)),
155 };
156 let rhs = match eval_value(pm, right, response, headers, trailers, timing, variables) {
157 Ok(v) => v,
158 Err(e) => return Ok(AssertionResult::Error(e)),
159 };
160 compare(lhs, op, rhs, left, right)
161 }
162 AssertionExpr::Paren(inner) => {
163 evaluate_ast(pm, inner, response, headers, trailers, timing, variables)
164 }
165 AssertionExpr::IfThenElse {
166 condition,
167 then_branch,
168 else_branch,
169 } => {
170 let cond = evaluate_ast(
171 pm, condition, response, headers, trailers, timing, variables,
172 )?;
173 if is_pass(&cond) {
174 evaluate_ast(
175 pm,
176 then_branch,
177 response,
178 headers,
179 trailers,
180 timing,
181 variables,
182 )
183 } else {
184 evaluate_ast(
185 pm,
186 else_branch,
187 response,
188 headers,
189 trailers,
190 timing,
191 variables,
192 )
193 }
194 }
195 AssertionExpr::Atom(_) => {
196 if let AssertionExpr::Atom(Expr::PluginCall { name, args }) = expr {
197 eval_plugin_as_assertion(
198 pm, name, args, response, headers, trailers, timing, variables,
199 )
200 } else {
201 let val = match eval_value(pm, expr, response, headers, trailers, timing, variables)
202 {
203 Ok(v) => v,
204 Err(e) => return Ok(AssertionResult::Error(e)),
205 };
206 if is_truthy(&val) {
207 Ok(AssertionResult::Pass)
208 } else {
209 Ok(AssertionResult::fail(format!(
210 "Expression evaluated to falsy: {:?}",
211 val
212 )))
213 }
214 }
215 }
216 AssertionExpr::Raw(_) => Ok(AssertionResult::Error("Unparsed expression".into())),
217 }
218}
219
220fn validate_type_cast(val: &Value, type_name: &str) -> Value {
224 let valid = match type_name {
225 "bool" => val.is_boolean(),
226 "uint" => val.as_u64().is_some(),
227 "number" => val.is_number(),
228 "string" | "uuid" | "email" | "url" | "ip" => val.is_string(),
229 "time" | "timestamp" | "duration" => val.is_string() || val.is_number(),
230 "json" => val.is_object() || val.is_array(),
231 "yaml" => val.is_string(),
232 _ => true,
233 };
234 if valid { val.clone() } else { Value::Null }
235}
236
237#[expect(clippy::too_many_arguments)]
238fn eval_plugin_as_assertion(
239 pm: &dyn PluginRegistry,
240 name: &str,
241 args: &[AssertionExpr],
242 response: &Value,
243 headers: Option<&HashMap<String, String>>,
244 trailers: Option<&HashMap<String, String>>,
245 timing: Option<&AssertionTiming>,
246 variables: &HashMap<String, Value>,
247) -> Result<AssertionResult> {
248 let func_name = format!("@{}", name);
249 let resolved_name = normalize_plugin_name(&func_name);
250 if let Some(plugin) = pm.get_plugin(resolved_name) {
251 let ctx = PluginContext::new(response)
252 .with_headers(headers)
253 .with_trailers(trailers)
254 .with_timing(timing);
255 let arg_values: Vec<Value> = match args
256 .iter()
257 .map(|a| eval_value(pm, a, response, headers, trailers, timing, variables))
258 .collect::<std::result::Result<_, _>>()
259 {
260 Ok(values) => values,
261 Err(e) => return Ok(AssertionResult::Error(e)),
262 };
263 match plugin.execute(&arg_values, &ctx) {
264 Ok(PluginResult::Assertion(res)) => Ok(res),
265 Ok(PluginResult::Value(val)) => {
266 if is_truthy(&val) {
267 Ok(AssertionResult::Pass)
268 } else {
269 Ok(AssertionResult::fail(format!(
270 "Plugin {} returned falsy value: {:?}",
271 resolved_name, val
272 )))
273 }
274 }
275 Err(e) => Ok(AssertionResult::Error(format!("Plugin error: {}", e))),
276 }
277 } else {
278 Ok(AssertionResult::Error(format!("Unknown plugin: {}", name)))
279 }
280}
281
282fn eval_value(
283 pm: &dyn PluginRegistry,
284 expr: &AssertionExpr,
285 response: &Value,
286 headers: Option<&HashMap<String, String>>,
287 trailers: Option<&HashMap<String, String>>,
288 timing: Option<&AssertionTiming>,
289 variables: &HashMap<String, Value>,
290) -> ValueResult {
291 match expr {
292 AssertionExpr::Atom(atom) => {
293 eval_atom(pm, atom, response, headers, trailers, timing, variables)
294 }
295 AssertionExpr::Paren(inner) => {
296 eval_value(pm, inner, response, headers, trailers, timing, variables)
297 }
298 AssertionExpr::Not(inner) => {
299 let v = eval_value(pm, inner, response, headers, trailers, timing, variables)?;
300 Ok(Value::Bool(!is_truthy(&v)))
301 }
302 AssertionExpr::NotNot(inner) => {
303 eval_value(pm, inner, response, headers, trailers, timing, variables)
304 }
305 AssertionExpr::And { left, right } => {
306 let lv = eval_value(pm, left, response, headers, trailers, timing, variables)?;
307 if !is_truthy(&lv) {
308 return Ok(Value::Bool(false));
309 }
310 let rv = eval_value(pm, right, response, headers, trailers, timing, variables)?;
311 Ok(Value::Bool(is_truthy(&rv)))
312 }
313 AssertionExpr::Or { left, right } => {
314 let lv = eval_value(pm, left, response, headers, trailers, timing, variables)?;
315 if is_truthy(&lv) {
316 return Ok(Value::Bool(true));
317 }
318 let rv = eval_value(pm, right, response, headers, trailers, timing, variables)?;
319 Ok(Value::Bool(is_truthy(&rv)))
320 }
321 AssertionExpr::Xor { left, right } => {
322 let lv = eval_value(pm, left, response, headers, trailers, timing, variables)?;
323 let rv = eval_value(pm, right, response, headers, trailers, timing, variables)?;
324 Ok(Value::Bool(is_truthy(&lv) != is_truthy(&rv)))
325 }
326 AssertionExpr::Binary { op, left, right } => {
327 let lhs = eval_value(pm, left, response, headers, trailers, timing, variables)?;
328 let rhs = eval_value(pm, right, response, headers, trailers, timing, variables)?;
329 Ok(eval_binary_value(lhs, op, rhs))
330 }
331 AssertionExpr::IfThenElse {
332 condition,
333 then_branch,
334 else_branch,
335 } => {
336 let cv = eval_value(
337 pm, condition, response, headers, trailers, timing, variables,
338 )?;
339 if is_truthy(&cv) {
340 eval_value(
341 pm,
342 then_branch,
343 response,
344 headers,
345 trailers,
346 timing,
347 variables,
348 )
349 } else {
350 eval_value(
351 pm,
352 else_branch,
353 response,
354 headers,
355 trailers,
356 timing,
357 variables,
358 )
359 }
360 }
361 AssertionExpr::Raw(s) => Ok(resolve_path(s, response)),
362 }
363}
364
365fn eval_atom(
366 pm: &dyn PluginRegistry,
367 atom: &Expr,
368 response: &Value,
369 headers: Option<&HashMap<String, String>>,
370 trailers: Option<&HashMap<String, String>>,
371 timing: Option<&AssertionTiming>,
372 variables: &HashMap<String, Value>,
373) -> ValueResult {
374 match atom {
375 Expr::JqPath(p) => Ok(resolve_path(p, response)),
376 Expr::PluginCall { name, args } => {
377 let func_name = format!("@{}", name);
378 let resolved_name = normalize_plugin_name(&func_name);
379 if let Some(plugin) = pm.get_plugin(resolved_name) {
380 let ctx = PluginContext::new(response)
381 .with_headers(headers)
382 .with_trailers(trailers)
383 .with_timing(timing);
384 let arg_values: Vec<Value> = args
385 .iter()
386 .map(|a| eval_value(pm, a, response, headers, trailers, timing, variables))
387 .collect::<std::result::Result<_, _>>()?;
388 match plugin.execute(&arg_values, &ctx) {
389 Ok(PluginResult::Value(v)) => Ok(v),
390 Ok(PluginResult::Assertion(AssertionResult::Pass)) => Ok(Value::Bool(true)),
391 Ok(PluginResult::Assertion(AssertionResult::Fail { .. })) => {
392 Ok(Value::Bool(false))
393 }
394 Ok(PluginResult::Assertion(AssertionResult::Error(e))) => {
397 Err(format!("Plugin {} error: {}", resolved_name, e))
398 }
399 Err(e) => Err(format!("Plugin {} error: {}", resolved_name, e)),
400 }
401 } else {
402 Ok(Value::Null)
403 }
404 }
405 Expr::Literal(lit) => Ok(match lit {
406 Literal::Bool(b) => Value::Bool(*b),
407 Literal::Number(n) => n
408 .parse::<i64>()
409 .map(|i| Value::Number(serde_json::Number::from(i)))
410 .unwrap_or_else(|_| {
411 n.parse::<f64>()
412 .ok()
413 .and_then(serde_json::Number::from_f64)
414 .map(Value::Number)
415 .unwrap_or(Value::Null)
416 }),
417 Literal::Str(s) => Value::String(s.clone()),
418 Literal::Null => Value::Null,
419 }),
420 Expr::Variable(name) => match variables.get(name.as_str()) {
421 Some(v) => Ok(v.clone()),
423 None => Err(format!("Undefined variable: ${}", name)),
424 },
425 Expr::RegExp { pattern, flags } => Ok(Value::String(regex_with_flags(pattern, flags))),
426 Expr::Json(s) | Expr::Yaml(s) => Ok(serde_json::from_str(s).unwrap_or(Value::Null)),
427 Expr::As(inner, type_name) => {
428 let val = eval_atom(pm, inner, response, headers, trailers, timing, variables)?;
429 Ok(validate_type_cast(&val, type_name))
430 }
431 }
432}
433
434fn values_numerically_equal(lhs: &Value, rhs: &Value) -> bool {
438 if let (Value::Number(l), Value::Number(r)) = (lhs, rhs) {
439 if let (Some(li), Some(ri)) = (l.as_i64(), r.as_i64()) {
440 return li == ri;
441 }
442 if let (Some(lu), Some(ru)) = (l.as_u64(), r.as_u64()) {
443 return lu == ru;
444 }
445 if let (Some(lf), Some(rf)) = (l.as_f64(), r.as_f64()) {
448 return lf == rf;
449 }
450 return l == r;
451 }
452 lhs == rhs
453}
454
455fn eval_binary_value(lhs: Value, op: &BinaryOp, rhs: Value) -> Value {
456 let pass = match op {
457 BinaryOp::Eq => values_numerically_equal(&lhs, &rhs),
458 BinaryOp::Ne => !values_numerically_equal(&lhs, &rhs),
459 BinaryOp::Gt => compare_numeric(&lhs, &rhs, ">").unwrap_or(false),
460 BinaryOp::Lt => compare_numeric(&lhs, &rhs, "<").unwrap_or(false),
461 BinaryOp::Ge => compare_numeric(&lhs, &rhs, ">=").unwrap_or(false),
462 BinaryOp::Le => compare_numeric(&lhs, &rhs, "<=").unwrap_or(false),
463 BinaryOp::Contains => match (&lhs, &rhs) {
464 (Value::String(l), Value::String(r)) => l.contains(r),
465 (Value::Array(l), r) => l.contains(r),
466 (Value::Object(l), Value::String(r)) => l.contains_key(r),
467 _ => false,
468 },
469 BinaryOp::StartsWith => match (&lhs, &rhs) {
470 (Value::String(l), Value::String(r)) => l.starts_with(r),
471 _ => false,
472 },
473 BinaryOp::EndsWith => match (&lhs, &rhs) {
474 (Value::String(l), Value::String(r)) => l.ends_with(r),
475 _ => false,
476 },
477 BinaryOp::Matches => match (&lhs, &rhs) {
478 (Value::String(l), Value::String(r)) => cached_regex(r).is_ok_and(|re| re.is_match(l)),
479 _ => false,
480 },
481 };
482 Value::Bool(pass)
483}
484
485fn compare(
486 lhs: Value,
487 op: &BinaryOp,
488 rhs: Value,
489 left_expr: &AssertionExpr,
490 right_expr: &AssertionExpr,
491) -> Result<AssertionResult> {
492 if let BinaryOp::Matches = op
493 && let (Value::String(_l), Value::String(r)) = (&lhs, &rhs)
494 && cached_regex(r).is_err()
495 {
496 return Ok(AssertionResult::Error(format!("Invalid regex: {}", r)));
497 }
498 let pass = eval_binary_value(lhs.clone(), op, rhs.clone());
499 if pass == Value::Bool(true) {
500 Ok(AssertionResult::Pass)
501 } else {
502 Ok(AssertionResult::Fail {
503 message: format!(
504 "Assertion failed: {} {} {} (Values: {:?} vs {:?})",
505 left_expr,
506 op.as_str(),
507 right_expr,
508 lhs,
509 rhs
510 ),
511 expected: Some(format!("{} {:?}", op.as_str(), rhs)),
512 actual: Some(format!("{:?}", lhs)),
513 })
514 }
515}
516
517fn compare_numeric(lhs: &Value, rhs: &Value, op: &str) -> Option<bool> {
518 let lhs_num = lhs.as_number()?;
519 let rhs_num = rhs.as_number()?;
520
521 let lhs_i = lhs_num
522 .as_i64()
523 .map(i128::from)
524 .or_else(|| lhs_num.as_u64().map(i128::from));
525 let rhs_i = rhs_num
526 .as_i64()
527 .map(i128::from)
528 .or_else(|| rhs_num.as_u64().map(i128::from));
529
530 if let (Some(l), Some(r)) = (lhs_i, rhs_i) {
531 return Some(match op {
532 ">" => l > r,
533 "<" => l < r,
534 ">=" => l >= r,
535 "<=" => l <= r,
536 _ => return None,
537 });
538 }
539
540 let (l, r) = (lhs_num.as_f64()?, rhs_num.as_f64()?);
541 Some(match op {
542 ">" => l > r,
543 "<" => l < r,
544 ">=" => l >= r,
545 "<=" => l <= r,
546 _ => return None,
547 })
548}
549
550fn resolve_path(path: &str, root: &Value) -> Value {
551 if path == "." {
552 return root.clone();
553 }
554 if path.is_empty() {
555 return Value::Null;
556 }
557 if !path.starts_with('.') && !path.starts_with('$') {
558 return Value::String(path.to_string());
559 }
560 eval_jaq_one(path, root).unwrap_or(Value::Null)
561}
562
563fn eval_jaq_one(expr: &str, input: &Value) -> anyhow::Result<Value> {
564 super::engine::AssertionEngine::eval_jaq_one(expr, input)
565}
566
567fn is_truthy(val: &Value) -> bool {
568 !val.is_null() && val != &Value::Bool(false)
569}
570
571fn is_pass(r: &AssertionResult) -> bool {
572 matches!(r, AssertionResult::Pass)
573}
574
575fn negate(r: AssertionResult) -> AssertionResult {
576 r.negate()
577}
578
579fn fmt_result_short(r: &AssertionResult) -> String {
580 match r {
581 AssertionResult::Pass => "pass".into(),
582 AssertionResult::Fail { message, .. } => message.clone(),
583 AssertionResult::Error(e) => format!("error: {}", e),
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use serde_json::json;
591
592 fn pm() -> crate::registry::NoopPluginRegistry {
593 crate::registry::NoopPluginRegistry
594 }
595
596 fn eval(pm: &dyn PluginRegistry, expr: &str, response: &Value) -> AssertionResult {
597 evaluate_assertion(pm, expr, response, None, None, None, &HashMap::new())
598 .unwrap()
599 .unwrap_or(AssertionResult::Error("AST returned None".into()))
600 }
601
602 fn eval_with_vars(
603 pm: &dyn PluginRegistry,
604 expr: &str,
605 response: &Value,
606 variables: &HashMap<String, Value>,
607 ) -> AssertionResult {
608 evaluate_assertion(pm, expr, response, None, None, None, variables)
609 .unwrap()
610 .unwrap_or(AssertionResult::Error("AST returned None".into()))
611 }
612
613 #[test]
614 fn test_equality_pass() {
615 let r = eval(
616 &pm(),
617 ".status == \"success\"",
618 &json!({"status": "success"}),
619 );
620 assert!(matches!(r, AssertionResult::Pass));
621 }
622
623 #[test]
624 fn test_equality_fail() {
625 let r = eval(&pm(), ".status == \"error\"", &json!({"status": "success"}));
626 assert!(matches!(r, AssertionResult::Fail { .. }));
627 }
628
629 #[test]
630 fn test_contains() {
631 let r = eval(&pm(), ".name contains \"te\"", &json!({"name": "test"}));
632 assert!(matches!(r, AssertionResult::Pass));
633 }
634
635 #[test]
636 fn test_xor_both_true() {
637 let r = eval(&pm(), ".x == 1 xor .y == 2", &json!({"x": 1, "y": 2}));
638 assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
639 }
640
641 #[test]
642 fn test_xor_both_false() {
643 let r = eval(&pm(), ".x == 9 xor .y == 9", &json!({"x": 1, "y": 2}));
644 assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
645 }
646
647 #[test]
648 fn test_numeric_greater() {
649 let r = eval(&pm(), ".id > 100", &json!({"id": 123}));
650 assert!(matches!(r, AssertionResult::Pass));
651 }
652
653 #[test]
654 fn test_numeric_less() {
655 let r = eval(&pm(), ".id < 200", &json!({"id": 123}));
656 assert!(matches!(r, AssertionResult::Pass));
657 }
658
659 #[test]
660 fn test_matches_regex() {
661 let r = eval(&pm(), ".name matches \"^te.*t$\"", &json!({"name": "test"}));
662 assert!(matches!(r, AssertionResult::Pass));
663 }
664
665 #[test]
666 fn test_matches_regex_fail() {
667 let r = eval(&pm(), ".name matches \"^xyz\"", &json!({"name": "test"}));
668 assert!(matches!(r, AssertionResult::Fail { .. }));
669 }
670
671 #[test]
672 fn test_jq_fallback_via_raw() {
673 let p = pm();
674 let r = evaluate_assertion(
675 &p,
676 ".tags | length",
677 &json!({"tags": [1, 2, 3]}),
678 None,
679 None,
680 None,
681 &HashMap::new(),
682 )
683 .unwrap();
684 assert!(
685 r.is_none(),
686 "JQ pipe should return None to trigger JQ fallback"
687 );
688 }
689
690 #[test]
691 fn test_resolve_path_simple() {
692 let r = resolve_path(".key", &json!({"key": "value"}));
693 assert_eq!(r, json!("value"));
694 }
695
696 #[test]
697 fn test_resolve_path_nested() {
698 let r = resolve_path(".outer.inner", &json!({"outer": {"inner": "value"}}));
699 assert_eq!(r, json!("value"));
700 }
701
702 #[test]
703 fn test_resolve_path_array_index() {
704 let r = resolve_path(".items[0]", &json!({"items": ["first", "second"]}));
705 assert_eq!(r, json!("first"));
706 }
707
708 #[test]
709 fn test_resolve_path_missing_key() {
710 let r = resolve_path(".missing", &json!({"a": 1}));
711 assert!(r.is_null());
712 }
713
714 #[test]
715 fn test_compare_numeric_greater() {
716 assert_eq!(compare_numeric(&json!(5), &json!(3), ">"), Some(true));
717 }
718
719 #[test]
720 fn test_compare_numeric_less() {
721 assert_eq!(compare_numeric(&json!(3), &json!(5), "<"), Some(true));
722 }
723
724 #[test]
725 fn test_compare_numeric_equality() {
726 assert_eq!(compare_numeric(&json!(5), &json!(5), ">="), Some(true));
727 assert_eq!(compare_numeric(&json!(5), &json!(5), "<="), Some(true));
728 }
729
730 #[test]
731 fn test_compare_numeric_mixed_types() {
732 assert_eq!(compare_numeric(&json!(5), &json!("5"), ">"), None);
733 }
734
735 #[test]
736 fn test_cached_regex_valid() {
737 assert!(cached_regex(r"\d+").is_ok());
738 }
739
740 #[test]
741 fn test_cached_regex_invalid() {
742 assert!(cached_regex(r"[").is_err());
743 }
744
745 #[test]
746 fn test_validate_type_cast() {
747 use serde_json::json;
748 assert_eq!(validate_type_cast(&json!(42), "number"), json!(42));
749 assert_eq!(validate_type_cast(&json!("hello"), "number"), Value::Null);
750 assert_eq!(
751 validate_type_cast(&json!("hello"), "string"),
752 json!("hello")
753 );
754 assert_eq!(validate_type_cast(&json!(42), "string"), Value::Null);
755 assert_eq!(validate_type_cast(&json!(true), "bool"), json!(true));
756 assert_eq!(validate_type_cast(&json!("hello"), "bool"), Value::Null);
757 assert_eq!(validate_type_cast(&json!(42u64), "uint"), json!(42u64));
758 assert_eq!(validate_type_cast(&json!(-1), "uint"), Value::Null);
759 assert_eq!(
760 validate_type_cast(&json!("uuid-str"), "uuid"),
761 json!("uuid-str")
762 );
763 assert_eq!(
764 validate_type_cast(&json!("email@x.com"), "email"),
765 json!("email@x.com")
766 );
767 assert_eq!(validate_type_cast(&json!("url"), "url"), json!("url"));
768 assert_eq!(
769 validate_type_cast(&json!("1.2.3.4"), "ip"),
770 json!("1.2.3.4")
771 );
772 assert_eq!(
773 validate_type_cast(&json!("2024-01-01"), "time"),
774 json!("2024-01-01")
775 );
776 assert_eq!(validate_type_cast(&json!(12345), "timestamp"), json!(12345));
777 assert_eq!(
778 validate_type_cast(&json!("100ms"), "duration"),
779 json!("100ms")
780 );
781 assert_eq!(
782 validate_type_cast(&json!({"k": "v"}), "json"),
783 json!({"k": "v"})
784 );
785 assert_eq!(validate_type_cast(&json!([1, 2]), "json"), json!([1, 2]));
786 assert_eq!(validate_type_cast(&json!("hello"), "json"), Value::Null);
787 assert_eq!(
788 validate_type_cast(&json!("yaml:val"), "yaml"),
789 json!("yaml:val")
790 );
791 assert_eq!(
792 validate_type_cast(&json!("any_val"), "unknown_type"),
793 json!("any_val")
794 );
795 }
796
797 #[test]
798 fn test_normalize_plugin_name_assert() {
799 assert_eq!(normalize_plugin_name("@uuid"), "uuid");
800 assert_eq!(normalize_plugin_name("uuid"), "uuid");
801 assert_eq!(normalize_plugin_name(" @uuid "), "uuid");
802 }
803
804 #[test]
805 fn test_is_truthy() {
806 assert!(!is_truthy(&Value::Null));
807 assert!(!is_truthy(&Value::Bool(false)));
808 assert!(is_truthy(&Value::Bool(true)));
809 assert!(is_truthy(&Value::Number(0.into())));
810 assert!(is_truthy(&Value::String("".into())));
811 }
812
813 #[test]
814 fn test_negate() {
815 let pass = AssertionResult::Pass;
816 assert!(matches!(negate(pass), AssertionResult::Fail { .. }));
817
818 let fail = AssertionResult::fail("msg");
819 assert!(matches!(negate(fail), AssertionResult::Pass));
820
821 let err = AssertionResult::Error("err".into());
822 assert!(matches!(negate(err), AssertionResult::Error(_)));
823 }
824
825 #[test]
826 fn test_fmt_result_short() {
827 assert_eq!(fmt_result_short(&AssertionResult::Pass), "pass");
828 assert_eq!(fmt_result_short(&AssertionResult::fail("msg")), "msg");
829 assert_eq!(
830 fmt_result_short(&AssertionResult::Error("err".into())),
831 "error: err"
832 );
833 }
834
835 #[test]
836 fn test_eval_atom_literal() {
837 let pm = crate::registry::NoopPluginRegistry;
838 let ctx = &json!({});
839 use apif_ast::assertion_ast::{Expr, Literal};
840 let result = eval_atom(
841 &pm,
842 &Expr::Literal(Literal::Number("42".into())),
843 ctx,
844 None,
845 None,
846 None,
847 &HashMap::new(),
848 )
849 .unwrap();
850 assert_eq!(result, json!(42));
851 }
852
853 #[test]
854 fn test_regex_with_flags() {
855 assert_eq!(regex_with_flags("^te.*t$", ""), "^te.*t$");
856 assert_eq!(regex_with_flags("^te.*t$", "i"), "(?i)^te.*t$");
857 assert_eq!(regex_with_flags("^te.*t$", "im"), "(?im)^te.*t$");
858 assert_eq!(regex_with_flags("^te.*t$", "gi"), "(?i)^te.*t$");
860 assert_eq!(regex_with_flags("^te.*t$", "g"), "^te.*t$");
861 }
862
863 #[test]
864 fn test_matches_regex_honors_case_insensitive_flag() {
865 use apif_ast::assertion_ast::{BinaryOp, Expr};
866 let expr = AssertionExpr::Binary {
867 op: BinaryOp::Matches,
868 left: Box::new(AssertionExpr::Atom(Expr::JqPath(".name".into()))),
869 right: Box::new(AssertionExpr::Atom(Expr::RegExp {
870 pattern: "^TE.*T$".into(),
871 flags: "i".into(),
872 })),
873 };
874 let response = json!({"name": "test"});
875 let r = evaluate_ast(&pm(), &expr, &response, None, None, None, &HashMap::new()).unwrap();
876 assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
877
878 let expr = AssertionExpr::Binary {
880 op: BinaryOp::Matches,
881 left: Box::new(AssertionExpr::Atom(Expr::JqPath(".name".into()))),
882 right: Box::new(AssertionExpr::Atom(Expr::RegExp {
883 pattern: "^TE.*T$".into(),
884 flags: String::new(),
885 })),
886 };
887 let r = evaluate_ast(&pm(), &expr, &response, None, None, None, &HashMap::new()).unwrap();
888 assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
889 }
890
891 struct ErrorPlugin;
892
893 impl crate::registry::PluginApi for ErrorPlugin {
894 fn execute(
895 &self,
896 _args: &[Value],
897 _context: &PluginContext,
898 ) -> anyhow::Result<crate::registry::PluginResult> {
899 Ok(crate::registry::PluginResult::Assertion(
900 AssertionResult::Error("boom".into()),
901 ))
902 }
903 }
904
905 struct ErrorPluginRegistry;
906
907 impl PluginRegistry for ErrorPluginRegistry {
908 fn get_plugin(&self, name: &str) -> Option<std::sync::Arc<dyn crate::registry::PluginApi>> {
909 (name == "err").then(|| {
910 std::sync::Arc::new(ErrorPlugin) as std::sync::Arc<dyn crate::registry::PluginApi>
911 })
912 }
913 }
914
915 #[test]
916 fn test_plugin_error_in_value_position_propagates() {
917 let r = eval(
920 &ErrorPluginRegistry,
921 "@err(.x) == \"error: boom\"",
922 &json!({"x": 1}),
923 );
924 assert!(matches!(r, AssertionResult::Error(_)), "got: {:?}", r);
925
926 let r = eval(&ErrorPluginRegistry, "@err(.x) != 1", &json!({"x": 1}));
928 assert!(matches!(r, AssertionResult::Error(_)), "got: {:?}", r);
929 }
930
931 #[test]
932 fn test_eval_binary_value_num() {
933 use apif_ast::assertion_ast::BinaryOp;
934 assert_eq!(
935 eval_binary_value(json!(5), &BinaryOp::Gt, json!(3)),
936 json!(true)
937 );
938 assert_eq!(
939 eval_binary_value(json!(3), &BinaryOp::Gt, json!(5)),
940 json!(false)
941 );
942 }
943
944 #[test]
945 fn test_eq_is_exact_on_large_integers() {
946 let r = eval(
949 &pm(),
950 ".id == 9223372036854775807",
951 &json!({"id": 9223372036854775806i64}),
952 );
953 assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
954
955 let r = eval(
957 &pm(),
958 ".id == 9223372036854775807",
959 &json!({"id": 9223372036854775807i64}),
960 );
961 assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
962
963 let r = eval(
965 &pm(),
966 ".id != 9223372036854775807",
967 &json!({"id": 9223372036854775806i64}),
968 );
969 assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
970 }
971
972 #[test]
973 fn test_extract_variable_resolves_in_assertion() {
974 let mut vars = HashMap::new();
977 vars.insert("price".to_string(), json!(42));
978 let r = eval_with_vars(&pm(), "$price >= 0", &json!({}), &vars);
979 assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
980
981 vars.insert("price".to_string(), json!(-5));
982 let r = eval_with_vars(&pm(), "$price >= 0", &json!({}), &vars);
983 assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
984 }
985
986 #[test]
987 fn test_extract_variable_string_contains() {
988 let mut vars = HashMap::new();
989 vars.insert("name".to_string(), json!("hello world"));
990 let r = eval_with_vars(&pm(), "$name contains \"hello\"", &json!({}), &vars);
991 assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
992 }
993
994 #[test]
995 fn test_unbound_variable_errors() {
996 let vars = HashMap::new();
997 let r = eval_with_vars(&pm(), "$missing >= 0", &json!({}), &vars);
998 match r {
999 AssertionResult::Error(msg) => assert!(msg.contains("missing"), "msg: {}", msg),
1000 other => panic!("expected Error for unbound variable, got: {:?}", other),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_eq_int_vs_float_still_equal_by_value() {
1006 let r = eval(&pm(), ".x == 3.0", &json!({"x": 3}));
1007 assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1008 let r = eval(&pm(), ".x == 3", &json!({"x": 3.0}));
1009 assert!(matches!(r, AssertionResult::Pass), "got: {:?}", r);
1010 }
1011}