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

gripmock / grpctestify-rust / 30381691232

28 Jul 2026 05:10PM UTC coverage: 80.718% (+6.5%) from 74.202%
30381691232

Pull #81

github

web-flow
Merge 035df44eb into 67c08fb25
Pull Request #81: rhai

47619 of 58994 relevant lines covered (80.72%)

23219.58 hits per line

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

69.74
/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 {
110✔
22
        Self {
110✔
23
            no_assert,
110✔
24
            assertion_engine: AssertionEngine::with_registry(PLUGIN_REGISTRY.clone()),
110✔
25
        }
110✔
26
    }
110✔
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>) {
32✔
96
        crate::execution::runner_helpers::substitute_variables(value, variables);
32✔
97
    }
32✔
98

99
    /// Validate a full document against a response (for testing purposes)
100
    pub fn validate_document(
12✔
101
        &self,
12✔
102
        document: &GctfDocument,
12✔
103
        response: &GrpcResponse,
12✔
104
    ) -> TestExecutionResult {
12✔
105
        let mut failure_reasons: Vec<String> = Vec::new();
12✔
106
        let mut variables: HashMap<String, Value> = HashMap::new();
12✔
107

108
        let mut message_iter = response.messages.iter();
12✔
109
        let sections = &document.sections;
12✔
110
        let mut skip_next_section = false;
12✔
111
        let mut last_message: Option<Value> = None;
12✔
112

113
        for (i, section) in sections.iter().enumerate() {
21✔
114
            if skip_next_section {
21✔
115
                skip_next_section = false;
4✔
116
                continue;
4✔
117
            }
17✔
118

119
            match section.section_type {
17✔
120
                SectionType::Response => {
121
                    let expected_values = Self::expected_values_for_section(section, &variables);
16✔
122
                    let mut received_messages_for_section: Vec<Value> = Vec::new();
16✔
123

124
                    for expected_template in expected_values {
16✔
125
                        if let Some(msg) = message_iter.next() {
16✔
126
                            last_message = Some(msg.clone());
15✔
127
                            received_messages_for_section.push(msg.clone());
15✔
128

129
                            if !self.no_assert {
15✔
130
                                let mut expected = expected_template.clone();
15✔
131
                                Self::substitute_variables_in_value(&mut expected, &variables);
15✔
132

133
                                let diffs = JsonComparator::compare(
15✔
134
                                    msg,
15✔
135
                                    &expected,
15✔
136
                                    &section.inline_options,
15✔
137
                                );
138

139
                                if !diffs.is_empty() {
15✔
140
                                    failure_reasons.push(format!(
3✔
141
                                        "Response mismatch at line {}:",
142
                                        section.start_line
143
                                    ));
144
                                    for diff in diffs {
4✔
145
                                        match diff {
4✔
146
                                            crate::assert::AssertionResult::Fail {
147
                                                message,
4✔
148
                                                expected: exp,
4✔
149
                                                actual: act,
4✔
150
                                            } => {
151
                                                let mut msg = format!("  - {}", message);
4✔
152
                                                if let (Some(exp), Some(act)) = (exp, act) {
4✔
153
                                                    msg.push_str(&format!(
3✔
154
                                                        "\n      Expected: {}\n      Actual:   {}",
3✔
155
                                                        exp, act
3✔
156
                                                    ));
3✔
157
                                                }
3✔
158
                                                failure_reasons.push(msg);
4✔
159
                                            }
160
                                            crate::assert::AssertionResult::Error(m) => {
×
161
                                                failure_reasons.push(format!("  - Error: {}", m))
×
162
                                            }
163
                                            _ => {}
×
164
                                        }
165
                                    }
166

167
                                    failure_reasons
3✔
168
                                        .push(crate::assert::get_json_diff(&expected, msg));
3✔
169
                                }
12✔
170
                            }
×
171
                        } else if !self.no_assert {
1✔
172
                            failure_reasons.push(format!(
1✔
173
                                "Expected message for RESPONSE section at line {}, but no more messages received",
174
                                section.start_line
175
                            ));
176
                            break;
1✔
177
                        }
×
178
                    }
179

180
                    if section.inline_options.with_asserts
16✔
181
                        && let Some(next_section) = sections.get(i + 1)
4✔
182
                        && next_section.section_type == SectionType::Asserts
4✔
183
                        && !self.no_assert
4✔
184
                        && let SectionContent::Assertions(lines) = &next_section.content
4✔
185
                    {
186
                        for msg in &received_messages_for_section {
4✔
187
                            let result = self.assertion_engine.evaluate_all(
4✔
188
                                lines,
4✔
189
                                msg,
4✔
190
                                Some(&response.headers),
4✔
191
                                Some(&response.trailers),
4✔
192
                            );
193

194
                            if self.assertion_engine.has_failures(&result) {
4✔
195
                                for fail in self.assertion_engine.get_failures(&result) {
1✔
196
                                    match fail {
1✔
197
                                        crate::assert::AssertionResult::Fail {
198
                                            message,
1✔
199
                                            expected,
1✔
200
                                            actual,
1✔
201
                                        } => {
202
                                            let ctx = format!(
1✔
203
                                                "(attached to RESPONSE at line {})",
204
                                                section.start_line
205
                                            );
206
                                            failure_reasons.push(format!(
1✔
207
                                                "Assertion failed {}: {}",
208
                                                ctx, message
209
                                            ));
210
                                            if let (Some(exp), Some(act)) = (expected, actual) {
1✔
211
                                                failure_reasons.push(format!(
1✔
212
                                                    "    Expected: {}\n    Actual:   {}",
1✔
213
                                                    exp, act
1✔
214
                                                ));
1✔
215
                                            }
1✔
216
                                        }
217
                                        crate::assert::AssertionResult::Error(m) => {
×
218
                                            let ctx = format!(
×
219
                                                "(attached to RESPONSE at line {})",
×
220
                                                section.start_line
×
221
                                            );
×
222
                                            failure_reasons
×
223
                                                .push(format!("Assertion error {}: {}", ctx, m));
×
224
                                        }
×
225
                                        _ => {}
×
226
                                    }
227
                                }
228
                            }
3✔
229
                        }
230
                        skip_next_section = true;
4✔
231
                    }
12✔
232
                }
233
                SectionType::Asserts => {
234
                    if let Some(msg) = message_iter.next() {
×
235
                        last_message = Some(msg.clone());
×
236
                        if !self.no_assert
×
237
                            && let SectionContent::Assertions(lines) = &section.content
×
238
                        {
239
                            let result = self.assertion_engine.evaluate_all(
×
240
                                lines,
×
241
                                msg,
×
242
                                Some(&response.headers),
×
243
                                Some(&response.trailers),
×
244
                            );
245

246
                            if self.assertion_engine.has_failures(&result) {
×
247
                                for fail in self.assertion_engine.get_failures(&result) {
×
248
                                    match fail {
×
249
                                        crate::assert::AssertionResult::Fail {
250
                                            message,
×
251
                                            expected,
×
252
                                            actual,
×
253
                                        } => {
254
                                            let ctx = format!("at line {}", section.start_line);
×
255
                                            failure_reasons.push(format!(
×
256
                                                "Assertion failed {}: {}",
257
                                                ctx, message
258
                                            ));
259
                                            if let (Some(exp), Some(act)) = (expected, actual) {
×
260
                                                failure_reasons.push(format!(
×
261
                                                    "    Expected: {}\n    Actual:   {}",
×
262
                                                    exp, act
×
263
                                                ));
×
264
                                            }
×
265
                                        }
266
                                        crate::assert::AssertionResult::Error(m) => {
×
267
                                            let ctx = format!("at line {}", section.start_line);
×
268
                                            failure_reasons
×
269
                                                .push(format!("Assertion error {}: {}", ctx, m));
×
270
                                        }
×
271
                                        _ => {}
×
272
                                    }
273
                                }
274
                            }
×
275
                        }
×
276
                    } else if !self.no_assert {
×
277
                        failure_reasons.push(format!(
×
278
                            "Expected message for ASSERTS section at line {}, but no more messages received",
×
279
                            section.start_line
×
280
                        ));
×
281
                    }
×
282
                }
283
                SectionType::Extract => {
284
                    if let Some(msg) = &last_message {
1✔
285
                        if let SectionContent::Extract(extractions) = &section.content {
1✔
286
                            for (key, query) in extractions {
1✔
287
                                match self.assertion_engine.query(query, msg) {
1✔
288
                                    Ok(results) => {
1✔
289
                                        if let Some(val) = results.first() {
1✔
290
                                            variables.insert(key.clone(), val.clone());
1✔
291
                                        } else {
1✔
292
                                            failure_reasons.push(format!(
×
293
                                                 "Extraction failed at line {}: Query '{}' returned no results",
×
294
                                                 section.start_line, query
×
295
                                             ));
×
296
                                        }
×
297
                                    }
298
                                    Err(e) => {
×
299
                                        failure_reasons.push(format!(
×
300
                                            "Extraction error at line {}: {}",
×
301
                                            section.start_line, e
×
302
                                        ));
×
303
                                    }
×
304
                                }
305
                            }
306
                        }
×
307
                    } else {
×
308
                        failure_reasons.push(format!(
×
309
                            "EXTRACT at line {} requires a previous response message",
×
310
                            section.start_line
×
311
                        ));
×
312
                    }
×
313
                }
314
                _ => {}
×
315
            }
316
        }
317

318
        if !failure_reasons.is_empty() {
12✔
319
            TestExecutionResult::fail(
5✔
320
                format!("Validation failed:\n  - {}", failure_reasons.join("\n  - ")),
5✔
321
                None,
5✔
322
            )
323
        } else {
324
            TestExecutionResult::pass(None)
7✔
325
        }
326
    }
12✔
327
}
328

329
// Most tests here construct a `ResponseHandler`, which forces this file's own
330
// `PLUGIN_REGISTRY` lazy static (`build_plugin_manager()` → `fs::metadata` on
331
// configured plugin dirs) — blocked under miri isolation.
332
#[cfg(all(test, not(miri)))]
333
mod tests {
334
    use super::*;
335
    use serde_json::json;
336

337
    #[test]
338
    fn test_response_handler_new() {
1✔
339
        let handler = ResponseHandler::new(false);
1✔
340
        assert!(!handler.no_assert);
1✔
341
    }
1✔
342

343
    #[test]
344
    fn test_validate_message_exact_match() {
1✔
345
        let handler = ResponseHandler::new(false);
1✔
346
        let actual = json!({"id": 123, "name": "test"});
1✔
347
        let expected = json!({"id": 123, "name": "test"});
1✔
348
        let options = InlineOptions::default();
1✔
349

350
        let result = handler.validate_message(&actual, &expected, &options);
1✔
351
        assert!(result.is_ok());
1✔
352
    }
1✔
353

354
    #[test]
355
    fn test_validate_message_mismatch() {
1✔
356
        let handler = ResponseHandler::new(false);
1✔
357
        let actual = json!({"id": 123, "name": "test"});
1✔
358
        let expected = json!({"id": 456, "name": "test"});
1✔
359
        let options = InlineOptions::default();
1✔
360

361
        let result = handler.validate_message(&actual, &expected, &options);
1✔
362
        assert!(result.is_err());
1✔
363
    }
1✔
364

365
    #[test]
366
    fn test_validate_message_partial_match() {
1✔
367
        let handler = ResponseHandler::new(false);
1✔
368
        let actual = json!({"id": 123, "name": "test", "extra": "field"});
1✔
369
        let expected = json!({"id": 123});
1✔
370
        let options = InlineOptions {
1✔
371
            partial: true,
1✔
372
            ..InlineOptions::default()
1✔
373
        };
1✔
374

375
        let result = handler.validate_message(&actual, &expected, &options);
1✔
376
        assert!(result.is_ok());
1✔
377
    }
1✔
378

379
    #[test]
380
    fn test_substitute_variables_in_value() {
1✔
381
        let mut value = json!({"id": "{{ user_id }}", "name": "test"});
1✔
382
        let mut variables = HashMap::new();
1✔
383
        variables.insert("user_id".to_string(), json!("123"));
1✔
384

385
        ResponseHandler::substitute_variables_in_value(&mut value, &variables);
1✔
386

387
        assert_eq!(value["id"], "123");
1✔
388
        assert_eq!(value["name"], "test");
1✔
389
    }
1✔
390

391
    #[test]
392
    fn test_response_handler_no_assert() {
1✔
393
        let handler = ResponseHandler::new(true);
1✔
394
        let actual = json!({"id": 123});
1✔
395
        let expected = json!({"id": 456});
1✔
396
        let options = InlineOptions::default();
1✔
397

398
        // Should always pass when no_assert is true
399
        let result = handler.validate_message(&actual, &expected, &options);
1✔
400
        assert!(result.is_ok());
1✔
401
    }
1✔
402
}
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