• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 24905977248

24 Apr 2026 06:40PM UTC coverage: 78.024% (+0.3%) from 77.729%
24905977248

Pull #43

github

web-flow
Merge fca25a9f8 into 017e47d15
Pull Request #43: new command gen grpcurl & call

741 of 993 new or added lines in 24 files covered. (74.62%)

2 existing lines in 2 files now uncovered.

19595 of 25114 relevant lines covered (78.02%)

39580.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.55
/src/assert/engine.rs
1
// Assertion engine using embedded jaq and operators fallback
2

3
use anyhow::Result;
4
use serde_json::Value;
5
use std::collections::HashMap;
6
use std::sync::Arc;
7
use std::sync::{LazyLock, Mutex};
8

9
// Plugin imports
10
use crate::plugins::AssertionTiming;
11
use crate::plugins::PluginManager;
12

13
// Jaq imports
14
use jaq_core::{Compiler, Ctx, Vars, data, load, unwrap_valr};
15
use jaq_json::{Map as JaqMap, Num as JaqNum, Rc as JaqRc, Val as JaqVal};
16

17
// Operators module
18
use super::operators;
19

20
/// Assertion result
21
#[derive(Debug, Clone, PartialEq, Eq)]
22
pub enum AssertionResult {
23
    Pass,
24
    Fail {
25
        message: String,
26
        expected: Option<String>,
27
        actual: Option<String>,
28
    },
29
    Error(String),
30
}
31

32
impl AssertionResult {
33
    pub fn fail(message: impl Into<String>) -> Self {
160✔
34
        Self::Fail {
160✔
35
            message: message.into(),
160✔
36
            expected: None,
160✔
37
            actual: None,
160✔
38
        }
160✔
39
    }
160✔
40

41
    pub fn fail_with_diff(
13✔
42
        message: impl Into<String>,
13✔
43
        expected: impl Into<String>,
13✔
44
        actual: impl Into<String>,
13✔
45
    ) -> Self {
13✔
46
        Self::Fail {
13✔
47
            message: message.into(),
13✔
48
            expected: Some(expected.into()),
13✔
49
            actual: Some(actual.into()),
13✔
50
        }
13✔
51
    }
13✔
52

53
    pub fn negate(self) -> Self {
34✔
54
        match self {
34✔
55
            Self::Pass => Self::fail("Negated assertion passed (expected false)"),
14✔
56
            Self::Fail { .. } => Self::Pass,
20✔
57
            Self::Error(e) => Self::Error(e),
×
58
        }
59
    }
34✔
60
}
61

62
/// Assertion engine
63
pub struct AssertionEngine {
64
    plugin_manager: PluginManager,
65
}
66

67
type JaqFilter = jaq_core::Filter<data::JustLut<JaqVal>>;
68

69
/// Thread-safe cache for compiled JQ filters.
70
/// Uses `Mutex` instead of `thread_local!` + `RefCell` to be safe with
71
/// tokio's work-stealing runtime where futures can migrate across threads.
72
static JAQ_FILTER_CACHE: LazyLock<Mutex<HashMap<String, Arc<JaqFilter>>>> =
73
    LazyLock::new(|| Mutex::new(HashMap::new()));
3✔
74

75
impl AssertionEngine {
76
    /// Create a new assertion engine
77
    pub fn new() -> Self {
143✔
78
        Self {
143✔
79
            plugin_manager: PluginManager::new(),
143✔
80
        }
143✔
81
    }
143✔
82

83
    /// Evaluate a single assertion
84
    pub fn evaluate(
323✔
85
        &self,
323✔
86
        assertion: &str,
323✔
87
        response: &Value,
323✔
88
        headers: Option<&HashMap<String, String>>,
323✔
89
        trailers: Option<&HashMap<String, String>>,
323✔
90
    ) -> Result<AssertionResult> {
323✔
91
        self.evaluate_with_timing(assertion, response, headers, trailers, None)
323✔
92
    }
323✔
93

94
    pub fn evaluate_with_timing(
336✔
95
        &self,
336✔
96
        assertion: &str,
336✔
97
        response: &Value,
336✔
98
        headers: Option<&HashMap<String, String>>,
336✔
99
        trailers: Option<&HashMap<String, String>>,
336✔
100
        timing: Option<&AssertionTiming>,
336✔
101
    ) -> Result<AssertionResult> {
336✔
102
        let trimmed = assertion.trim();
336✔
103

104
        // 1. Try AST-based operator engine
105
        match operators::evaluate_assertion(
336✔
106
            &self.plugin_manager,
336✔
107
            trimmed,
336✔
108
            response,
336✔
109
            headers,
336✔
110
            trailers,
336✔
111
            timing,
336✔
112
        ) {
113
            Ok(Some(result)) => Ok(result),
335✔
114
            Ok(None) => {
115
                // AST could not parse it — fall through to JQ
116
                self.evaluate_jaq(trimmed, response)
1✔
117
            }
118
            Err(e) => Err(e),
×
119
        }
120
    }
336✔
121

122
    /// Execute a JQ query and return the result(s)
123
    pub fn query(&self, expr: &str, input: &Value) -> Result<Vec<Value>> {
39✔
124
        let values = self.run_jaq(expr, input)?;
39✔
125
        Ok(values.iter().map(jaq_to_json).collect())
38✔
126
    }
39✔
127

128
    fn evaluate_jaq(&self, expr: &str, response: &Value) -> Result<AssertionResult> {
1✔
129
        let out = match self.run_jaq(expr, response) {
1✔
130
            Ok(out) => out,
×
131
            Err(e) => return Ok(AssertionResult::Error(format!("JQ Parse Error: {}", e))),
1✔
132
        };
133

134
        let mut passed = false;
×
135
        let mut seen_false = false;
×
136

137
        for val in out {
×
138
            if matches!(val, JaqVal::Bool(true)) {
×
139
                passed = true;
×
140
            } else {
×
141
                seen_false = true;
×
142
            }
×
143
        }
144

145
        if seen_false {
×
146
            return Ok(AssertionResult::fail(format!(
×
147
                "JQ assertion evaluated to false: {}",
×
148
                expr
×
149
            )));
×
150
        }
×
151

152
        if passed {
×
153
            Ok(AssertionResult::Pass)
×
154
        } else {
155
            Ok(AssertionResult::fail(format!(
×
156
                "JQ assertion produced no output (falsey): {}",
×
157
                expr
×
158
            )))
×
159
        }
160
    }
1✔
161

162
    fn run_jaq(&self, expr: &str, input: &Value) -> Result<Vec<JaqVal>> {
40✔
163
        let filter = Self::get_or_compile_jaq_filter(expr)?;
40✔
164

165
        let input = json_to_jaq(input);
38✔
166

167
        let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
38✔
168
        let out = filter.id.run((ctx, input)).map(unwrap_valr);
38✔
169

170
        let mut values = Vec::new();
38✔
171
        for item in out {
41✔
172
            match item {
41✔
173
                Ok(v) => values.push(v),
41✔
174
                Err(e) => return Err(anyhow::anyhow!("JQ Runtime Error: {}", e)),
×
175
            }
176
        }
177

178
        Ok(values)
38✔
179
    }
40✔
180

181
    fn get_or_compile_jaq_filter(expr: &str) -> Result<Arc<JaqFilter>> {
243✔
182
        use jaq_core::defs as core_defs;
183
        use jaq_core::funs as core_funs;
184

185
        if let Some(cached) = JAQ_FILTER_CACHE
243✔
186
            .lock()
243✔
187
            .unwrap_or_else(|e| e.into_inner())
243✔
188
            .get(expr)
243✔
189
            .cloned()
243✔
190
        {
191
            return Ok(cached);
170✔
192
        }
73✔
193

194
        let arena = load::Arena::default();
73✔
195
        let defs = core_defs().chain(jaq_std::defs()).chain(jaq_json::defs());
73✔
196
        let funs = core_funs().chain(jaq_std::funs()).chain(jaq_json::funs());
73✔
197
        let loader = load::Loader::new(defs);
73✔
198
        let program = load::File {
73✔
199
            code: expr,
73✔
200
            path: (),
73✔
201
        };
73✔
202

203
        let modules = loader
73✔
204
            .load(&arena, program)
73✔
205
            .map_err(|errs| anyhow::anyhow!("Failed to parse JQ expression: {:?}", errs))?;
73✔
206

207
        let filter = Compiler::default()
71✔
208
            .with_funs(funs)
71✔
209
            .compile(modules)
71✔
210
            .map_err(|errs| anyhow::anyhow!("Failed to compile JQ expression: {:?}", errs))?;
71✔
211

212
        let filter = Arc::new(filter);
71✔
213
        JAQ_FILTER_CACHE
71✔
214
            .lock()
71✔
215
            .unwrap_or_else(|e| e.into_inner())
71✔
216
            .insert(expr.to_string(), Arc::clone(&filter));
71✔
217

218
        Ok(filter)
71✔
219
    }
243✔
220

221
    /// Evaluate a JQ expression against `input`, returning the first output value.
222
    /// Uses `JAQ_FILTER_CACHE` to avoid recompilation on repeated calls.
223
    pub(super) fn eval_jaq_one(expr: &str, input: &Value) -> anyhow::Result<Value> {
201✔
224
        let filter = Self::get_or_compile_jaq_filter(expr)?;
201✔
225
        let jaq_input = json_to_jaq(input);
201✔
226
        let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
201✔
227
        let mut out = filter.id.run((ctx, jaq_input)).map(unwrap_valr);
201✔
228
        if let Some(Ok(val)) = out.next() {
201✔
229
            Ok(jaq_to_json(&val))
201✔
230
        } else {
NEW
231
            Err(anyhow::anyhow!("JQ produced no output for: {}", expr))
×
232
        }
233
    }
201✔
234

235
    // Check if any assertion failed (re-exported wrapper)
236
    pub fn has_failures(&self, results: &[AssertionResult]) -> bool {
4✔
237
        results
4✔
238
            .iter()
4✔
239
            .any(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
4✔
240
    }
4✔
241

242
    // Get failed assertions (re-exported wrapper)
243
    pub fn get_failures<'a>(&self, results: &'a [AssertionResult]) -> Vec<&'a AssertionResult> {
1✔
244
        results
1✔
245
            .iter()
1✔
246
            .filter(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
1✔
247
            .collect()
1✔
248
    }
1✔
249

250
    // Evaluate multiple assertions (re-exported wrapper)
251
    pub fn evaluate_all(
9✔
252
        &self,
9✔
253
        assertions: &[String],
9✔
254
        response: &serde_json::Value,
9✔
255
        headers: Option<&HashMap<String, String>>,
9✔
256
        trailers: Option<&HashMap<String, String>>,
9✔
257
    ) -> Vec<AssertionResult> {
9✔
258
        self.evaluate_all_with_timing(assertions, response, headers, trailers, None)
9✔
259
    }
9✔
260

261
    pub fn evaluate_all_with_timing(
10✔
262
        &self,
10✔
263
        assertions: &[String],
10✔
264
        response: &serde_json::Value,
10✔
265
        headers: Option<&HashMap<String, String>>,
10✔
266
        trailers: Option<&HashMap<String, String>>,
10✔
267
        timing: Option<&AssertionTiming>,
10✔
268
    ) -> Vec<AssertionResult> {
10✔
269
        assertions
10✔
270
            .iter()
10✔
271
            .map(|assertion| {
13✔
272
                self.evaluate_with_timing(assertion, response, headers, trailers, timing)
13✔
273
                    .unwrap_or_else(|e| AssertionResult::Error(format!("Internal error: {}", e)))
13✔
274
            })
13✔
275
            .collect()
10✔
276
    }
10✔
277
}
278

279
fn json_to_jaq(value: &Value) -> JaqVal {
1,208✔
280
    match value {
1,208✔
281
        Value::Null => JaqVal::Null,
2✔
282
        Value::Bool(v) => JaqVal::Bool(*v),
26✔
283
        Value::Number(n) => {
427✔
284
            if let Some(i) = n.as_i64() {
427✔
285
                JaqVal::Num(JaqNum::from_integral(i))
426✔
286
            } else if let Some(u) = n.as_u64() {
1✔
287
                JaqVal::Num(JaqNum::from_integral(u))
×
288
            } else if let Some(f) = n.as_f64() {
1✔
289
                JaqVal::Num(JaqNum::Float(f))
1✔
290
            } else {
291
                JaqVal::Null
×
292
            }
293
        }
294
        Value::String(s) => JaqVal::utf8_str(s.clone()),
331✔
295
        Value::Array(items) => JaqVal::Arr(JaqRc::new(items.iter().map(json_to_jaq).collect())),
120✔
296
        Value::Object(obj) => {
302✔
297
            let map: JaqMap = obj
302✔
298
                .iter()
302✔
299
                .map(|(k, v)| (JaqVal::utf8_str(k.clone()), json_to_jaq(v)))
742✔
300
                .collect();
302✔
301
            JaqVal::Obj(JaqRc::new(map))
302✔
302
        }
303
    }
304
}
1,208✔
305

306
fn jaq_to_json(value: &JaqVal) -> Value {
291✔
307
    match value {
291✔
308
        JaqVal::Null => Value::Null,
2✔
309
        JaqVal::Bool(v) => Value::Bool(*v),
4✔
310
        JaqVal::Num(n) => match n {
121✔
311
            JaqNum::Int(v) => Value::Number(serde_json::Number::from(*v)),
119✔
312
            JaqNum::Float(v) => serde_json::Number::from_f64(*v)
×
313
                .map(Value::Number)
×
314
                .unwrap_or(Value::Null),
×
315
            JaqNum::BigInt(bi) => {
×
316
                // Try to fit in isize first (public API), then fall back to string parse
317
                if let Some(i) = n.as_isize() {
×
318
                    Value::Number(serde_json::Number::from(i))
×
319
                } else {
320
                    // BigInt too large for isize — avoid JSON parser on hot path
321
                    let s = bi.to_string();
×
322
                    if let Ok(i) = s.parse::<i64>() {
×
323
                        Value::Number(serde_json::Number::from(i))
×
324
                    } else if let Ok(u) = s.parse::<u64>() {
×
325
                        Value::Number(serde_json::Number::from(u))
×
326
                    } else {
327
                        Value::Null
×
328
                    }
329
                }
330
            }
331
            JaqNum::Dec(s) => {
2✔
332
                // Dec is a string like "3.14" — parse as f64 directly, no JSON parser
333
                s.parse::<f64>()
2✔
334
                    .ok()
2✔
335
                    .and_then(serde_json::Number::from_f64)
2✔
336
                    .map(Value::Number)
2✔
337
                    .unwrap_or(Value::Null)
2✔
338
            }
339
        },
340
        JaqVal::TStr(s) | JaqVal::BStr(s) => {
134✔
341
            match std::str::from_utf8(s.as_ref()) {
134✔
342
                Ok(v) => Value::String(v.to_string()),
134✔
343
                Err(_) => Value::Null, // non-UTF8 bytes can't be represented in JSON
×
344
            }
345
        }
346
        JaqVal::Arr(items) => Value::Array(items.iter().map(jaq_to_json).collect()),
30✔
347
        JaqVal::Obj(obj) => {
×
348
            let map: serde_json::Map<String, Value> = obj
×
349
                .iter()
×
350
                .filter_map(|(k, v)| {
×
351
                    let key = match k {
×
352
                        JaqVal::TStr(s) | JaqVal::BStr(s) => {
×
353
                            std::str::from_utf8(s.as_ref()).ok().map(str::to_owned)
×
354
                        }
355
                        _ => None,
×
356
                    }?;
×
357
                    Some((key, jaq_to_json(v)))
×
358
                })
×
359
                .collect();
×
360
            Value::Object(map)
×
361
        }
362
    }
363
}
291✔
364

365
impl Default for AssertionEngine {
366
    fn default() -> Self {
1✔
367
        Self::new()
1✔
368
    }
1✔
369
}
370

371
#[cfg(test)]
372
mod tests {
373
    use super::*;
374
    use serde_json::json;
375

376
    fn create_test_response() -> Value {
27✔
377
        json!({
27✔
378
            "id": 123,
27✔
379
            "name": "test",
27✔
380
            "email": "test@example.com",
27✔
381
            "active": true,
27✔
382
            "tags": ["a", "b", "c"],
27✔
383
            "nested": {
27✔
384
                "value": 42
27✔
385
            }
386
        })
387
    }
27✔
388

389
    #[test]
390
    fn test_assertion_engine_new() {
1✔
391
        let engine = AssertionEngine::new();
1✔
392
        // Should have default plugins registered
393
        assert!(engine.plugin_manager.get("uuid").is_some());
1✔
394
        assert!(engine.plugin_manager.get("email").is_some());
1✔
395
    }
1✔
396

397
    #[test]
398
    fn test_assertion_engine_default() {
1✔
399
        let engine = AssertionEngine::default();
1✔
400
        assert!(engine.plugin_manager.get("uuid").is_some());
1✔
401
    }
1✔
402

403
    #[test]
404
    fn test_assertion_result_fail() {
1✔
405
        let result = AssertionResult::fail("test message");
1✔
406
        if let AssertionResult::Fail { message, .. } = result {
1✔
407
            assert_eq!(message, "test message");
1✔
408
        } else {
409
            panic!("Expected Fail result");
×
410
        }
411
    }
1✔
412

413
    #[test]
414
    fn test_assertion_result_fail_with_diff() {
1✔
415
        let result = AssertionResult::fail_with_diff("mismatch", "expected", "actual");
1✔
416
        if let AssertionResult::Fail {
417
            message,
1✔
418
            expected,
1✔
419
            actual,
1✔
420
        } = result
1✔
421
        {
422
            assert_eq!(message, "mismatch");
1✔
423
            assert_eq!(expected, Some("expected".to_string()));
1✔
424
            assert_eq!(actual, Some("actual".to_string()));
1✔
425
        } else {
426
            panic!("Expected Fail result");
×
427
        }
428
    }
1✔
429

430
    #[test]
431
    fn test_assertion_result_debug() {
1✔
432
        let result = AssertionResult::Pass;
1✔
433
        let debug_str = format!("{:?}", result);
1✔
434
        assert!(debug_str.contains("Pass"));
1✔
435
    }
1✔
436

437
    #[test]
438
    fn test_evaluate_plugin_function() {
1✔
439
        let engine = AssertionEngine::new();
1✔
440
        let response = create_test_response();
1✔
441

442
        // Test @email plugin
443
        let result = engine
1✔
444
            .evaluate("@email(.email)", &response, None, None)
1✔
445
            .unwrap();
1✔
446
        if let AssertionResult::Pass = result {
1✔
447
            // Pass
1✔
448
        } else {
1✔
449
            panic!("Expected Pass for valid email");
×
450
        }
451
    }
1✔
452

453
    #[test]
454
    fn test_evaluate_plugin_function_invalid() {
1✔
455
        let engine = AssertionEngine::new();
1✔
456
        let response = json!({"email": "not-an-email"});
1✔
457

458
        let result = engine
1✔
459
            .evaluate("@email(.email)", &response, None, None)
1✔
460
            .unwrap();
1✔
461
        if let AssertionResult::Fail { .. } = result {
1✔
462
            // Pass
1✔
463
        } else {
1✔
464
            panic!("Expected Fail for invalid email");
×
465
        }
466
    }
1✔
467

468
    #[test]
469
    fn test_evaluate_empty_plugin() {
1✔
470
        let engine = AssertionEngine::new();
1✔
471
        let response = json!({"tags": []});
1✔
472

473
        let result = engine
1✔
474
            .evaluate("@empty(.tags)", &response, None, None)
1✔
475
            .unwrap();
1✔
476
        if let AssertionResult::Pass = result {
1✔
477
            // Pass
1✔
478
        } else {
1✔
479
            panic!("Expected Pass for empty value");
×
480
        }
481
    }
1✔
482

483
    #[test]
484
    fn test_evaluate_equality_operator() {
1✔
485
        let engine = AssertionEngine::new();
1✔
486
        let response = create_test_response();
1✔
487

488
        let result = engine
1✔
489
            .evaluate(".id == 123", &response, None, None)
1✔
490
            .unwrap();
1✔
491
        if let AssertionResult::Pass = result {
1✔
492
            // Pass
1✔
493
        } else {
1✔
494
            panic!("Expected Pass for equality check");
×
495
        }
496
    }
1✔
497

498
    #[test]
499
    fn test_evaluate_bracket_index_assertion() {
1✔
500
        let engine = AssertionEngine::new();
1✔
501
        let response = serde_json::json!({
1✔
502
            "ipsToDecorations": {
1✔
503
                "10.0.0.1": {
1✔
504
                    "decoration": "web-frontend",
1✔
505
                    "environment": "production"
1✔
506
                }
507
            }
508
        });
509

510
        // Correct value - should PASS
511
        let result1 = engine
1✔
512
            .evaluate(
1✔
513
                ".ipsToDecorations[\"10.0.0.1\"].environment == \"production\"",
1✔
514
                &response,
1✔
515
                None,
1✔
516
                None,
1✔
517
            )
518
            .unwrap();
1✔
519
        assert!(
1✔
520
            matches!(result1, AssertionResult::Pass),
1✔
521
            "Expected Pass for correct value, got: {:?}",
522
            result1
523
        );
524

525
        // Wrong value - should FAIL
526
        let result2 = engine
1✔
527
            .evaluate(
1✔
528
                ".ipsToDecorations[\"10.0.0.1\"].environment == \"production1\"",
1✔
529
                &response,
1✔
530
                None,
1✔
531
                None,
1✔
532
            )
533
            .unwrap();
1✔
534
        assert!(
1✔
535
            matches!(result2, AssertionResult::Fail { .. }),
1✔
536
            "Expected Fail for wrong value, got: {:?}",
537
            result2
538
        );
539
    }
1✔
540

541
    #[test]
542
    fn test_evaluate_equality_operator_fail() {
1✔
543
        let engine = AssertionEngine::new();
1✔
544
        let response = create_test_response();
1✔
545

546
        let result = engine
1✔
547
            .evaluate(".id == 456", &response, None, None)
1✔
548
            .unwrap();
1✔
549
        if let AssertionResult::Fail { .. } = result {
1✔
550
            // Pass
1✔
551
        } else {
1✔
552
            panic!("Expected Fail for equality check");
×
553
        }
554
    }
1✔
555

556
    #[test]
557
    fn test_evaluate_inequality_operator() {
1✔
558
        let engine = AssertionEngine::new();
1✔
559
        let response = create_test_response();
1✔
560

561
        let result = engine
1✔
562
            .evaluate(".id != 456", &response, None, None)
1✔
563
            .unwrap();
1✔
564
        if let AssertionResult::Pass = result {
1✔
565
            // Pass
1✔
566
        } else {
1✔
567
            panic!("Expected Pass for inequality check");
×
568
        }
569
    }
1✔
570

571
    #[test]
572
    fn test_evaluate_contains_operator() {
1✔
573
        let engine = AssertionEngine::new();
1✔
574
        let response = create_test_response();
1✔
575

576
        let result = engine
1✔
577
            .evaluate(".name contains \"test\"", &response, None, None)
1✔
578
            .unwrap();
1✔
579
        if let AssertionResult::Pass = result {
1✔
580
            // Pass
1✔
581
        } else {
1✔
582
            panic!("Expected Pass for contains check");
×
583
        }
584
    }
1✔
585

586
    #[test]
587
    fn test_evaluate_contains_operator_array() {
1✔
588
        let engine = AssertionEngine::new();
1✔
589
        let response = create_test_response();
1✔
590

591
        let result = engine
1✔
592
            .evaluate(".tags contains \"a\"", &response, None, None)
1✔
593
            .unwrap();
1✔
594
        if let AssertionResult::Pass = result {
1✔
595
            // Pass
1✔
596
        } else {
1✔
597
            panic!("Expected Pass for array contains check");
×
598
        }
599
    }
1✔
600

601
    #[test]
602
    fn test_evaluate_starts_with_operator() {
1✔
603
        let engine = AssertionEngine::new();
1✔
604
        let response = create_test_response();
1✔
605

606
        let result = engine
1✔
607
            .evaluate(".name startsWith \"te\"", &response, None, None)
1✔
608
            .unwrap();
1✔
609
        if let AssertionResult::Pass = result {
1✔
610
            // Pass
1✔
611
        } else {
1✔
612
            panic!("Expected Pass for startsWith check");
×
613
        }
614
    }
1✔
615

616
    #[test]
617
    fn test_evaluate_ends_with_operator() {
1✔
618
        let engine = AssertionEngine::new();
1✔
619
        let response = create_test_response();
1✔
620

621
        let result = engine
1✔
622
            .evaluate(".name endsWith \"st\"", &response, None, None)
1✔
623
            .unwrap();
1✔
624
        if let AssertionResult::Pass = result {
1✔
625
            // Pass
1✔
626
        } else {
1✔
627
            panic!("Expected Pass for endsWith check");
×
628
        }
629
    }
1✔
630

631
    #[test]
632
    fn test_evaluate_numeric_greater_than() {
1✔
633
        let engine = AssertionEngine::new();
1✔
634
        let response = create_test_response();
1✔
635

636
        let result = engine.evaluate(".id > 100", &response, None, None).unwrap();
1✔
637
        if let AssertionResult::Pass = result {
1✔
638
            // Pass
1✔
639
        } else {
1✔
640
            panic!("Expected Pass for greater than check");
×
641
        }
642
    }
1✔
643

644
    #[test]
645
    fn test_evaluate_numeric_less_than() {
1✔
646
        let engine = AssertionEngine::new();
1✔
647
        let response = create_test_response();
1✔
648

649
        let result = engine.evaluate(".id < 200", &response, None, None).unwrap();
1✔
650
        if let AssertionResult::Pass = result {
1✔
651
            // Pass
1✔
652
        } else {
1✔
653
            panic!("Expected Pass for less than check");
×
654
        }
655
    }
1✔
656

657
    #[test]
658
    fn test_evaluate_numeric_gte() {
1✔
659
        let engine = AssertionEngine::new();
1✔
660
        let response = create_test_response();
1✔
661

662
        let result = engine
1✔
663
            .evaluate(".id >= 123", &response, None, None)
1✔
664
            .unwrap();
1✔
665
        if let AssertionResult::Pass = result {
1✔
666
            // Pass
1✔
667
        } else {
1✔
668
            panic!("Expected Pass for gte check");
×
669
        }
670
    }
1✔
671

672
    #[test]
673
    fn test_evaluate_numeric_lte() {
1✔
674
        let engine = AssertionEngine::new();
1✔
675
        let response = create_test_response();
1✔
676

677
        let result = engine
1✔
678
            .evaluate(".id <= 123", &response, None, None)
1✔
679
            .unwrap();
1✔
680
        if let AssertionResult::Pass = result {
1✔
681
            // Pass
1✔
682
        } else {
1✔
683
            panic!("Expected Pass for lte check");
×
684
        }
685
    }
1✔
686

687
    #[test]
688
    fn test_evaluate_matches_regex() {
1✔
689
        let engine = AssertionEngine::new();
1✔
690
        let response = create_test_response();
1✔
691

692
        let result = engine
1✔
693
            .evaluate(".name matches \"^te.*t$\"", &response, None, None)
1✔
694
            .unwrap();
1✔
695
        if let AssertionResult::Pass = result {
1✔
696
            // Pass
1✔
697
        } else {
1✔
698
            panic!("Expected Pass for regex match");
×
699
        }
700
    }
1✔
701

702
    #[test]
703
    fn test_evaluate_matches_regex_fail() {
1✔
704
        let engine = AssertionEngine::new();
1✔
705
        let response = create_test_response();
1✔
706

707
        let result = engine
1✔
708
            .evaluate(".name matches \"^xyz\"", &response, None, None)
1✔
709
            .unwrap();
1✔
710
        if let AssertionResult::Fail { .. } = result {
1✔
711
            // Pass
1✔
712
        } else {
1✔
713
            panic!("Expected Fail for regex match");
×
714
        }
715
    }
1✔
716

717
    #[test]
718
    fn test_evaluate_header_plugin() {
1✔
719
        let engine = AssertionEngine::new();
1✔
720
        let response = create_test_response();
1✔
721
        let mut headers = HashMap::new();
1✔
722
        headers.insert("content-type".to_string(), "application/json".to_string());
1✔
723

724
        let result = engine
1✔
725
            .evaluate("@header(\"content-type\")", &response, Some(&headers), None)
1✔
726
            .unwrap();
1✔
727
        if let AssertionResult::Pass = result {
1✔
728
            // Pass
1✔
729
        } else {
1✔
730
            panic!("Expected Pass for header check");
×
731
        }
732
    }
1✔
733

734
    #[test]
735
    fn test_evaluate_trailer_plugin() {
1✔
736
        let engine = AssertionEngine::new();
1✔
737
        let response = create_test_response();
1✔
738
        let mut trailers = HashMap::new();
1✔
739
        trailers.insert("x-custom".to_string(), "value".to_string());
1✔
740

741
        let result = engine
1✔
742
            .evaluate("@trailer(\"x-custom\")", &response, None, Some(&trailers))
1✔
743
            .unwrap();
1✔
744
        if let AssertionResult::Pass = result {
1✔
745
            // Pass
1✔
746
        } else {
1✔
747
            panic!("Expected Pass for trailer check");
×
748
        }
749
    }
1✔
750

751
    #[test]
752
    fn test_evaluate_nested_path() {
1✔
753
        let engine = AssertionEngine::new();
1✔
754
        let response = create_test_response();
1✔
755

756
        let result = engine
1✔
757
            .evaluate(".nested.value == 42", &response, None, None)
1✔
758
            .unwrap();
1✔
759
        if let AssertionResult::Pass = result {
1✔
760
            // Pass
1✔
761
        } else {
1✔
762
            panic!("Expected Pass for nested path check");
×
763
        }
764
    }
1✔
765

766
    #[test]
767
    fn test_evaluate_boolean_path() {
1✔
768
        let engine = AssertionEngine::new();
1✔
769
        let response = create_test_response();
1✔
770

771
        let result = engine
1✔
772
            .evaluate(".active == true", &response, None, None)
1✔
773
            .unwrap();
1✔
774
        if let AssertionResult::Pass = result {
1✔
775
            // Pass
1✔
776
        } else {
1✔
777
            panic!("Expected Pass for boolean check");
×
778
        }
779
    }
1✔
780

781
    #[test]
782
    fn test_evaluate_array_index() {
1✔
783
        let engine = AssertionEngine::new();
1✔
784
        let response = create_test_response();
1✔
785

786
        let result = engine
1✔
787
            .evaluate(".tags[0] == \"a\"", &response, None, None)
1✔
788
            .unwrap();
1✔
789
        if let AssertionResult::Pass = result {
1✔
790
            // Pass
1✔
791
        } else {
1✔
792
            panic!("Expected Pass for array index check");
×
793
        }
794
    }
1✔
795

796
    #[test]
797
    fn test_evaluate_unsupported_syntax() {
1✔
798
        let engine = AssertionEngine::new();
1✔
799
        let response = create_test_response();
1✔
800

801
        // This should fall through to JQ evaluation
802
        let result = engine.evaluate("some_unknown_function()", &response, None, None);
1✔
803
        // Should not panic, should return Error or handle gracefully
804
        assert!(result.is_ok());
1✔
805
    }
1✔
806

807
    #[test]
808
    fn test_evaluate_all() {
1✔
809
        let engine = AssertionEngine::new();
1✔
810
        let response = create_test_response();
1✔
811

812
        let assertions = vec![".id == 123".to_string(), ".name == \"test\"".to_string()];
1✔
813

814
        let results = engine.evaluate_all(&assertions, &response, None, None);
1✔
815
        assert_eq!(results.len(), 2);
1✔
816
        assert!(results.iter().all(|r| matches!(r, AssertionResult::Pass)));
2✔
817
    }
1✔
818

819
    #[test]
820
    fn test_evaluate_all_with_failure() {
1✔
821
        let engine = AssertionEngine::new();
1✔
822
        let response = create_test_response();
1✔
823

824
        let assertions = vec![".id == 123".to_string(), ".id == 999".to_string()];
1✔
825

826
        let results = engine.evaluate_all(&assertions, &response, None, None);
1✔
827
        assert_eq!(results.len(), 2);
1✔
828
        assert!(matches!(&results[0], AssertionResult::Pass));
1✔
829
        assert!(matches!(&results[1], AssertionResult::Fail { .. }));
1✔
830
    }
1✔
831

832
    #[test]
833
    fn test_query_jq_simple() {
1✔
834
        let engine = AssertionEngine::new();
1✔
835
        let response = create_test_response();
1✔
836

837
        let results = engine.query(".id", &response).unwrap();
1✔
838
        assert_eq!(results.len(), 1);
1✔
839
        assert_eq!(results[0], json!(123));
1✔
840
    }
1✔
841

842
    #[test]
843
    fn test_query_jq_nested() {
1✔
844
        let engine = AssertionEngine::new();
1✔
845
        let response = create_test_response();
1✔
846

847
        let results = engine.query(".nested.value", &response).unwrap();
1✔
848
        assert_eq!(results.len(), 1);
1✔
849
        assert_eq!(results[0], json!(42));
1✔
850
    }
1✔
851

852
    #[test]
853
    fn test_query_jq_array() {
1✔
854
        let engine = AssertionEngine::new();
1✔
855
        let response = create_test_response();
1✔
856

857
        let results = engine.query(".tags[]", &response).unwrap();
1✔
858
        assert_eq!(results.len(), 3);
1✔
859
        assert_eq!(results[0], json!("a"));
1✔
860
        assert_eq!(results[1], json!("b"));
1✔
861
        assert_eq!(results[2], json!("c"));
1✔
862
    }
1✔
863

864
    #[test]
865
    fn test_query_jq_filter() {
1✔
866
        let engine = AssertionEngine::new();
1✔
867
        let response = json!([1, 2, 3, 4, 5]);
1✔
868

869
        let results = engine.query(".[] | select(. > 3)", &response).unwrap();
1✔
870
        assert_eq!(results.len(), 2);
1✔
871
        assert_eq!(results[0], json!(4));
1✔
872
        assert_eq!(results[1], json!(5));
1✔
873
    }
1✔
874

875
    #[test]
876
    fn test_query_jq_length() {
1✔
877
        let engine = AssertionEngine::new();
1✔
878
        let response = create_test_response();
1✔
879

880
        let results = engine.query(".tags | length", &response).unwrap();
1✔
881
        assert_eq!(results.len(), 1);
1✔
882
        assert_eq!(results[0], json!(3));
1✔
883
    }
1✔
884

885
    #[test]
886
    fn test_query_invalid_expression() {
1✔
887
        let engine = AssertionEngine::new();
1✔
888
        let response = create_test_response();
1✔
889

890
        let results = engine.query("invalid[[[", &response);
1✔
891
        assert!(results.is_err());
1✔
892
    }
1✔
893

894
    #[test]
895
    fn test_jaq_to_json_dec_number() {
1✔
896
        let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("2.5".to_string())));
1✔
897
        assert_eq!(jaq_to_json(&dec), json!(2.5));
1✔
898
    }
1✔
899

900
    #[test]
901
    fn test_jaq_to_json_invalid_dec_number() {
1✔
902
        let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("not-a-number".to_string())));
1✔
903
        assert_eq!(jaq_to_json(&dec), Value::Null);
1✔
904
    }
1✔
905

906
    #[test]
907
    fn test_json_to_jaq_null() {
1✔
908
        let result = json_to_jaq(&json!(null));
1✔
909
        assert!(matches!(result, JaqVal::Null));
1✔
910
    }
1✔
911

912
    #[test]
913
    fn test_json_to_jaq_bool() {
1✔
914
        let result = json_to_jaq(&json!(true));
1✔
915
        assert!(matches!(result, JaqVal::Bool(true)));
1✔
916
    }
1✔
917

918
    #[test]
919
    fn test_json_to_jaq_number_int() {
1✔
920
        let result = json_to_jaq(&json!(42));
1✔
921
        assert!(matches!(result, JaqVal::Num(JaqNum::Int(42))));
1✔
922
    }
1✔
923

924
    #[test]
925
    fn test_json_to_jaq_number_float() {
1✔
926
        let result = json_to_jaq(&json!(4.14));
1✔
927
        assert!(matches!(result, JaqVal::Num(JaqNum::Float(f)) if (f - 4.14).abs() < 0.001));
1✔
928
    }
1✔
929

930
    #[test]
931
    fn test_json_to_jaq_string() {
1✔
932
        let result = json_to_jaq(&json!("hello"));
1✔
933
        assert!(matches!(result, JaqVal::TStr(_)));
1✔
934
    }
1✔
935

936
    #[test]
937
    fn test_json_to_jaq_array() {
1✔
938
        let result = json_to_jaq(&json!([1, 2, 3]));
1✔
939
        assert!(matches!(result, JaqVal::Arr(_)));
1✔
940
    }
1✔
941

942
    #[test]
943
    fn test_json_to_jaq_object() {
1✔
944
        let result = json_to_jaq(&json!({"key": "value"}));
1✔
945
        assert!(matches!(result, JaqVal::Obj(_)));
1✔
946
    }
1✔
947

948
    #[test]
949
    fn test_jaq_filter_cache_returns_same_arc() {
1✔
950
        let expr = ".__cache_test_sentinel__";
1✔
951
        let first = AssertionEngine::get_or_compile_jaq_filter(expr).unwrap();
1✔
952
        let second = AssertionEngine::get_or_compile_jaq_filter(expr).unwrap();
1✔
953
        assert!(Arc::ptr_eq(&first, &second));
1✔
954
    }
1✔
955

956
    #[test]
957
    fn test_operators_resolve_path_populates_cache() {
1✔
958
        use super::operators;
959
        use crate::plugins::{AssertionTiming, PluginManager};
960

961
        let unique_expr = ".cache_probe_field_xyz";
1✔
962
        assert!(!JAQ_FILTER_CACHE.lock().unwrap().contains_key(unique_expr));
1✔
963

964
        let response = serde_json::json!({"cache_probe_field_xyz": 42});
1✔
965
        let timing = AssertionTiming {
1✔
966
            elapsed_ms: 0,
1✔
967
            total_elapsed_ms: 0,
1✔
968
            scope_message_count: 0,
1✔
969
            scope_index: 1,
1✔
970
        };
1✔
971
        let pm = PluginManager::new();
1✔
972
        let _ =
973
            operators::evaluate_assertion(&pm, unique_expr, &response, None, None, Some(&timing));
1✔
974

975
        assert!(
1✔
976
            JAQ_FILTER_CACHE.lock().unwrap().contains_key(unique_expr),
1✔
977
            "eval_jaq_one in operators should populate JAQ_FILTER_CACHE"
978
        );
979
    }
1✔
980
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc