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

gripmock / grpctestify-rust / 24310555172

12 Apr 2026 03:55PM UTC coverage: 75.375% (+1.2%) from 74.127%
24310555172

Pull #32

github

web-flow
Merge 2715b3951 into 65f4b1f6e
Pull Request #32: multi requests

1930 of 2645 new or added lines in 23 files covered. (72.97%)

85 existing lines in 6 files now uncovered.

15868 of 21052 relevant lines covered (75.38%)

1533.91 hits per line

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

87.01
/src/assert/operators.rs
1
// Operators assertion engine
2
// Supports @plugin functions and custom operators (==, !=, contains, etc.)
3

4
use anyhow::Result;
5
use regex::Regex;
6
use serde_json::Value;
7
use std::cell::RefCell;
8
use std::collections::HashMap;
9
use std::rc::Rc;
10

11
use crate::assert::engine::AssertionResult;
12
use crate::plugins::{
13
    AssertionTiming, PluginContext, PluginManager, PluginResult, normalize_plugin_name,
14
};
15

16
thread_local! {
17
    static REGEX_CACHE: RefCell<HashMap<String, std::result::Result<Rc<Regex>, String>>> =
18
        RefCell::new(HashMap::new());
19
}
20

21
fn cached_regex(pattern: &str) -> std::result::Result<Rc<Regex>, String> {
4✔
22
    if let Some(cached) = REGEX_CACHE.with(|cache| cache.borrow().get(pattern).cloned()) {
4✔
23
        return cached;
×
24
    }
4✔
25

26
    let compiled = Regex::new(pattern)
4✔
27
        .map(Rc::new)
4✔
28
        .map_err(|err| err.to_string());
4✔
29

30
    REGEX_CACHE.with(|cache| {
4✔
31
        cache
4✔
32
            .borrow_mut()
4✔
33
            .insert(pattern.to_string(), compiled.clone());
4✔
34
    });
4✔
35

36
    compiled
4✔
37
}
4✔
38

39
/// Evaluate assertion expression (plugins and operators)
40
pub fn evaluate_assertion(
93✔
41
    plugin_manager: &PluginManager,
93✔
42
    assertion: &str,
93✔
43
    response: &Value,
93✔
44
    headers: Option<&HashMap<String, String>>,
93✔
45
    trailers: Option<&HashMap<String, String>>,
93✔
46
    timing: Option<&AssertionTiming>,
93✔
47
) -> Result<AssertionResult> {
93✔
48
    let trimmed = assertion.trim();
93✔
49

50
    // Check for built-in boolean functions first (e.g. @uuid(.id))
51
    if trimmed.starts_with('@')
93✔
52
        && !trimmed.contains("==")
60✔
53
        && !trimmed.contains("!=")
47✔
54
        && !trimmed.contains('>')
47✔
55
        && !trimmed.contains('<')
47✔
56
    {
57
        return evaluate_boolean_function(
47✔
58
            plugin_manager,
47✔
59
            trimmed,
47✔
60
            response,
47✔
61
            headers,
47✔
62
            trailers,
47✔
63
            timing,
47✔
64
        );
65
    }
46✔
66

67
    // List of operators sorted by length (descending)
68
    let operators = [
46✔
69
        "contains",
46✔
70
        "matches",
46✔
71
        "startsWith",
46✔
72
        "endsWith",
46✔
73
        "==",
46✔
74
        "!=",
46✔
75
        ">=",
46✔
76
        "<=",
46✔
77
        ">",
46✔
78
        "<",
46✔
79
    ];
46✔
80

81
    for op in operators {
229✔
82
        if let Some(idx) = trimmed.find(op) {
229✔
83
            let lhs_str = &trimmed[..idx].trim();
45✔
84
            let rhs_str = &trimmed[idx + op.len()..].trim();
45✔
85

86
            if lhs_str.is_empty() {
45✔
87
                continue;
×
88
            }
45✔
89

90
            // If LHS contains pipe '|', it's likely a JQ filter
91
            if lhs_str.contains('|') {
45✔
92
                continue;
×
93
            }
45✔
94

95
            // If LHS contains '(', it might be a function call.
96
            // Legacy only supports functions starting with '@'.
97
            if lhs_str.contains('(') && !lhs_str.trim().starts_with('@') {
45✔
98
                continue;
×
99
            }
45✔
100

101
            let lhs_val =
45✔
102
                evaluate_expression(plugin_manager, lhs_str, response, headers, trailers, timing);
45✔
103
            let rhs_val = parse_value(rhs_str);
45✔
104

105
            return compare(lhs_val, op, rhs_val, lhs_str, rhs_str);
45✔
106
        }
184✔
107
    }
108

109
    Ok(AssertionResult::Error(format!(
1✔
110
        "Unsupported assertion syntax: {}",
1✔
111
        assertion
1✔
112
    )))
1✔
113
}
93✔
114

115
fn evaluate_boolean_function(
47✔
116
    plugin_manager: &PluginManager,
47✔
117
    expr: &str,
47✔
118
    response: &Value,
47✔
119
    headers: Option<&HashMap<String, String>>,
47✔
120
    trailers: Option<&HashMap<String, String>>,
47✔
121
    timing: Option<&AssertionTiming>,
47✔
122
) -> Result<AssertionResult> {
47✔
123
    if let (Some(start_paren), Some(end_paren)) = (expr.find('('), expr.rfind(')')) {
47✔
124
        let func_name = &expr[0..start_paren];
47✔
125
        let arg_str = &expr[start_paren + 1..end_paren];
47✔
126

127
        let resolved_name = normalize_plugin_name(func_name);
47✔
128

129
        if let Some(plugin) = plugin_manager.get(resolved_name) {
47✔
130
            let context = PluginContext::new(response)
47✔
131
                .with_headers(headers)
47✔
132
                .with_trailers(trailers)
47✔
133
                .with_timing(timing);
47✔
134

135
            let args = parse_plugin_arguments(
47✔
136
                plugin_manager,
47✔
137
                arg_str,
47✔
138
                response,
47✔
139
                headers,
47✔
140
                trailers,
47✔
141
                timing,
47✔
142
            );
143

144
            return match plugin.execute(&args, &context) {
47✔
145
                Ok(PluginResult::Assertion(res)) => Ok(res),
40✔
146
                Ok(PluginResult::Value(val)) => {
7✔
147
                    if !val.is_null() && val != false {
7✔
148
                        Ok(AssertionResult::Pass)
5✔
149
                    } else {
150
                        Ok(AssertionResult::fail(format!(
2✔
151
                            "Plugin {} returned falsy value: {:?}",
2✔
152
                            resolved_name, val
2✔
153
                        )))
2✔
154
                    }
155
                }
156
                Err(e) => Ok(AssertionResult::Error(format!("Plugin error: {}", e))),
×
157
            };
158
        }
×
159
    }
×
160
    Ok(AssertionResult::Error(format!(
×
161
        "Invalid function call syntax: {}",
×
162
        expr
×
163
    )))
×
164
}
47✔
165

166
fn evaluate_expression(
45✔
167
    plugin_manager: &PluginManager,
45✔
168
    expr: &str,
45✔
169
    response: &Value,
45✔
170
    headers: Option<&HashMap<String, String>>,
45✔
171
    trailers: Option<&HashMap<String, String>>,
45✔
172
    timing: Option<&AssertionTiming>,
45✔
173
) -> Value {
45✔
174
    if expr.starts_with('@')
45✔
175
        && let (Some(start_paren), Some(end_paren)) = (expr.find('('), expr.rfind(')'))
13✔
176
    {
177
        let func_name = &expr[0..start_paren];
13✔
178
        let arg_str = &expr[start_paren + 1..end_paren];
13✔
179

180
        let resolved_name = normalize_plugin_name(func_name);
13✔
181

182
        if let Some(plugin) = plugin_manager.get(resolved_name) {
13✔
183
            let context = PluginContext::new(response)
13✔
184
                .with_headers(headers)
13✔
185
                .with_trailers(trailers)
13✔
186
                .with_timing(timing);
13✔
187

188
            let args = parse_plugin_arguments(
13✔
189
                plugin_manager,
13✔
190
                arg_str,
13✔
191
                response,
13✔
192
                headers,
13✔
193
                trailers,
13✔
194
                timing,
13✔
195
            );
196

197
            match plugin.execute(&args, &context) {
13✔
198
                Ok(PluginResult::Value(v)) => return v,
13✔
199
                _ => return Value::Null,
×
200
            }
201
        }
×
202
    }
32✔
203
    resolve_path(expr, response)
32✔
204
}
45✔
205

206
fn parse_value(s: &str) -> Value {
56✔
207
    if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
56✔
208
        let inner = s.trim_matches('"');
24✔
209
        Value::String(inner.to_string())
24✔
210
    } else if s == "true" {
32✔
211
        Value::Bool(true)
4✔
212
    } else if s == "false" {
28✔
213
        Value::Bool(false)
1✔
214
    } else if s == "null" {
27✔
215
        Value::Null
×
216
    } else if let Ok(i) = s.parse::<i64>() {
27✔
217
        Value::Number(serde_json::Number::from(i))
27✔
218
    } else if let Ok(f) = s.parse::<f64>() {
×
219
        serde_json::Number::from_f64(f)
×
220
            .map(Value::Number)
×
221
            .unwrap_or(Value::Null)
×
222
    } else {
223
        Value::String(s.to_string())
×
224
    }
225
}
56✔
226

227
fn compare_numeric_values(lhs: &Value, rhs: &Value, op: &str) -> Option<bool> {
9✔
228
    let lhs_num = lhs.as_number()?;
9✔
229
    let rhs_num = rhs.as_number()?;
9✔
230

231
    let lhs_i = lhs_num
8✔
232
        .as_i64()
8✔
233
        .map(i128::from)
8✔
234
        .or_else(|| lhs_num.as_u64().map(i128::from));
8✔
235
    let rhs_i = rhs_num
8✔
236
        .as_i64()
8✔
237
        .map(i128::from)
8✔
238
        .or_else(|| rhs_num.as_u64().map(i128::from));
8✔
239

240
    if let (Some(l), Some(r)) = (lhs_i, rhs_i) {
8✔
241
        return Some(match op {
8✔
242
            ">" => l > r,
8✔
243
            "<" => l < r,
6✔
244
            ">=" => l >= r,
4✔
245
            "<=" => l <= r,
2✔
246
            _ => return None,
×
247
        });
248
    }
×
249

250
    let (l, r) = (lhs_num.as_f64()?, rhs_num.as_f64()?);
×
251
    Some(match op {
×
252
        ">" => l > r,
×
253
        "<" => l < r,
×
254
        ">=" => l >= r,
×
255
        "<=" => l <= r,
×
256
        _ => return None,
×
257
    })
258
}
9✔
259

260
fn parse_plugin_arguments(
60✔
261
    plugin_manager: &PluginManager,
60✔
262
    arg_str: &str,
60✔
263
    response: &Value,
60✔
264
    headers: Option<&HashMap<String, String>>,
60✔
265
    trailers: Option<&HashMap<String, String>>,
60✔
266
    timing: Option<&AssertionTiming>,
60✔
267
) -> Vec<Value> {
60✔
268
    split_arguments(arg_str)
60✔
269
        .into_iter()
60✔
270
        .map(|token| {
60✔
271
            parse_argument_value(plugin_manager, token, response, headers, trailers, timing)
60✔
272
        })
60✔
273
        .collect()
60✔
274
}
60✔
275

276
fn split_arguments(input: &str) -> Vec<&str> {
63✔
277
    let trimmed = input.trim();
63✔
278
    if trimmed.is_empty() {
63✔
279
        return Vec::new();
1✔
280
    }
62✔
281

282
    let mut out = Vec::new();
62✔
283
    let mut start = 0;
62✔
284
    let mut depth = 0;
62✔
285
    let mut in_string = false;
62✔
286
    let mut escaped = false;
62✔
287

288
    for (idx, ch) in trimmed.char_indices() {
387✔
289
        if in_string {
387✔
290
            if escaped {
79✔
291
                escaped = false;
×
292
                continue;
×
293
            }
79✔
294
            if ch == '\\' {
79✔
295
                escaped = true;
×
296
                continue;
×
297
            }
79✔
298
            if ch == '"' {
79✔
299
                in_string = false;
7✔
300
            }
72✔
301
            continue;
79✔
302
        }
308✔
303

304
        match ch {
3✔
305
            '"' => in_string = true,
7✔
306
            '(' | '[' | '{' => depth += 1,
2✔
307
            ')' | ']' | '}' => {
308
                if depth > 0 {
2✔
309
                    depth -= 1;
2✔
310
                }
2✔
311
            }
312
            ',' if depth == 0 => {
3✔
313
                out.push(trimmed[start..idx].trim());
3✔
314
                start = idx + 1;
3✔
315
            }
3✔
316
            _ => {}
294✔
317
        }
318
    }
319

320
    out.push(trimmed[start..].trim());
62✔
321
    out
62✔
322
}
63✔
323

324
fn parse_argument_value(
60✔
325
    plugin_manager: &PluginManager,
60✔
326
    token: &str,
60✔
327
    response: &Value,
60✔
328
    headers: Option<&HashMap<String, String>>,
60✔
329
    trailers: Option<&HashMap<String, String>>,
60✔
330
    timing: Option<&AssertionTiming>,
60✔
331
) -> Value {
60✔
332
    let t = token.trim();
60✔
333
    if t.is_empty() {
60✔
334
        return Value::Null;
×
335
    }
60✔
336

337
    if t.starts_with('@') && t.contains('(') && t.ends_with(')') {
60✔
338
        return evaluate_expression(plugin_manager, t, response, headers, trailers, timing);
×
339
    }
60✔
340

341
    if t == "." || t.starts_with('.') {
60✔
342
        return resolve_path(t, response);
51✔
343
    }
9✔
344

345
    if (t.starts_with('"') && t.ends_with('"') && t.len() >= 2)
9✔
346
        || t == "true"
2✔
347
        || t == "false"
2✔
348
        || t == "null"
2✔
349
        || t.parse::<f64>().is_ok()
2✔
350
    {
351
        return parse_value(t);
7✔
352
    }
2✔
353

354
    Value::String(t.to_string())
2✔
355
}
60✔
356

357
fn compare(
45✔
358
    lhs: Value,
45✔
359
    op: &str,
45✔
360
    rhs: Value,
45✔
361
    lhs_expr: &str,
45✔
362
    rhs_expr: &str,
45✔
363
) -> Result<AssertionResult> {
45✔
364
    let pass = match op {
45✔
365
        "==" => lhs == rhs,
45✔
366
        "!=" => lhs != rhs,
12✔
367
        ">" => compare_numeric_values(&lhs, &rhs, op).unwrap_or(false),
11✔
368
        "<" => compare_numeric_values(&lhs, &rhs, op).unwrap_or(false),
10✔
369
        ">=" => compare_numeric_values(&lhs, &rhs, op).unwrap_or(false),
9✔
370
        "<=" => compare_numeric_values(&lhs, &rhs, op).unwrap_or(false),
8✔
371
        "contains" => match (&lhs, &rhs) {
7✔
372
            (Value::String(l), Value::String(r)) => l.contains(r),
2✔
373
            (Value::Array(l), r) => l.contains(r),
1✔
374
            (Value::Object(l), Value::String(r)) => l.contains_key(r),
×
375
            _ => false,
×
376
        },
377
        "startsWith" => match (&lhs, &rhs) {
4✔
378
            (Value::String(l), Value::String(r)) => l.starts_with(r),
1✔
379
            _ => false,
×
380
        },
381
        "endsWith" => match (&lhs, &rhs) {
3✔
382
            (Value::String(l), Value::String(r)) => l.ends_with(r),
1✔
383
            _ => false,
×
384
        },
385
        "matches" => match (&lhs, &rhs) {
2✔
386
            (Value::String(l), Value::String(r)) => {
2✔
387
                if let Ok(re) = cached_regex(r) {
2✔
388
                    re.is_match(l)
2✔
389
                } else {
390
                    return Ok(AssertionResult::Error(format!("Invalid regex: {}", r)));
×
391
                }
392
            }
393
            _ => false,
×
394
        },
395
        _ => return Ok(AssertionResult::Error(format!("Unknown operator: {}", op))),
×
396
    };
397

398
    if pass {
45✔
399
        Ok(AssertionResult::Pass)
36✔
400
    } else {
401
        Ok(AssertionResult::Fail {
9✔
402
            message: format!(
9✔
403
                "Assertion failed: {} {} {} (Values: {:?} vs {:?})",
9✔
404
                lhs_expr, op, rhs_expr, lhs, rhs
9✔
405
            ),
9✔
406
            expected: Some(format!("{} {:?}", op, rhs)),
9✔
407
            actual: Some(format!("{:?}", lhs)),
9✔
408
        })
9✔
409
    }
410
}
45✔
411

412
/// Evaluate a JQ path expression using the jaq engine.
413
/// Delegates to jaq's native parser — handles all JQ syntax:
414
///   .foo["bar"].baz, .arr[0], .obj["key"]["nested"], etc.
415
fn resolve_path(path: &str, root: &Value) -> Value {
87✔
416
    if path == "." {
87✔
417
        return root.clone();
×
418
    }
87✔
419
    eval_jaq_one(path, root).unwrap_or(Value::Null)
87✔
420
}
87✔
421

422
/// Evaluate a JQ expression and return the first result as serde_json::Value.
423
fn eval_jaq_one(expr: &str, input: &Value) -> anyhow::Result<Value> {
87✔
424
    use jaq_core::defs as core_defs;
425
    use jaq_core::funs as core_funs;
426
    use jaq_core::{Compiler, Ctx, Vars, data, load, unwrap_valr};
427
    use jaq_json::Val as JaqVal;
428

429
    let arena = load::Arena::default();
87✔
430
    let defs = core_defs().chain(jaq_std::defs()).chain(jaq_json::defs());
87✔
431
    let funs = core_funs().chain(jaq_std::funs()).chain(jaq_json::funs());
87✔
432
    let loader = load::Loader::new(defs);
87✔
433
    let program = load::File {
87✔
434
        code: expr,
87✔
435
        path: (),
87✔
436
    };
87✔
437

438
    let modules = loader
87✔
439
        .load(&arena, program)
87✔
440
        .map_err(|errs| anyhow::anyhow!("JQ parse error: {:?}", errs))?;
87✔
441

442
    let filter = Compiler::default()
87✔
443
        .with_funs(funs)
87✔
444
        .compile(modules)
87✔
445
        .map_err(|errs| anyhow::anyhow!("JQ compile error: {:?}", errs))?;
87✔
446

447
    let jaq_input = to_jaq_val(input);
87✔
448
    let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
87✔
449
    let mut out = filter.id.run((ctx, jaq_input)).map(unwrap_valr);
87✔
450

451
    if let Some(Ok(val)) = out.next() {
87✔
452
        Ok(from_jaq_val(&val))
87✔
453
    } else {
NEW
454
        Err(anyhow::anyhow!("JQ produced no output"))
×
455
    }
456
}
87✔
457

458
/// Convert serde_json::Value → jaq_json::Val
459
fn to_jaq_val(v: &Value) -> jaq_json::Val {
428✔
460
    use jaq_json::Num as JaqNum;
461
    match v {
428✔
NEW
462
        Value::Null => jaq_json::Val::Null,
×
463
        Value::Bool(b) => jaq_json::Val::Bool(*b),
21✔
464
        Value::Number(n) => {
75✔
465
            if let Some(i) = n.as_i64() {
75✔
466
                #[allow(clippy::cast_possible_wrap)]
467
                let isize_val = i as isize;
75✔
468
                jaq_json::Val::Num(JaqNum::Int(isize_val))
75✔
NEW
469
            } else if let Some(f) = n.as_f64() {
×
NEW
470
                jaq_json::Val::Num(JaqNum::Float(f))
×
471
            } else {
NEW
472
                jaq_json::Val::Null
×
473
            }
474
        }
475
        Value::String(s) => jaq_json::Val::utf8_str(bytes::Bytes::from(s.clone())),
185✔
476
        Value::Array(arr) => {
34✔
477
            jaq_json::Val::Arr(std::rc::Rc::new(arr.iter().map(to_jaq_val).collect()))
34✔
478
        }
479
        Value::Object(map) => {
113✔
480
            let entries: Vec<(jaq_json::Val, jaq_json::Val)> = map
113✔
481
                .iter()
113✔
482
                .map(|(k, v)| {
248✔
483
                    (
248✔
484
                        jaq_json::Val::utf8_str(bytes::Bytes::from(k.clone())),
248✔
485
                        to_jaq_val(v),
248✔
486
                    )
248✔
487
                })
248✔
488
                .collect();
113✔
489
            jaq_json::Val::Obj(std::rc::Rc::new(jaq_json::Map::from_iter(entries)))
113✔
490
        }
491
    }
492
}
428✔
493

494
/// Convert jaq_json::Val → serde_json::Value
495
fn from_jaq_val(v: &jaq_json::Val) -> Value {
106✔
496
    match v {
106✔
497
        jaq_json::Val::Null => Value::Null,
1✔
498
        jaq_json::Val::Bool(b) => Value::Bool(*b),
1✔
499
        jaq_json::Val::Num(n) => match n {
31✔
500
            jaq_json::Num::Int(i) => Value::Number(serde_json::Number::from(*i as i64)),
31✔
NEW
501
            jaq_json::Num::BigInt(_) => Value::Null,
×
NEW
502
            jaq_json::Num::Float(f) => serde_json::Number::from_f64(*f)
×
NEW
503
                .map(Value::Number)
×
NEW
504
                .unwrap_or(Value::Null),
×
NEW
505
            jaq_json::Num::Dec(f) => f
×
NEW
506
                .parse::<f64>()
×
NEW
507
                .ok()
×
NEW
508
                .and_then(serde_json::Number::from_f64)
×
NEW
509
                .map(Value::Number)
×
NEW
510
                .unwrap_or(Value::Null),
×
511
        },
512
        jaq_json::Val::BStr(b) | jaq_json::Val::TStr(b) => {
64✔
513
            String::from_utf8_lossy(b).into_owned().into()
64✔
514
        }
515
        jaq_json::Val::Arr(arr) => Value::Array(arr.iter().map(from_jaq_val).collect()),
9✔
NEW
516
        jaq_json::Val::Obj(map) => {
×
NEW
517
            let entries: serde_json::Map<String, Value> = map
×
NEW
518
                .iter()
×
NEW
519
                .map(|(k, v)| {
×
NEW
520
                    let key = match k {
×
NEW
521
                        jaq_json::Val::TStr(b) | jaq_json::Val::BStr(b) => {
×
NEW
522
                            String::from_utf8_lossy(b).into_owned()
×
523
                        }
NEW
524
                        _ => k.to_string(),
×
525
                    };
NEW
526
                    (key, from_jaq_val(v))
×
NEW
527
                })
×
NEW
528
                .collect();
×
NEW
529
            Value::Object(entries)
×
530
        }
531
    }
532
}
106✔
533

534
#[cfg(test)]
535
mod tests {
536
    use super::*;
537
    use serde_json::json;
538

539
    fn create_plugin_manager() -> PluginManager {
6✔
540
        PluginManager::new()
6✔
541
    }
6✔
542

543
    #[test]
544
    fn test_evaluate_assertion_equality() {
1✔
545
        let pm = create_plugin_manager();
1✔
546
        let response = json!({"status": "success"});
1✔
547
        let result =
1✔
548
            evaluate_assertion(&pm, ".status == \"success\"", &response, None, None, None).unwrap();
1✔
549
        assert!(matches!(result, AssertionResult::Pass));
1✔
550
    }
1✔
551

552
    #[test]
553
    fn test_evaluate_assertion_inequality() {
1✔
554
        let pm = create_plugin_manager();
1✔
555
        let response = json!({"status": "success"});
1✔
556
        let result =
1✔
557
            evaluate_assertion(&pm, ".status == \"error\"", &response, None, None, None).unwrap();
1✔
558
        assert!(matches!(result, AssertionResult::Fail { .. }));
1✔
559
    }
1✔
560

561
    #[test]
562
    fn test_evaluate_assertion_contains() {
1✔
563
        let pm = create_plugin_manager();
1✔
564
        let response = json!({"name": "test"});
1✔
565
        let result =
1✔
566
            evaluate_assertion(&pm, ".name contains \"te\"", &response, None, None, None).unwrap();
1✔
567
        assert!(matches!(result, AssertionResult::Pass));
1✔
568
    }
1✔
569

570
    #[test]
571
    fn test_evaluate_assertion_plugin() {
1✔
572
        let pm = create_plugin_manager();
1✔
573
        let response = json!({"id": "550e8400-e29b-41d4-a716-446655440000"});
1✔
574
        let result = evaluate_assertion(&pm, "@uuid(.id)", &response, None, None, None).unwrap();
1✔
575
        assert!(matches!(result, AssertionResult::Pass));
1✔
576
    }
1✔
577

578
    #[test]
579
    fn test_evaluate_assertion_has_header_unquoted_argument() {
1✔
580
        let pm = create_plugin_manager();
1✔
581
        let response = json!({});
1✔
582
        let mut headers = HashMap::new();
1✔
583
        headers.insert("content-type".to_string(), "application/json".to_string());
1✔
584

585
        let result = evaluate_assertion(
1✔
586
            &pm,
1✔
587
            "@has_header(content-type) == true",
1✔
588
            &response,
1✔
589
            Some(&headers),
1✔
590
            None,
1✔
591
            None,
1✔
592
        )
593
        .unwrap();
1✔
594

595
        assert!(matches!(result, AssertionResult::Pass));
1✔
596
    }
1✔
597

598
    #[test]
599
    fn test_evaluate_assertion_trailer_value_plugin() {
1✔
600
        let pm = create_plugin_manager();
1✔
601
        let response = json!({});
1✔
602
        let mut trailers = HashMap::new();
1✔
603
        trailers.insert("grpc-status".to_string(), "0".to_string());
1✔
604

605
        let result = evaluate_assertion(
1✔
606
            &pm,
1✔
607
            "@trailer(\"grpc-status\") == \"0\"",
1✔
608
            &response,
1✔
609
            None,
1✔
610
            Some(&trailers),
1✔
611
            None,
1✔
612
        )
613
        .unwrap();
1✔
614

615
        assert!(matches!(result, AssertionResult::Pass));
1✔
616
    }
1✔
617

618
    #[test]
619
    fn test_resolve_path_simple() {
1✔
620
        let response = json!({"key": "value"});
1✔
621
        let result = resolve_path(".key", &response);
1✔
622
        assert_eq!(result, json!("value"));
1✔
623
    }
1✔
624

625
    #[test]
626
    fn test_resolve_path_nested() {
1✔
627
        let response = json!({"outer": {"inner": "value"}});
1✔
628
        let result = resolve_path(".outer.inner", &response);
1✔
629
        assert_eq!(result, json!("value"));
1✔
630
    }
1✔
631

632
    #[test]
633
    fn test_parse_value_string() {
1✔
634
        assert_eq!(parse_value("\"hello\""), json!("hello"));
1✔
635
    }
1✔
636

637
    #[test]
638
    fn test_parse_value_number() {
1✔
639
        assert_eq!(parse_value("123"), json!(123));
1✔
640
    }
1✔
641

642
    #[test]
643
    fn test_parse_value_bool() {
1✔
644
        assert_eq!(parse_value("true"), json!(true));
1✔
645
        assert_eq!(parse_value("false"), json!(false));
1✔
646
    }
1✔
647

648
    #[test]
649
    fn test_cached_regex_valid() {
1✔
650
        let result = cached_regex(r"\d+");
1✔
651
        assert!(result.is_ok());
1✔
652
    }
1✔
653

654
    #[test]
655
    fn test_cached_regex_invalid() {
1✔
656
        let result = cached_regex(r"[");
1✔
657
        assert!(result.is_err());
1✔
658
    }
1✔
659

660
    #[test]
661
    fn test_compare_numeric_greater() {
1✔
662
        let lhs = json!(5);
1✔
663
        let rhs = json!(3);
1✔
664
        assert_eq!(compare_numeric_values(&lhs, &rhs, ">"), Some(true));
1✔
665
    }
1✔
666

667
    #[test]
668
    fn test_compare_numeric_less() {
1✔
669
        let lhs = json!(3);
1✔
670
        let rhs = json!(5);
1✔
671
        assert_eq!(compare_numeric_values(&lhs, &rhs, "<"), Some(true));
1✔
672
    }
1✔
673

674
    #[test]
675
    fn test_compare_numeric_equality() {
1✔
676
        let lhs = json!(5);
1✔
677
        let rhs = json!(5);
1✔
678
        assert_eq!(compare_numeric_values(&lhs, &rhs, ">="), Some(true));
1✔
679
        assert_eq!(compare_numeric_values(&lhs, &rhs, "<="), Some(true));
1✔
680
    }
1✔
681

682
    #[test]
683
    fn test_compare_numeric_mixed_types() {
1✔
684
        let lhs = json!(5);
1✔
685
        let rhs = json!("5");
1✔
686
        assert_eq!(compare_numeric_values(&lhs, &rhs, ">"), None);
1✔
687
    }
1✔
688

689
    #[test]
690
    fn test_resolve_path_array_index() {
1✔
691
        let root = json!({"items": ["first", "second"]});
1✔
692
        let result = resolve_path(".items[0]", &root);
1✔
693
        assert_eq!(result, json!("first"));
1✔
694
    }
1✔
695

696
    #[test]
697
    fn test_resolve_path_missing_key() {
1✔
698
        let root = json!({"a": 1});
1✔
699
        let result = resolve_path(".missing", &root);
1✔
700
        assert!(result.is_null());
1✔
701
    }
1✔
702

703
    #[test]
704
    fn test_split_arguments_simple() {
1✔
705
        let args = split_arguments("arg1, arg2, arg3");
1✔
706
        assert_eq!(args.len(), 3);
1✔
707
    }
1✔
708

709
    #[test]
710
    fn test_split_arguments_empty() {
1✔
711
        let args = split_arguments("");
1✔
712
        assert!(args.is_empty());
1✔
713
    }
1✔
714

715
    #[test]
716
    fn test_split_arguments_with_parens() {
1✔
717
        let args = split_arguments("@len(.x), @empty(.y)");
1✔
718
        assert_eq!(args.len(), 2);
1✔
719
    }
1✔
720
}
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