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