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

gripmock / grpctestify-rust / 29052555164

09 Jul 2026 09:47PM UTC coverage: 72.078% (-6.1%) from 78.205%
29052555164

Pull #46

github

web-flow
Merge b719e7c99 into c7b8c50f3
Pull Request #46: [1.7] add BENCH section

27162 of 37684 relevant lines covered (72.08%)

26935.5 hits per line

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

71.13
/src/execution/response_handler.rs
1
// Response Handler - handles response validation and processing
2

3
use crate::assert::{AssertionEngine, JsonComparator};
4
use crate::execution::runner::{TestExecutionResult, TestExecutionStatus};
5
use crate::grpc::GrpcResponse;
6
use crate::parser::GctfDocument;
7
use crate::parser::ast::{InlineOptions, Section, SectionContent, SectionType};
8
use crate::plugins::PluginManager;
9
use serde_json::Value;
10
use std::collections::HashMap;
11
use std::sync::Arc;
12
use std::sync::LazyLock;
13

14
/// Response validation result
15
#[derive(Debug, Clone)]
16
pub struct ResponseValidationResult {
17
    pub status: TestExecutionStatus,
18
    pub failure_reasons: Vec<String>,
19
}
20

21
/// Response Handler - validates responses against expected values
22
pub struct ResponseHandler {
23
    no_assert: bool,
24
    assertion_engine: AssertionEngine,
25
}
26

27
static PLUGIN_REGISTRY: LazyLock<Arc<dyn apif_assert::registry::PluginRegistry>> =
28
    LazyLock::new(|| Arc::new(PluginManager::new()));
3✔
29

30
impl ResponseHandler {
31
    /// Create new response handler
32
    pub fn new(no_assert: bool) -> Self {
28✔
33
        Self {
28✔
34
            no_assert,
28✔
35
            assertion_engine: AssertionEngine::with_registry(PLUGIN_REGISTRY.clone()),
28✔
36
        }
28✔
37
    }
28✔
38

39
    /// Validate a single response message against expected value
40
    pub fn validate_message(
4✔
41
        &self,
4✔
42
        actual: &Value,
4✔
43
        expected: &Value,
4✔
44
        options: &InlineOptions,
4✔
45
    ) -> Result<(), String> {
4✔
46
        if self.no_assert {
4✔
47
            return Ok(());
1✔
48
        }
3✔
49

50
        let expected_clone = expected.clone();
3✔
51

52
        let diffs = JsonComparator::compare(actual, &expected_clone, options);
3✔
53

54
        if !diffs.is_empty() {
3✔
55
            let mut messages = Vec::new();
1✔
56
            for diff in diffs {
1✔
57
                match diff {
1✔
58
                    crate::assert::AssertionResult::Fail {
59
                        message,
1✔
60
                        expected: exp,
1✔
61
                        actual: act,
1✔
62
                    } => {
63
                        let mut msg = format!("  - {}", message);
1✔
64
                        if let (Some(e), Some(a)) = (exp, act) {
1✔
65
                            msg.push_str(&format!(
1✔
66
                                "\n      Expected: {}\n      Actual:   {}",
1✔
67
                                e, a
1✔
68
                            ));
1✔
69
                        }
1✔
70
                        messages.push(msg);
1✔
71
                    }
72
                    crate::assert::AssertionResult::Error(m) => {
×
73
                        messages.push(format!("  - Error: {}", m))
×
74
                    }
75
                    _ => {}
×
76
                }
77
            }
78
            return Err(messages.join("\n"));
1✔
79
        }
2✔
80

81
        Ok(())
2✔
82
    }
4✔
83

84
    /// Get expected values for a response section
85
    fn expected_values_for_section(
16✔
86
        section: &Section,
16✔
87
        variables: &HashMap<String, Value>,
16✔
88
    ) -> Vec<Value> {
16✔
89
        match &section.content {
16✔
90
            SectionContent::Json(value) => {
16✔
91
                let mut expected = value.clone();
16✔
92
                // Substitute variables in expected value
93
                Self::substitute_variables_in_value(&mut expected, variables);
16✔
94
                vec![expected]
16✔
95
            }
96
            SectionContent::JsonLines(values) => values
×
97
                .iter()
×
98
                .map(|v: &Value| {
×
99
                    let mut expected = v.clone();
×
100
                    Self::substitute_variables_in_value(&mut expected, variables);
×
101
                    expected
×
102
                })
×
103
                .collect(),
×
104
            _ => Vec::new(),
×
105
        }
106
    }
16✔
107

108
    /// Substitute variables in a JSON value (public for use by orchestrator)
109
    pub fn substitute_variables_in_value(value: &mut Value, variables: &HashMap<String, Value>) {
77✔
110
        match value {
77✔
111
            Value::String(s) => {
14✔
112
                for (var_name, var_value) in variables {
14✔
113
                    let pattern = format!("{{{{ {} }}}}", var_name);
2✔
114
                    if s.contains(&pattern) {
2✔
115
                        if let Value::String(replacement) = var_value {
1✔
116
                            *s = s.replace(&pattern, replacement);
1✔
117
                        } else {
1✔
118
                            *s = s.replace(&pattern, &var_value.to_string());
×
119
                        }
×
120
                    }
1✔
121
                }
122
            }
123
            Value::Array(arr) => {
4✔
124
                for item in arr {
8✔
125
                    Self::substitute_variables_in_value(item, variables);
8✔
126
                }
8✔
127
            }
128
            Value::Object(map) => {
36✔
129
                for (_, val) in map {
37✔
130
                    Self::substitute_variables_in_value(val, variables);
37✔
131
                }
37✔
132
            }
133
            _ => {}
23✔
134
        }
135
    }
77✔
136

137
    /// Validate a full document against a response (for testing purposes)
138
    pub fn validate_document(
12✔
139
        &self,
12✔
140
        document: &GctfDocument,
12✔
141
        response: &GrpcResponse,
12✔
142
    ) -> TestExecutionResult {
12✔
143
        let mut failure_reasons: Vec<String> = Vec::new();
12✔
144
        let mut variables: HashMap<String, Value> = HashMap::new();
12✔
145

146
        let mut message_iter = response.messages.iter();
12✔
147
        let sections = &document.sections;
12✔
148
        let mut skip_next_section = false;
12✔
149
        let mut last_message: Option<Value> = None;
12✔
150

151
        for (i, section) in sections.iter().enumerate() {
21✔
152
            if skip_next_section {
21✔
153
                skip_next_section = false;
4✔
154
                continue;
4✔
155
            }
17✔
156

157
            match section.section_type {
17✔
158
                SectionType::Response => {
159
                    let expected_values = Self::expected_values_for_section(section, &variables);
16✔
160
                    let mut received_messages_for_section: Vec<Value> = Vec::new();
16✔
161

162
                    for expected_template in expected_values {
16✔
163
                        if let Some(msg) = message_iter.next() {
16✔
164
                            last_message = Some(msg.clone());
15✔
165
                            received_messages_for_section.push(msg.clone());
15✔
166

167
                            if !self.no_assert {
15✔
168
                                let mut expected = expected_template.clone();
15✔
169
                                Self::substitute_variables_in_value(&mut expected, &variables);
15✔
170

171
                                let diffs = JsonComparator::compare(
15✔
172
                                    msg,
15✔
173
                                    &expected,
15✔
174
                                    &section.inline_options,
15✔
175
                                );
176

177
                                if !diffs.is_empty() {
15✔
178
                                    failure_reasons.push(format!(
3✔
179
                                        "Response mismatch at line {}:",
180
                                        section.start_line
181
                                    ));
182
                                    for diff in diffs {
4✔
183
                                        match diff {
4✔
184
                                            crate::assert::AssertionResult::Fail {
185
                                                message,
4✔
186
                                                expected: exp,
4✔
187
                                                actual: act,
4✔
188
                                            } => {
189
                                                let mut msg = format!("  - {}", message);
4✔
190
                                                if let (Some(exp), Some(act)) = (exp, act) {
4✔
191
                                                    msg.push_str(&format!(
3✔
192
                                                        "\n      Expected: {}\n      Actual:   {}",
3✔
193
                                                        exp, act
3✔
194
                                                    ));
3✔
195
                                                }
3✔
196
                                                failure_reasons.push(msg);
4✔
197
                                            }
198
                                            crate::assert::AssertionResult::Error(m) => {
×
199
                                                failure_reasons.push(format!("  - Error: {}", m))
×
200
                                            }
201
                                            _ => {}
×
202
                                        }
203
                                    }
204

205
                                    failure_reasons
3✔
206
                                        .push(crate::assert::get_json_diff(&expected, msg));
3✔
207
                                }
12✔
208
                            }
×
209
                        } else if !self.no_assert {
1✔
210
                            failure_reasons.push(format!(
1✔
211
                                "Expected message for RESPONSE section at line {}, but no more messages received",
212
                                section.start_line
213
                            ));
214
                            break;
1✔
215
                        }
×
216
                    }
217

218
                    if section.inline_options.with_asserts
16✔
219
                        && let Some(next_section) = sections.get(i + 1)
4✔
220
                        && next_section.section_type == SectionType::Asserts
4✔
221
                        && !self.no_assert
4✔
222
                        && let SectionContent::Assertions(lines) = &next_section.content
4✔
223
                    {
224
                        for msg in &received_messages_for_section {
4✔
225
                            let result = self.assertion_engine.evaluate_all(
4✔
226
                                lines,
4✔
227
                                msg,
4✔
228
                                Some(&response.headers),
4✔
229
                                Some(&response.trailers),
4✔
230
                            );
231

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

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

356
        if !failure_reasons.is_empty() {
12✔
357
            TestExecutionResult::fail(
5✔
358
                format!("Validation failed:\n  - {}", failure_reasons.join("\n  - ")),
5✔
359
                None,
5✔
360
            )
361
        } else {
362
            TestExecutionResult::pass(None)
7✔
363
        }
364
    }
12✔
365
}
366

367
#[cfg(test)]
368
mod tests {
369
    use super::*;
370
    use serde_json::json;
371

372
    #[test]
373
    fn test_response_handler_new() {
1✔
374
        let handler = ResponseHandler::new(false);
1✔
375
        assert!(!handler.no_assert);
1✔
376
    }
1✔
377

378
    #[test]
379
    fn test_validate_message_exact_match() {
1✔
380
        let handler = ResponseHandler::new(false);
1✔
381
        let actual = json!({"id": 123, "name": "test"});
1✔
382
        let expected = json!({"id": 123, "name": "test"});
1✔
383
        let options = InlineOptions::default();
1✔
384

385
        let result = handler.validate_message(&actual, &expected, &options);
1✔
386
        assert!(result.is_ok());
1✔
387
    }
1✔
388

389
    #[test]
390
    fn test_validate_message_mismatch() {
1✔
391
        let handler = ResponseHandler::new(false);
1✔
392
        let actual = json!({"id": 123, "name": "test"});
1✔
393
        let expected = json!({"id": 456, "name": "test"});
1✔
394
        let options = InlineOptions::default();
1✔
395

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

400
    #[test]
401
    fn test_validate_message_partial_match() {
1✔
402
        let handler = ResponseHandler::new(false);
1✔
403
        let actual = json!({"id": 123, "name": "test", "extra": "field"});
1✔
404
        let expected = json!({"id": 123});
1✔
405
        let options = InlineOptions {
1✔
406
            partial: true,
1✔
407
            ..InlineOptions::default()
1✔
408
        };
1✔
409

410
        let result = handler.validate_message(&actual, &expected, &options);
1✔
411
        assert!(result.is_ok());
1✔
412
    }
1✔
413

414
    #[test]
415
    fn test_substitute_variables_in_value() {
1✔
416
        let mut value = json!({"id": "{{ user_id }}", "name": "test"});
1✔
417
        let mut variables = HashMap::new();
1✔
418
        variables.insert("user_id".to_string(), json!("123"));
1✔
419

420
        ResponseHandler::substitute_variables_in_value(&mut value, &variables);
1✔
421

422
        assert_eq!(value["id"], "123");
1✔
423
        assert_eq!(value["name"], "test");
1✔
424
    }
1✔
425

426
    #[test]
427
    fn test_response_handler_no_assert() {
1✔
428
        let handler = ResponseHandler::new(true);
1✔
429
        let actual = json!({"id": 123});
1✔
430
        let expected = json!({"id": 456});
1✔
431
        let options = InlineOptions::default();
1✔
432

433
        // Should always pass when no_assert is true
434
        let result = handler.validate_message(&actual, &expected, &options);
1✔
435
        assert!(result.is_ok());
1✔
436
    }
1✔
437
}
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