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

gripmock / grpctestify-rust / 24805865717

22 Apr 2026 10:25PM UTC coverage: 76.897% (+0.3%) from 76.58%
24805865717

Pull #41

github

web-flow
Merge 2f12f7204 into 60c00bf76
Pull Request #41: feat: ERROR section should support partial and with_asserts inline options

463 of 609 new or added lines in 6 files covered. (76.03%)

13 existing lines in 2 files now uncovered.

18103 of 23542 relevant lines covered (76.9%)

2375.92 hits per line

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

85.69
/src/execution/error_handler.rs
1
// Error handling for test execution
2

3
use prost::Message;
4
use prost_types::Any;
5
use serde_json::{Map, Value, json};
6

7
#[derive(Clone, PartialEq, Message)]
8
struct GoogleRpcStatus {
9
    #[prost(int32, tag = "1")]
10
    code: i32,
11
    #[prost(string, tag = "2")]
12
    message: String,
13
    #[prost(message, repeated, tag = "3")]
14
    details: Vec<Any>,
15
}
16

17
#[derive(Clone, PartialEq, Message)]
18
struct GoogleRpcErrorInfo {
19
    #[prost(string, tag = "1")]
20
    reason: String,
21
    #[prost(string, tag = "2")]
22
    domain: String,
23
    #[prost(map = "string, string", tag = "3")]
24
    metadata: std::collections::HashMap<String, String>,
25
}
26

27
#[derive(Clone, PartialEq, Message)]
28
struct GoogleRpcBadRequest {
29
    #[prost(message, repeated, tag = "1")]
30
    field_violations: Vec<GoogleRpcBadRequestFieldViolation>,
31
}
32

33
#[derive(Clone, PartialEq, Message)]
34
struct GoogleRpcBadRequestFieldViolation {
35
    #[prost(string, tag = "1")]
36
    field: String,
37
    #[prost(string, tag = "2")]
38
    description: String,
39
}
40

41
/// Error handler for gRPC test execution
42
pub struct ErrorHandler;
43

44
impl ErrorHandler {
45
    /// Check if error matches expected error
46
    pub fn error_matches_expected(error_text: &str, expected: &Value) -> bool {
11✔
47
        Self::message_matches_error_text(error_text, expected)
11✔
48
            && Self::code_matches_error_text(error_text, expected)
9✔
49
            && expected.get("details").is_none()
7✔
50
    }
11✔
51

52
    /// Check if tonic::Status matches expected error JSON (supports details)
53
    pub fn status_matches_expected(status: &tonic::Status, expected: &Value) -> bool {
7✔
54
        Self::status_matches_expected_with_options(status, expected, false)
7✔
55
    }
7✔
56

57
    /// Check if tonic::Status matches expected error JSON, optionally as partial subset.
58
    pub fn status_matches_expected_with_options(
13✔
59
        status: &tonic::Status,
13✔
60
        expected: &Value,
13✔
61
        partial: bool,
13✔
62
    ) -> bool {
13✔
63
        if !partial && !Self::status_has_no_unexpected_top_level_fields(status, expected) {
13✔
64
            return false;
3✔
65
        }
10✔
66

67
        if partial {
10✔
68
            let actual = Self::status_to_json(status);
4✔
69
            return Self::is_subset_match(&actual, expected);
4✔
70
        }
6✔
71

72
        if !Self::message_matches_status(status, expected)
6✔
73
            || !Self::code_matches_status(status, expected)
5✔
74
        {
75
            return false;
1✔
76
        }
5✔
77

78
        let expects_details = expected.get("details").is_some();
5✔
79
        if !expects_details {
5✔
80
            return status.details().is_empty();
2✔
81
        }
3✔
82

83
        let Some(actual_details) = Self::decode_status_details(status.details()) else {
3✔
84
            return false;
×
85
        };
86

87
        Self::compare_details(expected, actual_details).is_none()
3✔
88
    }
13✔
89

90
    /// Convert tonic::Status details to JSON array
91
    pub fn status_details_json(status: &tonic::Status) -> Value {
2✔
92
        match Self::decode_status_details(status.details()) {
2✔
93
            Some(details) => Value::Array(details),
2✔
94
            None => Value::Null,
×
95
        }
96
    }
2✔
97

98
    /// Convert tonic::Status to JSON object for diff output
99
    pub fn status_to_json(status: &tonic::Status) -> Value {
20✔
100
        let mut obj = Map::with_capacity(3);
20✔
101
        obj.insert("code".into(), Value::from(status.code() as i64));
20✔
102
        obj.insert("message".into(), Value::from(status.message()));
20✔
103

104
        if let Some(details) = Self::decode_status_details(status.details())
20✔
105
            && !details.is_empty()
20✔
106
        {
11✔
107
            obj.insert("details".into(), Value::Array(details));
11✔
108
        }
11✔
109

110
        Value::Object(obj)
20✔
111
    }
20✔
112

113
    /// Returns a human-readable mismatch reason for tonic::Status comparison
114
    pub fn status_mismatch_reason(status: &tonic::Status, expected: &Value) -> Option<String> {
4✔
115
        Self::status_mismatch_reason_with_options(status, expected, false)
4✔
116
    }
4✔
117

118
    /// Returns mismatch reason for tonic::Status comparison with optional partial mode.
119
    pub fn status_mismatch_reason_with_options(
6✔
120
        status: &tonic::Status,
6✔
121
        expected: &Value,
6✔
122
        partial: bool,
6✔
123
    ) -> Option<String> {
6✔
124
        if !partial
6✔
125
            && let Some(reason) = Self::status_unexpected_top_level_field_reason(status, expected)
5✔
126
        {
127
            return Some(reason);
2✔
128
        }
4✔
129

130
        if partial {
4✔
131
            let actual = Self::status_to_json(status);
1✔
132
            return Self::subset_mismatch_reason(&actual, expected, "$");
1✔
133
        }
3✔
134

135
        if !Self::message_matches_status(status, expected) {
3✔
136
            let actual = status.message();
1✔
137
            if let Some(expected_msg) = expected.get("message").and_then(|v| v.as_str()) {
1✔
138
                return Some(format!(
1✔
139
                    "message mismatch: expected '{}', got '{}'",
1✔
140
                    expected_msg, actual
1✔
141
                ));
1✔
142
            }
×
143
            if expected.is_string()
×
144
                && let Some(s) = expected.as_str()
×
145
            {
146
                return Some(format!(
×
NEW
147
                    "message mismatch: expected '{}', got '{}'",
×
148
                    s, actual
×
149
                ));
×
150
            }
×
151
            return Some("message mismatch".to_string());
×
152
        }
2✔
153

154
        if !Self::code_matches_status(status, expected) {
2✔
155
            if let Some(code) = expected.get("code").and_then(|v| v.as_i64()) {
×
156
                let expected_name = Self::grpc_code_name_from_numeric(code).unwrap_or("Unknown");
×
157
                return Some(format!(
×
158
                    "code mismatch: expected {} ({}), got {} ({:?})",
×
159
                    code,
×
160
                    expected_name,
×
161
                    status.code() as i64,
×
162
                    status.code()
×
163
                ));
×
164
            }
×
165
            return Some("code mismatch".to_string());
×
166
        }
2✔
167

168
        let actual_details = match Self::decode_status_details(status.details()) {
2✔
169
            Some(details) => details,
2✔
170
            None => return Some("cannot decode gRPC status details".to_string()),
×
171
        };
172

173
        Self::compare_details(expected, actual_details)
2✔
174
    }
6✔
175

176
    fn is_subset_match(actual: &Value, expected: &Value) -> bool {
4✔
177
        Self::subset_mismatch_reason(actual, expected, "$").is_none()
4✔
178
    }
4✔
179

180
    fn status_has_no_unexpected_top_level_fields(status: &tonic::Status, expected: &Value) -> bool {
9✔
181
        Self::status_unexpected_top_level_field_reason(status, expected).is_none()
9✔
182
    }
9✔
183

184
    fn status_unexpected_top_level_field_reason(
14✔
185
        status: &tonic::Status,
14✔
186
        expected: &Value,
14✔
187
    ) -> Option<String> {
14✔
188
        let Value::Object(expected_obj) = expected else {
14✔
NEW
189
            return None;
×
190
        };
191
        let Value::Object(actual_obj) = Self::status_to_json(status) else {
14✔
NEW
192
            return None;
×
193
        };
194

195
        for key in actual_obj.keys() {
30✔
196
            if key == "details" && !expected_obj.contains_key("details") {
30✔
197
                return Some(format!(
2✔
198
                    "backend returned details, but ERROR.details is missing in gctf; actual details: {}",
2✔
199
                    Self::status_details_json(status)
2✔
200
                ));
2✔
201
            }
28✔
202
            if !expected_obj.contains_key(key) {
28✔
203
                return Some(format!(
3✔
204
                    "unexpected field in actual error: '{}' is present but missing in ERROR section (use partial=true or ASSERTS)",
3✔
205
                    key
3✔
206
                ));
3✔
207
            }
25✔
208
        }
209

210
        None
9✔
211
    }
14✔
212

213
    fn subset_mismatch_reason(actual: &Value, expected: &Value, path: &str) -> Option<String> {
24✔
214
        match (actual, expected) {
24✔
215
            (Value::Object(act_map), Value::Object(exp_map)) => {
10✔
216
                for (k, exp_val) in exp_map {
14✔
217
                    let key_path = format!("{}.{}", path, k);
14✔
218
                    let Some(act_val) = act_map.get(k) else {
14✔
NEW
219
                        return Some(format!("partial mismatch: missing key '{}'", key_path));
×
220
                    };
221
                    if let Some(reason) = Self::subset_mismatch_reason(act_val, exp_val, &key_path)
14✔
222
                    {
223
                        return Some(reason);
6✔
224
                    }
8✔
225
                }
226
                None
4✔
227
            }
228
            (Value::Array(act_arr), Value::Array(exp_arr)) => {
3✔
229
                let mut used_actual = vec![false; act_arr.len()];
3✔
230
                for (exp_idx, exp_item) in exp_arr.iter().enumerate() {
3✔
231
                    let mut matched = false;
3✔
232
                    for (act_idx, act_item) in act_arr.iter().enumerate() {
5✔
233
                        if used_actual[act_idx] {
5✔
NEW
234
                            continue;
×
235
                        }
5✔
236
                        if Self::subset_mismatch_reason(act_item, exp_item, path).is_none() {
5✔
237
                            used_actual[act_idx] = true;
1✔
238
                            matched = true;
1✔
239
                            break;
1✔
240
                        }
4✔
241
                    }
242

243
                    if !matched {
3✔
244
                        return Some(format!(
2✔
245
                            "partial mismatch: expected array item at '{}[{}]' not found",
2✔
246
                            path, exp_idx
2✔
247
                        ));
2✔
248
                    }
1✔
249
                }
250
                None
1✔
251
            }
252
            _ => {
253
                if actual == expected {
11✔
254
                    None
7✔
255
                } else {
256
                    Some(format!(
4✔
257
                        "partial mismatch at '{}': expected {}, got {}",
4✔
258
                        path, expected, actual
4✔
259
                    ))
4✔
260
                }
261
            }
262
        }
263
    }
24✔
264

265
    fn message_matches_error_text(error_text: &str, expected: &Value) -> bool {
11✔
266
        // Check message
267
        if let Some(expected_msg) = expected.get("message").and_then(|v| v.as_str()) {
11✔
268
            if !error_text.contains(expected_msg) {
5✔
269
                return false;
2✔
270
            }
3✔
271
        } else if expected.is_string()
6✔
272
            && let Some(s) = expected.as_str()
2✔
273
            && !error_text.contains(s)
2✔
274
        {
275
            return false;
×
276
        }
6✔
277

278
        true
9✔
279
    }
11✔
280

281
    fn message_matches_status(status: &tonic::Status, expected: &Value) -> bool {
9✔
282
        if let Some(expected_msg) = expected.get("message").and_then(|v| v.as_str()) {
9✔
283
            return status.message() == expected_msg;
9✔
UNCOV
284
        }
×
285

UNCOV
286
        if expected.is_string()
×
287
            && let Some(s) = expected.as_str()
×
288
        {
NEW
289
            return status.message() == s;
×
UNCOV
290
        }
×
291

UNCOV
292
        true
×
293
    }
9✔
294

295
    fn code_matches_error_text(error_text: &str, expected: &Value) -> bool {
9✔
296
        // Check code
297
        if let Some(code) = expected.get("code").and_then(|v| v.as_i64())
9✔
298
            && let Some(code_name) = Self::grpc_code_name_from_numeric(code)
6✔
299
        {
300
            let status_marker = format!("status: {}", code_name);
6✔
301
            if !error_text.contains(&status_marker)
6✔
302
                && !error_text.contains(&format!("code: {}", code))
2✔
303
            {
304
                return false;
2✔
305
            }
4✔
306
        }
3✔
307

308
        true
7✔
309
    }
9✔
310

311
    fn code_matches_status(status: &tonic::Status, expected: &Value) -> bool {
7✔
312
        if let Some(code) = expected.get("code").and_then(|v| v.as_i64()) {
7✔
313
            return status.code() as i64 == code;
7✔
UNCOV
314
        }
×
UNCOV
315
        true
×
316
    }
7✔
317

318
    fn compare_details(expected: &Value, actual_details: Vec<Value>) -> Option<String> {
5✔
319
        if let Some(expected_details) = expected.get("details") {
5✔
320
            let expected_array = match expected_details.as_array() {
5✔
321
                Some(array) => array,
5✔
322
                None => return Some("expected ERROR.details must be an array".to_string()),
×
323
            };
324

325
            if expected_array.len() != actual_details.len() {
5✔
326
                return Some(format!(
2✔
327
                    "details mismatch: expected {} item(s), got {}",
2✔
328
                    expected_array.len(),
2✔
329
                    actual_details.len()
2✔
330
                ));
2✔
331
            }
3✔
332

333
            for (idx, (exp, act)) in expected_array.iter().zip(actual_details.iter()).enumerate() {
6✔
334
                if exp != act {
6✔
335
                    return Some(format!(
2✔
336
                        "details mismatch at index {}: expected {} but got {}",
2✔
337
                        idx, exp, act
2✔
338
                    ));
2✔
339
                }
4✔
340
            }
341

342
            return None;
1✔
UNCOV
343
        }
×
344

UNCOV
345
        if expected.is_object() && !actual_details.is_empty() {
×
UNCOV
346
            return Some(format!(
×
UNCOV
347
                "backend returned details, but ERROR.details is missing in gctf; actual details: {}",
×
UNCOV
348
                Value::Array(actual_details)
×
UNCOV
349
            ));
×
350
        }
×
351

352
        None
×
353
    }
5✔
354

355
    fn decode_status_details(raw: &[u8]) -> Option<Vec<Value>> {
27✔
356
        if raw.is_empty() {
27✔
357
            return Some(Vec::new());
11✔
358
        }
16✔
359

360
        let status = GoogleRpcStatus::decode(raw).ok()?;
16✔
361
        Some(status.details.into_iter().map(Self::any_to_json).collect())
16✔
362
    }
27✔
363

364
    fn any_to_json(any: Any) -> Value {
32✔
365
        let type_url = any.type_url;
32✔
366
        let value = any.value;
32✔
367

368
        if type_url.ends_with("/google.rpc.ErrorInfo") {
32✔
369
            if let Ok(info) = GoogleRpcErrorInfo::decode(value.as_slice()) {
16✔
370
                let mut metadata = Map::new();
16✔
371
                for (k, v) in info.metadata {
32✔
372
                    metadata.insert(k, Value::String(v));
32✔
373
                }
32✔
374

375
                return json!({
16✔
376
                    "@type": type_url,
16✔
377
                    "reason": info.reason,
16✔
378
                    "domain": info.domain,
16✔
379
                    "metadata": metadata,
16✔
380
                });
381
            }
×
382

383
            return json!({
×
384
                "@type": type_url,
×
385
                "_decodeError": "failed to decode ErrorInfo",
×
386
                "_valueHex": Self::hex_encode(value.as_slice()),
×
387
            });
388
        }
16✔
389

390
        if type_url.ends_with("/google.rpc.BadRequest") {
16✔
391
            if let Ok(bad_request) = GoogleRpcBadRequest::decode(value.as_slice()) {
16✔
392
                let field_violations = bad_request
16✔
393
                    .field_violations
16✔
394
                    .into_iter()
16✔
395
                    .map(|violation| {
16✔
396
                        json!({
16✔
397
                            "field": violation.field,
16✔
398
                            "description": violation.description,
16✔
399
                        })
400
                    })
16✔
401
                    .collect::<Vec<_>>();
16✔
402

403
                return json!({
16✔
404
                    "@type": type_url,
16✔
405
                    "fieldViolations": field_violations,
16✔
406
                });
407
            }
×
408

409
            return json!({
×
410
                "@type": type_url,
×
411
                "_decodeError": "failed to decode BadRequest",
×
412
                "_valueHex": Self::hex_encode(value.as_slice()),
×
413
            });
414
        }
×
415

416
        json!({
×
417
            "@type": type_url,
×
418
            "_valueHex": Self::hex_encode(value.as_slice()),
×
419
        })
420
    }
32✔
421

422
    fn hex_encode(bytes: &[u8]) -> String {
×
423
        const HEX: &[u8; 16] = b"0123456789abcdef";
424
        let mut output = String::with_capacity(bytes.len() * 2);
×
425
        for byte in bytes {
×
426
            output.push(HEX[(byte >> 4) as usize] as char);
×
427
            output.push(HEX[(byte & 0x0f) as usize] as char);
×
428
        }
×
429
        output
×
430
    }
×
431

432
    /// Get gRPC code name from numeric code
433
    pub fn grpc_code_name_from_numeric(code: i64) -> Option<&'static str> {
17✔
434
        match code {
17✔
435
            0 => Some("OK"),
2✔
436
            1 => Some("Cancelled"),
×
437
            2 => Some("Unknown"),
×
438
            3 => Some("InvalidArgument"),
3✔
439
            4 => Some("DeadlineExceeded"),
×
440
            5 => Some("NotFound"),
8✔
441
            6 => Some("AlreadyExists"),
×
442
            7 => Some("PermissionDenied"),
×
443
            8 => Some("ResourceExhausted"),
×
444
            9 => Some("FailedPrecondition"),
×
445
            10 => Some("Aborted"),
×
446
            11 => Some("OutOfRange"),
×
447
            12 => Some("Unimplemented"),
×
448
            13 => Some("Internal"),
2✔
449
            14 => Some("Unavailable"),
×
450
            15 => Some("DataLoss"),
×
451
            16 => Some("Unauthenticated"),
×
452
            _ => None,
2✔
453
        }
454
    }
17✔
455

456
    /// Format error message for display
457
    pub fn format_error_message(_error_text: &str, expected: &Value) -> String {
1✔
458
        let mut parts = Vec::new();
1✔
459

460
        if let Some(msg) = expected.get("message").and_then(|v| v.as_str()) {
1✔
461
            parts.push(format!("expected message: {}", msg));
1✔
462
        }
1✔
463

464
        if let Some(code) = expected.get("code").and_then(|v| v.as_i64()) {
1✔
465
            if let Some(code_name) = Self::grpc_code_name_from_numeric(code) {
1✔
466
                parts.push(format!("expected code: {} ({})", code, code_name));
1✔
467
            } else {
1✔
468
                parts.push(format!("expected code: {}", code));
×
469
            }
×
470
        }
×
471

472
        if expected.get("details").is_some() {
1✔
473
            parts.push("expected details".to_string());
×
474
        }
1✔
475

476
        if parts.is_empty() {
1✔
477
            "error expected".to_string()
×
478
        } else {
479
            parts.join(", ")
1✔
480
        }
481
    }
1✔
482

483
    /// Check if error text contains expected code
484
    pub fn error_contains_code(error_text: &str, expected_code: i64) -> bool {
2✔
485
        if let Some(code_name) = Self::grpc_code_name_from_numeric(expected_code) {
2✔
486
            error_text.contains(&format!("status: {}", code_name))
2✔
487
                || error_text.contains(&format!("code: {}", expected_code))
1✔
488
        } else {
489
            error_text.contains(&format!("code: {}", expected_code))
×
490
        }
491
    }
2✔
492

493
    /// Check if error text contains expected message
494
    pub fn error_contains_message(error_text: &str, expected_message: &str) -> bool {
2✔
495
        error_text.contains(expected_message)
2✔
496
    }
2✔
497
}
498

499
#[cfg(test)]
500
mod tests {
501
    use super::*;
502
    use prost::Message;
503
    use serde_json::json;
504
    use tonic::Code;
505

506
    fn status_with_details() -> tonic::Status {
9✔
507
        let error_info = GoogleRpcErrorInfo {
9✔
508
            reason: "API_DISABLED".to_string(),
9✔
509
            domain: "your.service.com".to_string(),
9✔
510
            metadata: std::collections::HashMap::from([
9✔
511
                ("service".to_string(), "your.service.com".to_string()),
9✔
512
                ("consumer".to_string(), "projects/123".to_string()),
9✔
513
            ]),
9✔
514
        };
9✔
515

516
        let bad_request = GoogleRpcBadRequest {
9✔
517
            field_violations: vec![GoogleRpcBadRequestFieldViolation {
9✔
518
                field: "name".to_string(),
9✔
519
                description: "Name must be at least 3 characters".to_string(),
9✔
520
            }],
9✔
521
        };
9✔
522

523
        let status_proto = GoogleRpcStatus {
9✔
524
            code: Code::InvalidArgument as i32,
9✔
525
            message: "Invalid argument provided".to_string(),
9✔
526
            details: vec![
9✔
527
                Any {
9✔
528
                    type_url: "type.googleapis.com/google.rpc.ErrorInfo".to_string(),
9✔
529
                    value: error_info.encode_to_vec(),
9✔
530
                },
9✔
531
                Any {
9✔
532
                    type_url: "type.googleapis.com/google.rpc.BadRequest".to_string(),
9✔
533
                    value: bad_request.encode_to_vec(),
9✔
534
                },
9✔
535
            ],
9✔
536
        };
9✔
537

538
        tonic::Status::with_details(
9✔
539
            Code::InvalidArgument,
9✔
540
            "Invalid argument provided",
541
            status_proto.encode_to_vec().into(),
9✔
542
        )
543
    }
9✔
544

545
    fn status_without_details() -> tonic::Status {
7✔
546
        tonic::Status::new(Code::InvalidArgument, "Invalid argument provided")
7✔
547
    }
7✔
548

549
    #[test]
550
    fn test_error_matches_expected_message() {
1✔
551
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
552
        let expected = json!({"message": "Resource not found"});
1✔
553

554
        assert!(ErrorHandler::error_matches_expected(error_text, &expected));
1✔
555
    }
1✔
556

557
    #[test]
558
    fn test_error_matches_expected_code() {
1✔
559
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
560
        let expected = json!({"code": 5});
1✔
561

562
        assert!(ErrorHandler::error_matches_expected(error_text, &expected));
1✔
563
    }
1✔
564

565
    #[test]
566
    fn test_error_matches_expected_both() {
1✔
567
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
568
        let expected = json!({"code": 5, "message": "Resource not found"});
1✔
569

570
        assert!(ErrorHandler::error_matches_expected(error_text, &expected));
1✔
571
    }
1✔
572

573
    #[test]
574
    fn test_error_matches_expected_wrong_message() {
1✔
575
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
576
        let expected = json!({"message": "Wrong message"});
1✔
577

578
        assert!(!ErrorHandler::error_matches_expected(error_text, &expected));
1✔
579
    }
1✔
580

581
    #[test]
582
    fn test_error_matches_expected_wrong_code() {
1✔
583
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
584
        let expected = json!({"code": 3});
1✔
585

586
        assert!(!ErrorHandler::error_matches_expected(error_text, &expected));
1✔
587
    }
1✔
588

589
    #[test]
590
    fn test_grpc_code_name_from_numeric() {
1✔
591
        assert_eq!(ErrorHandler::grpc_code_name_from_numeric(0), Some("OK"));
1✔
592
        assert_eq!(
1✔
593
            ErrorHandler::grpc_code_name_from_numeric(5),
1✔
594
            Some("NotFound")
595
        );
596
        assert_eq!(
1✔
597
            ErrorHandler::grpc_code_name_from_numeric(13),
1✔
598
            Some("Internal")
599
        );
600
        assert_eq!(ErrorHandler::grpc_code_name_from_numeric(999), None);
1✔
601
    }
1✔
602

603
    #[test]
604
    fn test_format_error_message() {
1✔
605
        let expected = json!({"code": 5, "message": "Resource not found"});
1✔
606
        let formatted = ErrorHandler::format_error_message("", &expected);
1✔
607

608
        assert!(formatted.contains("expected message: Resource not found"));
1✔
609
        assert!(formatted.contains("expected code: 5 (NotFound)"));
1✔
610
    }
1✔
611

612
    #[test]
613
    fn test_error_contains_code() {
1✔
614
        let error_text = "Error: status: NotFound, code: 5";
1✔
615

616
        assert!(ErrorHandler::error_contains_code(error_text, 5));
1✔
617
        assert!(!ErrorHandler::error_contains_code(error_text, 3));
1✔
618
    }
1✔
619

620
    #[test]
621
    fn test_error_contains_message() {
1✔
622
        let error_text = "Error: Resource not found";
1✔
623

624
        assert!(ErrorHandler::error_contains_message(
1✔
625
            error_text,
1✔
626
            "Resource not found"
1✔
627
        ));
628
        assert!(!ErrorHandler::error_contains_message(
1✔
629
            error_text,
1✔
630
            "Wrong message"
1✔
631
        ));
1✔
632
    }
1✔
633

634
    #[test]
635
    fn test_error_matches_expected_string() {
1✔
636
        let error_text = "Error: Resource not found";
1✔
637
        let expected = json!("Resource not found");
1✔
638

639
        assert!(ErrorHandler::error_matches_expected(error_text, &expected));
1✔
640
    }
1✔
641

642
    #[test]
643
    fn test_status_matches_expected_with_details() {
1✔
644
        let status = status_with_details();
1✔
645
        let expected = json!({
1✔
646
            "code": 3,
1✔
647
            "message": "Invalid argument provided",
1✔
648
            "details": [
1✔
649
                {
650
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
651
                    "reason": "API_DISABLED",
1✔
652
                    "domain": "your.service.com",
1✔
653
                    "metadata": {
1✔
654
                        "service": "your.service.com",
1✔
655
                        "consumer": "projects/123"
1✔
656
                    }
657
                },
658
                {
659
                    "@type": "type.googleapis.com/google.rpc.BadRequest",
1✔
660
                    "fieldViolations": [
1✔
661
                        {
662
                            "field": "name",
1✔
663
                            "description": "Name must be at least 3 characters"
1✔
664
                        }
665
                    ]
666
                }
667
            ]
668
        });
669

670
        assert!(ErrorHandler::status_matches_expected(&status, &expected));
1✔
671
    }
1✔
672

673
    #[test]
674
    fn test_status_matches_expected_with_wrong_details() {
1✔
675
        let status = status_with_details();
1✔
676
        let expected = json!({
1✔
677
            "details": [
1✔
678
                {
679
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
680
                    "reason": "WRONG_REASON"
1✔
681
                }
682
            ]
683
        });
684

685
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
686
    }
1✔
687

688
    #[test]
689
    fn test_status_matches_expected_fails_when_actual_has_unexpected_details() {
1✔
690
        let status = status_with_details();
1✔
691
        let expected = json!({
1✔
692
            "code": 3,
1✔
693
            "message": "Invalid argument provided"
1✔
694
        });
695

696
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
697
    }
1✔
698

699
    #[test]
700
    fn test_status_matches_expected_fails_when_expected_requires_details() {
1✔
701
        let status = status_without_details();
1✔
702
        let expected = json!({
1✔
703
            "code": 3,
1✔
704
            "message": "Invalid argument provided",
1✔
705
            "details": [
1✔
706
                {
707
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
708
                    "reason": "API_DISABLED"
1✔
709
                }
710
            ]
711
        });
712

713
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
714
    }
1✔
715

716
    #[test]
717
    fn test_status_matches_expected_passes_when_no_details_on_both_sides() {
1✔
718
        let status = status_without_details();
1✔
719
        let expected = json!({
1✔
720
            "code": 3,
1✔
721
            "message": "Invalid argument provided"
1✔
722
        });
723

724
        assert!(ErrorHandler::status_matches_expected(&status, &expected));
1✔
725
    }
1✔
726

727
    #[test]
728
    fn test_status_matches_expected_rejects_partial_message_match() {
1✔
729
        let status = status_without_details();
1✔
730
        let expected = json!({
1✔
731
            "code": 3,
1✔
732
            "message": "Invalid argument"
1✔
733
        });
734

735
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
736
        let reason = ErrorHandler::status_mismatch_reason(&status, &expected).unwrap();
1✔
737
        assert!(reason.contains("expected 'Invalid argument'"));
1✔
738
        assert!(reason.contains("got 'Invalid argument provided'"));
1✔
739
    }
1✔
740

741
    #[test]
742
    fn test_status_matches_expected_partial_allows_subset_top_level_fields() {
1✔
743
        let status = status_with_details();
1✔
744
        let expected = json!({
1✔
745
            "code": 3
1✔
746
        });
747

748
        assert!(ErrorHandler::status_matches_expected_with_options(
1✔
749
            &status, &expected, true
1✔
750
        ));
751
    }
1✔
752

753
    #[test]
754
    fn test_status_matches_expected_partial_allows_subset_details_payload() {
1✔
755
        let status = status_with_details();
1✔
756
        let expected = json!({
1✔
757
            "code": 3,
1✔
758
            "details": [
1✔
759
                {
760
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
761
                    "reason": "API_DISABLED"
1✔
762
                }
763
            ]
764
        });
765

766
        assert!(ErrorHandler::status_matches_expected_with_options(
1✔
767
            &status, &expected, true
1✔
768
        ));
769
    }
1✔
770

771
    #[test]
772
    fn test_status_matches_expected_partial_fails_when_expected_detail_item_missing() {
1✔
773
        let status = status_with_details();
1✔
774
        let expected = json!({
1✔
775
            "code": 3,
1✔
776
            "details": [
1✔
777
                {
778
                    "@type": "type.googleapis.com/google.rpc.DebugInfo",
1✔
779
                    "detail": "not-present"
1✔
780
                }
781
            ]
782
        });
783

784
        assert!(!ErrorHandler::status_matches_expected_with_options(
1✔
785
            &status, &expected, true
1✔
786
        ));
1✔
787
        let reason = ErrorHandler::status_mismatch_reason_with_options(&status, &expected, true)
1✔
788
            .expect("partial mismatch reason");
1✔
789
        assert!(reason.contains("expected array item"));
1✔
790
    }
1✔
791

792
    #[test]
793
    fn test_status_matches_expected_strict_fails_when_message_omitted() {
1✔
794
        let status = status_without_details();
1✔
795
        let expected = json!({
1✔
796
            "code": 3
1✔
797
        });
798

799
        assert!(!ErrorHandler::status_matches_expected_with_options(
1✔
800
            &status, &expected, false
1✔
801
        ));
1✔
802
        let reason = ErrorHandler::status_mismatch_reason_with_options(&status, &expected, false)
1✔
803
            .expect("strict mismatch reason");
1✔
804
        assert!(reason.contains("unexpected field"));
1✔
805
        assert!(reason.contains("message"));
1✔
806
    }
1✔
807

808
    #[test]
809
    fn test_status_matches_expected_partial_passes_when_message_omitted() {
1✔
810
        let status = status_without_details();
1✔
811
        let expected = json!({
1✔
812
            "code": 3
1✔
813
        });
814

815
        assert!(ErrorHandler::status_matches_expected_with_options(
1✔
816
            &status, &expected, true
1✔
817
        ));
818
    }
1✔
819

820
    #[test]
821
    fn test_status_matches_expected_strict_allows_missing_details_when_not_present_in_actual() {
1✔
822
        let status = status_without_details();
1✔
823
        let expected = json!({
1✔
824
            "code": 3,
1✔
825
            "message": "Invalid argument provided"
1✔
826
        });
827

828
        assert!(ErrorHandler::status_matches_expected_with_options(
1✔
829
            &status, &expected, false
1✔
830
        ));
831
    }
1✔
832

833
    #[test]
834
    fn test_status_mismatch_reason_for_unexpected_details() {
1✔
835
        let status = status_with_details();
1✔
836
        let expected = json!({
1✔
837
            "code": 3,
1✔
838
            "message": "Invalid argument provided"
1✔
839
        });
840

841
        let reason = ErrorHandler::status_mismatch_reason(&status, &expected).unwrap();
1✔
842
        assert!(reason.contains("ERROR.details is missing"));
1✔
843
        assert!(reason.contains("actual details"));
1✔
844
        assert!(reason.contains("type.googleapis.com/google.rpc.ErrorInfo"));
1✔
845
    }
1✔
846

847
    #[test]
848
    fn test_status_mismatch_reason_for_missing_required_details() {
1✔
849
        let status = status_without_details();
1✔
850
        let expected = json!({
1✔
851
            "code": 3,
1✔
852
            "message": "Invalid argument provided",
1✔
853
            "details": [
1✔
854
                {"@type": "type.googleapis.com/google.rpc.ErrorInfo"}
1✔
855
            ]
856
        });
857

858
        let reason = ErrorHandler::status_mismatch_reason(&status, &expected).unwrap();
1✔
859
        assert!(reason.contains("details mismatch"));
1✔
860
    }
1✔
861

862
    #[test]
863
    fn test_status_to_json_contains_details() {
1✔
864
        let status = status_with_details();
1✔
865
        let json = ErrorHandler::status_to_json(&status);
1✔
866
        assert_eq!(json["code"], 3);
1✔
867
        assert_eq!(json["message"], "Invalid argument provided");
1✔
868
        assert!(json.get("details").is_some());
1✔
869
    }
1✔
870

871
    #[test]
872
    fn test_status_matches_expected_fails_when_details_field_is_missing_in_expected_object() {
1✔
873
        let status = status_with_details();
1✔
874
        let expected = json!({
1✔
875
            "code": 3,
1✔
876
            "message": "Invalid argument provided",
1✔
877
            "details": [
1✔
878
                {
879
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
880
                    "reason": "API_DISABLED",
1✔
881
                    "domain": "your.service.com",
1✔
882
                    "metadata": {
1✔
883
                        "service": "your.service.com",
1✔
884
                        "consumer": "projects/123"
1✔
885
                    }
886
                },
887
                {
888
                    "@type": "type.googleapis.com/google.rpc.BadRequest",
1✔
889
                    "fieldViolations": [
1✔
890
                        {
891
                            "description": "Name must be at least 3 characters"
1✔
892
                        }
893
                    ]
894
                }
895
            ]
896
        });
897

898
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
899
        let reason = ErrorHandler::status_mismatch_reason(&status, &expected).unwrap();
1✔
900
        assert!(reason.contains("details mismatch at index 1"));
1✔
901
    }
1✔
902
}
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