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

gripmock / grpctestify-rust / 24582789334

17 Apr 2026 07:23PM UTC coverage: 76.58% (+0.1%) from 76.438%
24582789334

push

github

web-flow
Merge pull request #39 from gripmock/refactoring-v3

refactoring part3

579 of 719 new or added lines in 11 files covered. (80.53%)

5 existing lines in 3 files now uncovered.

17585 of 22963 relevant lines covered (76.58%)

2430.68 hits per line

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

88.01
/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 {
68✔
34
        Self::Fail {
68✔
35
            message: message.into(),
68✔
36
            expected: None,
68✔
37
            actual: None,
68✔
38
        }
68✔
39
    }
68✔
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 {
7✔
54
        match self {
7✔
55
            Self::Pass => Self::fail("Negated assertion passed (expected false)"),
3✔
56
            Self::Fail { .. } => Self::Pass,
4✔
NEW
57
            Self::Error(e) => Self::Error(e),
×
58
        }
59
    }
7✔
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()));
2✔
74

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

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

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

104
        // 1. Try AST-based operator engine
105
        match operators::evaluate_assertion(
87✔
106
            &self.plugin_manager,
87✔
107
            trimmed,
87✔
108
            response,
87✔
109
            headers,
87✔
110
            trailers,
87✔
111
            timing,
87✔
112
        ) {
113
            Ok(Some(result)) => Ok(result),
86✔
114
            Ok(None) => {
115
                // AST could not parse it — fall through to JQ
116
                self.evaluate_jaq(trimmed, response)
1✔
117
            }
NEW
118
            Err(e) => Err(e),
×
119
        }
120
    }
87✔
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>> {
40✔
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
40✔
186
            .lock()
40✔
187
            .expect("JQ filter cache poisoned")
40✔
188
            .get(expr)
40✔
189
            .cloned()
40✔
190
        {
191
            return Ok(cached);
21✔
192
        }
19✔
193

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

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

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

212
        let filter = Arc::new(filter);
17✔
213
        JAQ_FILTER_CACHE
17✔
214
            .lock()
17✔
215
            .expect("JQ filter cache poisoned")
17✔
216
            .insert(expr.to_string(), Arc::clone(&filter));
17✔
217

218
        Ok(filter)
17✔
219
    }
40✔
220

221
    // Check if any assertion failed (re-exported wrapper)
222
    pub fn has_failures(&self, results: &[AssertionResult]) -> bool {
4✔
223
        results
4✔
224
            .iter()
4✔
225
            .any(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
4✔
226
    }
4✔
227

228
    // Get failed assertions (re-exported wrapper)
229
    pub fn get_failures<'a>(&self, results: &'a [AssertionResult]) -> Vec<&'a AssertionResult> {
1✔
230
        results
1✔
231
            .iter()
1✔
232
            .filter(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
1✔
233
            .collect()
1✔
234
    }
1✔
235

236
    // Evaluate multiple assertions (re-exported wrapper)
237
    pub fn evaluate_all(
9✔
238
        &self,
9✔
239
        assertions: &[String],
9✔
240
        response: &serde_json::Value,
9✔
241
        headers: Option<&HashMap<String, String>>,
9✔
242
        trailers: Option<&HashMap<String, String>>,
9✔
243
    ) -> Vec<AssertionResult> {
9✔
244
        self.evaluate_all_with_timing(assertions, response, headers, trailers, None)
9✔
245
    }
9✔
246

247
    pub fn evaluate_all_with_timing(
9✔
248
        &self,
9✔
249
        assertions: &[String],
9✔
250
        response: &serde_json::Value,
9✔
251
        headers: Option<&HashMap<String, String>>,
9✔
252
        trailers: Option<&HashMap<String, String>>,
9✔
253
        timing: Option<&AssertionTiming>,
9✔
254
    ) -> Vec<AssertionResult> {
9✔
255
        assertions
9✔
256
            .iter()
9✔
257
            .map(|assertion| {
10✔
258
                self.evaluate_with_timing(assertion, response, headers, trailers, timing)
10✔
259
                    .unwrap_or_else(|e| AssertionResult::Error(format!("Internal error: {}", e)))
10✔
260
            })
10✔
261
            .collect()
9✔
262
    }
9✔
263
}
264

265
fn json_to_jaq(value: &Value) -> JaqVal {
142✔
266
    match value {
142✔
267
        Value::Null => JaqVal::Null,
2✔
268
        Value::Bool(v) => JaqVal::Bool(*v),
5✔
269
        Value::Number(n) => {
54✔
270
            if let Some(i) = n.as_i64() {
54✔
271
                JaqVal::Num(JaqNum::from_integral(i))
53✔
272
            } else if let Some(u) = n.as_u64() {
1✔
273
                JaqVal::Num(JaqNum::from_integral(u))
×
274
            } else if let Some(f) = n.as_f64() {
1✔
275
                JaqVal::Num(JaqNum::Float(f))
1✔
276
            } else {
277
                JaqVal::Null
×
278
            }
279
        }
280
        Value::String(s) => JaqVal::utf8_str(s.clone()),
31✔
281
        Value::Array(items) => JaqVal::Arr(JaqRc::new(items.iter().map(json_to_jaq).collect())),
8✔
282
        Value::Object(obj) => {
42✔
283
            let map: JaqMap = obj
42✔
284
                .iter()
42✔
285
                .map(|(k, v)| (JaqVal::utf8_str(k.clone()), json_to_jaq(v)))
75✔
286
                .collect();
42✔
287
            JaqVal::Obj(JaqRc::new(map))
42✔
288
        }
289
    }
290
}
142✔
291

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

351
impl Default for AssertionEngine {
352
    fn default() -> Self {
1✔
353
        Self::new()
1✔
354
    }
1✔
355
}
356

357
#[cfg(test)]
358
mod tests {
359
    use super::*;
360
    use serde_json::json;
361

362
    fn create_test_response() -> Value {
27✔
363
        json!({
27✔
364
            "id": 123,
27✔
365
            "name": "test",
27✔
366
            "email": "test@example.com",
27✔
367
            "active": true,
27✔
368
            "tags": ["a", "b", "c"],
27✔
369
            "nested": {
27✔
370
                "value": 42
27✔
371
            }
372
        })
373
    }
27✔
374

375
    #[test]
376
    fn test_assertion_engine_new() {
1✔
377
        let engine = AssertionEngine::new();
1✔
378
        // Should have default plugins registered
379
        assert!(engine.plugin_manager.get("uuid").is_some());
1✔
380
        assert!(engine.plugin_manager.get("email").is_some());
1✔
381
    }
1✔
382

383
    #[test]
384
    fn test_assertion_engine_default() {
1✔
385
        let engine = AssertionEngine::default();
1✔
386
        assert!(engine.plugin_manager.get("uuid").is_some());
1✔
387
    }
1✔
388

389
    #[test]
390
    fn test_assertion_result_fail() {
1✔
391
        let result = AssertionResult::fail("test message");
1✔
392
        if let AssertionResult::Fail { message, .. } = result {
1✔
393
            assert_eq!(message, "test message");
1✔
394
        } else {
395
            panic!("Expected Fail result");
×
396
        }
397
    }
1✔
398

399
    #[test]
400
    fn test_assertion_result_fail_with_diff() {
1✔
401
        let result = AssertionResult::fail_with_diff("mismatch", "expected", "actual");
1✔
402
        if let AssertionResult::Fail {
403
            message,
1✔
404
            expected,
1✔
405
            actual,
1✔
406
        } = result
1✔
407
        {
408
            assert_eq!(message, "mismatch");
1✔
409
            assert_eq!(expected, Some("expected".to_string()));
1✔
410
            assert_eq!(actual, Some("actual".to_string()));
1✔
411
        } else {
412
            panic!("Expected Fail result");
×
413
        }
414
    }
1✔
415

416
    #[test]
417
    fn test_assertion_result_debug() {
1✔
418
        let result = AssertionResult::Pass;
1✔
419
        let debug_str = format!("{:?}", result);
1✔
420
        assert!(debug_str.contains("Pass"));
1✔
421
    }
1✔
422

423
    #[test]
424
    fn test_evaluate_plugin_function() {
1✔
425
        let engine = AssertionEngine::new();
1✔
426
        let response = create_test_response();
1✔
427

428
        // Test @email plugin
429
        let result = engine
1✔
430
            .evaluate("@email(.email)", &response, None, None)
1✔
431
            .unwrap();
1✔
432
        if let AssertionResult::Pass = result {
1✔
433
            // Pass
1✔
434
        } else {
1✔
435
            panic!("Expected Pass for valid email");
×
436
        }
437
    }
1✔
438

439
    #[test]
440
    fn test_evaluate_plugin_function_invalid() {
1✔
441
        let engine = AssertionEngine::new();
1✔
442
        let response = json!({"email": "not-an-email"});
1✔
443

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

454
    #[test]
455
    fn test_evaluate_empty_plugin() {
1✔
456
        let engine = AssertionEngine::new();
1✔
457
        let response = json!({"tags": []});
1✔
458

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

469
    #[test]
470
    fn test_evaluate_equality_operator() {
1✔
471
        let engine = AssertionEngine::new();
1✔
472
        let response = create_test_response();
1✔
473

474
        let result = engine
1✔
475
            .evaluate(".id == 123", &response, None, None)
1✔
476
            .unwrap();
1✔
477
        if let AssertionResult::Pass = result {
1✔
478
            // Pass
1✔
479
        } else {
1✔
480
            panic!("Expected Pass for equality check");
×
481
        }
482
    }
1✔
483

484
    #[test]
485
    fn test_evaluate_bracket_index_assertion() {
1✔
486
        let engine = AssertionEngine::new();
1✔
487
        let response = serde_json::json!({
1✔
488
            "ipsToDecorations": {
1✔
489
                "10.0.0.1": {
1✔
490
                    "decoration": "web-frontend",
1✔
491
                    "environment": "production"
1✔
492
                }
493
            }
494
        });
495

496
        // Correct value - should PASS
497
        let result1 = engine
1✔
498
            .evaluate(
1✔
499
                ".ipsToDecorations[\"10.0.0.1\"].environment == \"production\"",
1✔
500
                &response,
1✔
501
                None,
1✔
502
                None,
1✔
503
            )
504
            .unwrap();
1✔
505
        assert!(
1✔
506
            matches!(result1, AssertionResult::Pass),
1✔
507
            "Expected Pass for correct value, got: {:?}",
508
            result1
509
        );
510

511
        // Wrong value - should FAIL
512
        let result2 = engine
1✔
513
            .evaluate(
1✔
514
                ".ipsToDecorations[\"10.0.0.1\"].environment == \"production1\"",
1✔
515
                &response,
1✔
516
                None,
1✔
517
                None,
1✔
518
            )
519
            .unwrap();
1✔
520
        assert!(
1✔
521
            matches!(result2, AssertionResult::Fail { .. }),
1✔
522
            "Expected Fail for wrong value, got: {:?}",
523
            result2
524
        );
525
    }
1✔
526

527
    #[test]
528
    fn test_evaluate_equality_operator_fail() {
1✔
529
        let engine = AssertionEngine::new();
1✔
530
        let response = create_test_response();
1✔
531

532
        let result = engine
1✔
533
            .evaluate(".id == 456", &response, None, None)
1✔
534
            .unwrap();
1✔
535
        if let AssertionResult::Fail { .. } = result {
1✔
536
            // Pass
1✔
537
        } else {
1✔
538
            panic!("Expected Fail for equality check");
×
539
        }
540
    }
1✔
541

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

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

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

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

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

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

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

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

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

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

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

622
        let result = engine.evaluate(".id > 100", &response, None, None).unwrap();
1✔
623
        if let AssertionResult::Pass = result {
1✔
624
            // Pass
1✔
625
        } else {
1✔
626
            panic!("Expected Pass for greater than check");
×
627
        }
628
    }
1✔
629

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

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

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

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

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

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

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

678
        let result = engine
1✔
679
            .evaluate(".name matches \"^te.*t$\"", &response, None, None)
1✔
680
            .unwrap();
1✔
681
        if let AssertionResult::Pass = result {
1✔
682
            // Pass
1✔
683
        } else {
1✔
684
            panic!("Expected Pass for regex match");
×
685
        }
686
    }
1✔
687

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

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

703
    #[test]
704
    fn test_evaluate_header_plugin() {
1✔
705
        let engine = AssertionEngine::new();
1✔
706
        let response = create_test_response();
1✔
707
        let mut headers = HashMap::new();
1✔
708
        headers.insert("content-type".to_string(), "application/json".to_string());
1✔
709

710
        let result = engine
1✔
711
            .evaluate("@header(\"content-type\")", &response, Some(&headers), None)
1✔
712
            .unwrap();
1✔
713
        if let AssertionResult::Pass = result {
1✔
714
            // Pass
1✔
715
        } else {
1✔
716
            panic!("Expected Pass for header check");
×
717
        }
718
    }
1✔
719

720
    #[test]
721
    fn test_evaluate_trailer_plugin() {
1✔
722
        let engine = AssertionEngine::new();
1✔
723
        let response = create_test_response();
1✔
724
        let mut trailers = HashMap::new();
1✔
725
        trailers.insert("x-custom".to_string(), "value".to_string());
1✔
726

727
        let result = engine
1✔
728
            .evaluate("@trailer(\"x-custom\")", &response, None, Some(&trailers))
1✔
729
            .unwrap();
1✔
730
        if let AssertionResult::Pass = result {
1✔
731
            // Pass
1✔
732
        } else {
1✔
733
            panic!("Expected Pass for trailer check");
×
734
        }
735
    }
1✔
736

737
    #[test]
738
    fn test_evaluate_nested_path() {
1✔
739
        let engine = AssertionEngine::new();
1✔
740
        let response = create_test_response();
1✔
741

742
        let result = engine
1✔
743
            .evaluate(".nested.value == 42", &response, None, None)
1✔
744
            .unwrap();
1✔
745
        if let AssertionResult::Pass = result {
1✔
746
            // Pass
1✔
747
        } else {
1✔
748
            panic!("Expected Pass for nested path check");
×
749
        }
750
    }
1✔
751

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

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

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

772
        let result = engine
1✔
773
            .evaluate(".tags[0] == \"a\"", &response, None, None)
1✔
774
            .unwrap();
1✔
775
        if let AssertionResult::Pass = result {
1✔
776
            // Pass
1✔
777
        } else {
1✔
778
            panic!("Expected Pass for array index check");
×
779
        }
780
    }
1✔
781

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

787
        // This should fall through to JQ evaluation
788
        let result = engine.evaluate("some_unknown_function()", &response, None, None);
1✔
789
        // Should not panic, should return Error or handle gracefully
790
        assert!(result.is_ok());
1✔
791
    }
1✔
792

793
    #[test]
794
    fn test_evaluate_all() {
1✔
795
        let engine = AssertionEngine::new();
1✔
796
        let response = create_test_response();
1✔
797

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

800
        let results = engine.evaluate_all(&assertions, &response, None, None);
1✔
801
        assert_eq!(results.len(), 2);
1✔
802
        assert!(results.iter().all(|r| matches!(r, AssertionResult::Pass)));
2✔
803
    }
1✔
804

805
    #[test]
806
    fn test_evaluate_all_with_failure() {
1✔
807
        let engine = AssertionEngine::new();
1✔
808
        let response = create_test_response();
1✔
809

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

812
        let results = engine.evaluate_all(&assertions, &response, None, None);
1✔
813
        assert_eq!(results.len(), 2);
1✔
814
        assert!(matches!(&results[0], AssertionResult::Pass));
1✔
815
        assert!(matches!(&results[1], AssertionResult::Fail { .. }));
1✔
816
    }
1✔
817

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

823
        let results = engine.query(".id", &response).unwrap();
1✔
824
        assert_eq!(results.len(), 1);
1✔
825
        assert_eq!(results[0], json!(123));
1✔
826
    }
1✔
827

828
    #[test]
829
    fn test_query_jq_nested() {
1✔
830
        let engine = AssertionEngine::new();
1✔
831
        let response = create_test_response();
1✔
832

833
        let results = engine.query(".nested.value", &response).unwrap();
1✔
834
        assert_eq!(results.len(), 1);
1✔
835
        assert_eq!(results[0], json!(42));
1✔
836
    }
1✔
837

838
    #[test]
839
    fn test_query_jq_array() {
1✔
840
        let engine = AssertionEngine::new();
1✔
841
        let response = create_test_response();
1✔
842

843
        let results = engine.query(".tags[]", &response).unwrap();
1✔
844
        assert_eq!(results.len(), 3);
1✔
845
        assert_eq!(results[0], json!("a"));
1✔
846
        assert_eq!(results[1], json!("b"));
1✔
847
        assert_eq!(results[2], json!("c"));
1✔
848
    }
1✔
849

850
    #[test]
851
    fn test_query_jq_filter() {
1✔
852
        let engine = AssertionEngine::new();
1✔
853
        let response = json!([1, 2, 3, 4, 5]);
1✔
854

855
        let results = engine.query(".[] | select(. > 3)", &response).unwrap();
1✔
856
        assert_eq!(results.len(), 2);
1✔
857
        assert_eq!(results[0], json!(4));
1✔
858
        assert_eq!(results[1], json!(5));
1✔
859
    }
1✔
860

861
    #[test]
862
    fn test_query_jq_length() {
1✔
863
        let engine = AssertionEngine::new();
1✔
864
        let response = create_test_response();
1✔
865

866
        let results = engine.query(".tags | length", &response).unwrap();
1✔
867
        assert_eq!(results.len(), 1);
1✔
868
        assert_eq!(results[0], json!(3));
1✔
869
    }
1✔
870

871
    #[test]
872
    fn test_query_invalid_expression() {
1✔
873
        let engine = AssertionEngine::new();
1✔
874
        let response = create_test_response();
1✔
875

876
        let results = engine.query("invalid[[[", &response);
1✔
877
        assert!(results.is_err());
1✔
878
    }
1✔
879

880
    #[test]
881
    fn test_jaq_to_json_dec_number() {
1✔
882
        let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("2.5".to_string())));
1✔
883
        assert_eq!(jaq_to_json(&dec), json!(2.5));
1✔
884
    }
1✔
885

886
    #[test]
887
    fn test_jaq_to_json_invalid_dec_number() {
1✔
888
        let dec = JaqVal::Num(JaqNum::Dec(JaqRc::new("not-a-number".to_string())));
1✔
889
        assert_eq!(jaq_to_json(&dec), Value::Null);
1✔
890
    }
1✔
891

892
    #[test]
893
    fn test_json_to_jaq_null() {
1✔
894
        let result = json_to_jaq(&json!(null));
1✔
895
        assert!(matches!(result, JaqVal::Null));
1✔
896
    }
1✔
897

898
    #[test]
899
    fn test_json_to_jaq_bool() {
1✔
900
        let result = json_to_jaq(&json!(true));
1✔
901
        assert!(matches!(result, JaqVal::Bool(true)));
1✔
902
    }
1✔
903

904
    #[test]
905
    fn test_json_to_jaq_number_int() {
1✔
906
        let result = json_to_jaq(&json!(42));
1✔
907
        assert!(matches!(result, JaqVal::Num(JaqNum::Int(42))));
1✔
908
    }
1✔
909

910
    #[test]
911
    fn test_json_to_jaq_number_float() {
1✔
912
        let result = json_to_jaq(&json!(4.14));
1✔
913
        assert!(matches!(result, JaqVal::Num(JaqNum::Float(f)) if (f - 4.14).abs() < 0.001));
1✔
914
    }
1✔
915

916
    #[test]
917
    fn test_json_to_jaq_string() {
1✔
918
        let result = json_to_jaq(&json!("hello"));
1✔
919
        assert!(matches!(result, JaqVal::TStr(_)));
1✔
920
    }
1✔
921

922
    #[test]
923
    fn test_json_to_jaq_array() {
1✔
924
        let result = json_to_jaq(&json!([1, 2, 3]));
1✔
925
        assert!(matches!(result, JaqVal::Arr(_)));
1✔
926
    }
1✔
927

928
    #[test]
929
    fn test_json_to_jaq_object() {
1✔
930
        let result = json_to_jaq(&json!({"key": "value"}));
1✔
931
        assert!(matches!(result, JaqVal::Obj(_)));
1✔
932
    }
1✔
933
}
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