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

gripmock / grpctestify-rust / 30248040547

27 Jul 2026 07:57AM UTC coverage: 80.574% (+6.4%) from 74.202%
30248040547

Pull #81

github

web-flow
Merge 55b8756ce into 67c08fb25
Pull Request #81: rhai

47513 of 58968 relevant lines covered (80.57%)

23229.44 hits per line

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

71.03
/src/execution/response_handler.rs
1
use crate::assert::{AssertionEngine, JsonComparator};
2
use crate::execution::runner::TestExecutionResult;
3
use crate::grpc::GrpcResponse;
4
use crate::parser::GctfDocument;
5
use crate::parser::ast::{InlineOptions, Section, SectionContent, SectionType};
6
use serde_json::Value;
7
use std::collections::HashMap;
8
use std::sync::Arc;
9
use std::sync::LazyLock;
10

11
pub struct ResponseHandler {
12
    no_assert: bool,
13
    assertion_engine: AssertionEngine,
14
}
15

16
static PLUGIN_REGISTRY: LazyLock<Arc<dyn apif_assert::registry::PluginRegistry>> =
17
    LazyLock::new(|| Arc::new(crate::execution::plugin_dir::build_plugin_manager()));
62✔
18

19
impl ResponseHandler {
20
    /// Create new response handler
21
    pub fn new(no_assert: bool) -> Self {
109✔
22
        Self {
109✔
23
            no_assert,
109✔
24
            assertion_engine: AssertionEngine::with_registry(PLUGIN_REGISTRY.clone()),
109✔
25
        }
109✔
26
    }
109✔
27

28
    /// Validate a single response message against expected value
29
    pub fn validate_message(
4✔
30
        &self,
4✔
31
        actual: &Value,
4✔
32
        expected: &Value,
4✔
33
        options: &InlineOptions,
4✔
34
    ) -> Result<(), String> {
4✔
35
        if self.no_assert {
4✔
36
            return Ok(());
1✔
37
        }
3✔
38

39
        let diffs = JsonComparator::compare(actual, expected, options);
3✔
40

41
        if !diffs.is_empty() {
3✔
42
            let mut messages = Vec::new();
1✔
43
            for diff in diffs {
1✔
44
                match diff {
1✔
45
                    crate::assert::AssertionResult::Fail {
46
                        message,
1✔
47
                        expected: exp,
1✔
48
                        actual: act,
1✔
49
                    } => {
50
                        let mut msg = format!("  - {}", message);
1✔
51
                        if let (Some(e), Some(a)) = (exp, act) {
1✔
52
                            msg.push_str(&format!(
1✔
53
                                "\n      Expected: {}\n      Actual:   {}",
1✔
54
                                e, a
1✔
55
                            ));
1✔
56
                        }
1✔
57
                        messages.push(msg);
1✔
58
                    }
59
                    crate::assert::AssertionResult::Error(m) => {
×
60
                        messages.push(format!("  - Error: {}", m))
×
61
                    }
62
                    _ => {}
×
63
                }
64
            }
65
            return Err(messages.join("\n"));
1✔
66
        }
2✔
67

68
        Ok(())
2✔
69
    }
4✔
70

71
    /// Get expected values for a response section
72
    fn expected_values_for_section(
16✔
73
        section: &Section,
16✔
74
        variables: &HashMap<String, Value>,
16✔
75
    ) -> Vec<Value> {
16✔
76
        match &section.content {
16✔
77
            SectionContent::Json(value) => {
16✔
78
                let mut expected = value.clone();
16✔
79
                Self::substitute_variables_in_value(&mut expected, variables);
16✔
80
                vec![expected]
16✔
81
            }
82
            SectionContent::JsonLines(values) => values
×
83
                .iter()
×
84
                .map(|v: &Value| {
×
85
                    let mut expected = v.clone();
×
86
                    Self::substitute_variables_in_value(&mut expected, variables);
×
87
                    expected
×
88
                })
×
89
                .collect(),
×
90
            _ => Vec::new(),
×
91
        }
92
    }
16✔
93

94
    /// Substitute variables in a JSON value (public for use by orchestrator)
95
    pub fn substitute_variables_in_value(value: &mut Value, variables: &HashMap<String, Value>) {
77✔
96
        match value {
77✔
97
            Value::String(s) => {
14✔
98
                for (var_name, var_value) in variables {
14✔
99
                    let pattern = format!("{{{{ {} }}}}", var_name);
2✔
100
                    if s.contains(&pattern) {
2✔
101
                        if let Value::String(replacement) = var_value {
1✔
102
                            *s = s.replace(&pattern, replacement);
1✔
103
                        } else {
1✔
104
                            *s = s.replace(&pattern, &var_value.to_string());
×
105
                        }
×
106
                    }
1✔
107
                }
108
            }
109
            Value::Array(arr) => {
4✔
110
                for item in arr {
8✔
111
                    Self::substitute_variables_in_value(item, variables);
8✔
112
                }
8✔
113
            }
114
            Value::Object(map) => {
36✔
115
                for (_, val) in map {
37✔
116
                    Self::substitute_variables_in_value(val, variables);
37✔
117
                }
37✔
118
            }
119
            _ => {}
23✔
120
        }
121
    }
77✔
122

123
    /// Validate a full document against a response (for testing purposes)
124
    pub fn validate_document(
12✔
125
        &self,
12✔
126
        document: &GctfDocument,
12✔
127
        response: &GrpcResponse,
12✔
128
    ) -> TestExecutionResult {
12✔
129
        let mut failure_reasons: Vec<String> = Vec::new();
12✔
130
        let mut variables: HashMap<String, Value> = HashMap::new();
12✔
131

132
        let mut message_iter = response.messages.iter();
12✔
133
        let sections = &document.sections;
12✔
134
        let mut skip_next_section = false;
12✔
135
        let mut last_message: Option<Value> = None;
12✔
136

137
        for (i, section) in sections.iter().enumerate() {
21✔
138
            if skip_next_section {
21✔
139
                skip_next_section = false;
4✔
140
                continue;
4✔
141
            }
17✔
142

143
            match section.section_type {
17✔
144
                SectionType::Response => {
145
                    let expected_values = Self::expected_values_for_section(section, &variables);
16✔
146
                    let mut received_messages_for_section: Vec<Value> = Vec::new();
16✔
147

148
                    for expected_template in expected_values {
16✔
149
                        if let Some(msg) = message_iter.next() {
16✔
150
                            last_message = Some(msg.clone());
15✔
151
                            received_messages_for_section.push(msg.clone());
15✔
152

153
                            if !self.no_assert {
15✔
154
                                let mut expected = expected_template.clone();
15✔
155
                                Self::substitute_variables_in_value(&mut expected, &variables);
15✔
156

157
                                let diffs = JsonComparator::compare(
15✔
158
                                    msg,
15✔
159
                                    &expected,
15✔
160
                                    &section.inline_options,
15✔
161
                                );
162

163
                                if !diffs.is_empty() {
15✔
164
                                    failure_reasons.push(format!(
3✔
165
                                        "Response mismatch at line {}:",
166
                                        section.start_line
167
                                    ));
168
                                    for diff in diffs {
4✔
169
                                        match diff {
4✔
170
                                            crate::assert::AssertionResult::Fail {
171
                                                message,
4✔
172
                                                expected: exp,
4✔
173
                                                actual: act,
4✔
174
                                            } => {
175
                                                let mut msg = format!("  - {}", message);
4✔
176
                                                if let (Some(exp), Some(act)) = (exp, act) {
4✔
177
                                                    msg.push_str(&format!(
3✔
178
                                                        "\n      Expected: {}\n      Actual:   {}",
3✔
179
                                                        exp, act
3✔
180
                                                    ));
3✔
181
                                                }
3✔
182
                                                failure_reasons.push(msg);
4✔
183
                                            }
184
                                            crate::assert::AssertionResult::Error(m) => {
×
185
                                                failure_reasons.push(format!("  - Error: {}", m))
×
186
                                            }
187
                                            _ => {}
×
188
                                        }
189
                                    }
190

191
                                    failure_reasons
3✔
192
                                        .push(crate::assert::get_json_diff(&expected, msg));
3✔
193
                                }
12✔
194
                            }
×
195
                        } else if !self.no_assert {
1✔
196
                            failure_reasons.push(format!(
1✔
197
                                "Expected message for RESPONSE section at line {}, but no more messages received",
198
                                section.start_line
199
                            ));
200
                            break;
1✔
201
                        }
×
202
                    }
203

204
                    if section.inline_options.with_asserts
16✔
205
                        && let Some(next_section) = sections.get(i + 1)
4✔
206
                        && next_section.section_type == SectionType::Asserts
4✔
207
                        && !self.no_assert
4✔
208
                        && let SectionContent::Assertions(lines) = &next_section.content
4✔
209
                    {
210
                        for msg in &received_messages_for_section {
4✔
211
                            let result = self.assertion_engine.evaluate_all(
4✔
212
                                lines,
4✔
213
                                msg,
4✔
214
                                Some(&response.headers),
4✔
215
                                Some(&response.trailers),
4✔
216
                            );
217

218
                            if self.assertion_engine.has_failures(&result) {
4✔
219
                                for fail in self.assertion_engine.get_failures(&result) {
1✔
220
                                    match fail {
1✔
221
                                        crate::assert::AssertionResult::Fail {
222
                                            message,
1✔
223
                                            expected,
1✔
224
                                            actual,
1✔
225
                                        } => {
226
                                            let ctx = format!(
1✔
227
                                                "(attached to RESPONSE at line {})",
228
                                                section.start_line
229
                                            );
230
                                            failure_reasons.push(format!(
1✔
231
                                                "Assertion failed {}: {}",
232
                                                ctx, message
233
                                            ));
234
                                            if let (Some(exp), Some(act)) = (expected, actual) {
1✔
235
                                                failure_reasons.push(format!(
1✔
236
                                                    "    Expected: {}\n    Actual:   {}",
1✔
237
                                                    exp, act
1✔
238
                                                ));
1✔
239
                                            }
1✔
240
                                        }
241
                                        crate::assert::AssertionResult::Error(m) => {
×
242
                                            let ctx = format!(
×
243
                                                "(attached to RESPONSE at line {})",
×
244
                                                section.start_line
×
245
                                            );
×
246
                                            failure_reasons
×
247
                                                .push(format!("Assertion error {}: {}", ctx, m));
×
248
                                        }
×
249
                                        _ => {}
×
250
                                    }
251
                                }
252
                            }
3✔
253
                        }
254
                        skip_next_section = true;
4✔
255
                    }
12✔
256
                }
257
                SectionType::Asserts => {
258
                    if let Some(msg) = message_iter.next() {
×
259
                        last_message = Some(msg.clone());
×
260
                        if !self.no_assert
×
261
                            && let SectionContent::Assertions(lines) = &section.content
×
262
                        {
263
                            let result = self.assertion_engine.evaluate_all(
×
264
                                lines,
×
265
                                msg,
×
266
                                Some(&response.headers),
×
267
                                Some(&response.trailers),
×
268
                            );
269

270
                            if self.assertion_engine.has_failures(&result) {
×
271
                                for fail in self.assertion_engine.get_failures(&result) {
×
272
                                    match fail {
×
273
                                        crate::assert::AssertionResult::Fail {
274
                                            message,
×
275
                                            expected,
×
276
                                            actual,
×
277
                                        } => {
278
                                            let ctx = format!("at line {}", section.start_line);
×
279
                                            failure_reasons.push(format!(
×
280
                                                "Assertion failed {}: {}",
281
                                                ctx, message
282
                                            ));
283
                                            if let (Some(exp), Some(act)) = (expected, actual) {
×
284
                                                failure_reasons.push(format!(
×
285
                                                    "    Expected: {}\n    Actual:   {}",
×
286
                                                    exp, act
×
287
                                                ));
×
288
                                            }
×
289
                                        }
290
                                        crate::assert::AssertionResult::Error(m) => {
×
291
                                            let ctx = format!("at line {}", section.start_line);
×
292
                                            failure_reasons
×
293
                                                .push(format!("Assertion error {}: {}", ctx, m));
×
294
                                        }
×
295
                                        _ => {}
×
296
                                    }
297
                                }
298
                            }
×
299
                        }
×
300
                    } else if !self.no_assert {
×
301
                        failure_reasons.push(format!(
×
302
                            "Expected message for ASSERTS section at line {}, but no more messages received",
×
303
                            section.start_line
×
304
                        ));
×
305
                    }
×
306
                }
307
                SectionType::Extract => {
308
                    if let Some(msg) = &last_message {
1✔
309
                        if let SectionContent::Extract(extractions) = &section.content {
1✔
310
                            for (key, query) in extractions {
1✔
311
                                match self.assertion_engine.query(query, msg) {
1✔
312
                                    Ok(results) => {
1✔
313
                                        if let Some(val) = results.first() {
1✔
314
                                            variables.insert(key.clone(), val.clone());
1✔
315
                                        } else {
1✔
316
                                            failure_reasons.push(format!(
×
317
                                                 "Extraction failed at line {}: Query '{}' returned no results",
×
318
                                                 section.start_line, query
×
319
                                             ));
×
320
                                        }
×
321
                                    }
322
                                    Err(e) => {
×
323
                                        failure_reasons.push(format!(
×
324
                                            "Extraction error at line {}: {}",
×
325
                                            section.start_line, e
×
326
                                        ));
×
327
                                    }
×
328
                                }
329
                            }
330
                        }
×
331
                    } else {
×
332
                        failure_reasons.push(format!(
×
333
                            "EXTRACT at line {} requires a previous response message",
×
334
                            section.start_line
×
335
                        ));
×
336
                    }
×
337
                }
338
                _ => {}
×
339
            }
340
        }
341

342
        if !failure_reasons.is_empty() {
12✔
343
            TestExecutionResult::fail(
5✔
344
                format!("Validation failed:\n  - {}", failure_reasons.join("\n  - ")),
5✔
345
                None,
5✔
346
            )
347
        } else {
348
            TestExecutionResult::pass(None)
7✔
349
        }
350
    }
12✔
351
}
352

353
#[cfg(test)]
354
mod tests {
355
    use super::*;
356
    use serde_json::json;
357

358
    #[test]
359
    fn test_response_handler_new() {
1✔
360
        let handler = ResponseHandler::new(false);
1✔
361
        assert!(!handler.no_assert);
1✔
362
    }
1✔
363

364
    #[test]
365
    fn test_validate_message_exact_match() {
1✔
366
        let handler = ResponseHandler::new(false);
1✔
367
        let actual = json!({"id": 123, "name": "test"});
1✔
368
        let expected = json!({"id": 123, "name": "test"});
1✔
369
        let options = InlineOptions::default();
1✔
370

371
        let result = handler.validate_message(&actual, &expected, &options);
1✔
372
        assert!(result.is_ok());
1✔
373
    }
1✔
374

375
    #[test]
376
    fn test_validate_message_mismatch() {
1✔
377
        let handler = ResponseHandler::new(false);
1✔
378
        let actual = json!({"id": 123, "name": "test"});
1✔
379
        let expected = json!({"id": 456, "name": "test"});
1✔
380
        let options = InlineOptions::default();
1✔
381

382
        let result = handler.validate_message(&actual, &expected, &options);
1✔
383
        assert!(result.is_err());
1✔
384
    }
1✔
385

386
    #[test]
387
    fn test_validate_message_partial_match() {
1✔
388
        let handler = ResponseHandler::new(false);
1✔
389
        let actual = json!({"id": 123, "name": "test", "extra": "field"});
1✔
390
        let expected = json!({"id": 123});
1✔
391
        let options = InlineOptions {
1✔
392
            partial: true,
1✔
393
            ..InlineOptions::default()
1✔
394
        };
1✔
395

396
        let result = handler.validate_message(&actual, &expected, &options);
1✔
397
        assert!(result.is_ok());
1✔
398
    }
1✔
399

400
    #[test]
401
    fn test_substitute_variables_in_value() {
1✔
402
        let mut value = json!({"id": "{{ user_id }}", "name": "test"});
1✔
403
        let mut variables = HashMap::new();
1✔
404
        variables.insert("user_id".to_string(), json!("123"));
1✔
405

406
        ResponseHandler::substitute_variables_in_value(&mut value, &variables);
1✔
407

408
        assert_eq!(value["id"], "123");
1✔
409
        assert_eq!(value["name"], "test");
1✔
410
    }
1✔
411

412
    #[test]
413
    fn test_response_handler_no_assert() {
1✔
414
        let handler = ResponseHandler::new(true);
1✔
415
        let actual = json!({"id": 123});
1✔
416
        let expected = json!({"id": 456});
1✔
417
        let options = InlineOptions::default();
1✔
418

419
        // Should always pass when no_assert is true
420
        let result = handler.validate_message(&actual, &expected, &options);
1✔
421
        assert!(result.is_ok());
1✔
422
    }
1✔
423
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc