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

gripmock / grpctestify-rust / 24804333679

22 Apr 2026 09:45PM UTC coverage: 76.62% (+0.04%) from 76.58%
24804333679

Pull #41

github

web-flow
Merge d85669f34 into 60c00bf76
Pull Request #41: fix error.message strict

12 of 14 new or added lines in 1 file covered. (85.71%)

422 existing lines in 4 files now uncovered.

17602 of 22973 relevant lines covered (76.62%)

2429.63 hits per line

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

83.45
/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
        if !Self::message_matches_status(status, expected)
7✔
55
            || !Self::code_matches_status(status, expected)
6✔
56
        {
57
            return false;
1✔
58
        }
6✔
59

60
        let expects_details = expected.get("details").is_some();
6✔
61
        if !expects_details {
6✔
62
            return status.details().is_empty();
2✔
63
        }
4✔
64

65
        let Some(actual_details) = Self::decode_status_details(status.details()) else {
4✔
66
            return false;
×
67
        };
68

69
        Self::compare_details(expected, actual_details).is_none()
4✔
70
    }
7✔
71

72
    /// Convert tonic::Status details to JSON array
UNCOV
73
    pub fn status_details_json(status: &tonic::Status) -> Value {
×
UNCOV
74
        match Self::decode_status_details(status.details()) {
×
75
            Some(details) => Value::Array(details),
×
UNCOV
76
            None => Value::Null,
×
77
        }
UNCOV
78
    }
×
79

80
    /// Convert tonic::Status to JSON object for diff output
81
    pub fn status_to_json(status: &tonic::Status) -> Value {
1✔
82
        let mut obj = Map::with_capacity(3);
1✔
83
        obj.insert("code".into(), Value::from(status.code() as i64));
1✔
84
        obj.insert("message".into(), Value::from(status.message()));
1✔
85

86
        if let Some(details) = Self::decode_status_details(status.details())
1✔
87
            && !details.is_empty()
1✔
88
        {
1✔
89
            obj.insert("details".into(), Value::Array(details));
1✔
90
        }
1✔
91

92
        Value::Object(obj)
1✔
93
    }
1✔
94

95
    /// Returns a human-readable mismatch reason for tonic::Status comparison
96
    pub fn status_mismatch_reason(status: &tonic::Status, expected: &Value) -> Option<String> {
4✔
97
        if !Self::message_matches_status(status, expected) {
4✔
98
            let actual = status.message();
1✔
99
            if let Some(expected_msg) = expected.get("message").and_then(|v| v.as_str()) {
1✔
100
                return Some(format!(
1✔
101
                    "message mismatch: expected '{}', got '{}'",
1✔
102
                    expected_msg, actual
1✔
103
                ));
1✔
UNCOV
104
            }
×
UNCOV
105
            if expected.is_string()
×
UNCOV
106
                && let Some(s) = expected.as_str()
×
107
            {
UNCOV
108
                return Some(format!(
×
NEW
UNCOV
109
                    "message mismatch: expected '{}', got '{}'",
×
UNCOV
110
                    s, actual
×
UNCOV
111
                ));
×
UNCOV
112
            }
×
UNCOV
113
            return Some("message mismatch".to_string());
×
114
        }
3✔
115

116
        if !Self::code_matches_status(status, expected) {
3✔
117
            if let Some(code) = expected.get("code").and_then(|v| v.as_i64()) {
×
118
                let expected_name = Self::grpc_code_name_from_numeric(code).unwrap_or("Unknown");
×
119
                return Some(format!(
×
120
                    "code mismatch: expected {} ({}), got {} ({:?})",
×
121
                    code,
×
122
                    expected_name,
×
123
                    status.code() as i64,
×
124
                    status.code()
×
125
                ));
×
126
            }
×
127
            return Some("code mismatch".to_string());
×
128
        }
3✔
129

130
        let actual_details = match Self::decode_status_details(status.details()) {
3✔
131
            Some(details) => details,
3✔
132
            None => return Some("cannot decode gRPC status details".to_string()),
×
133
        };
134

135
        Self::compare_details(expected, actual_details)
3✔
136
    }
4✔
137

138
    fn message_matches_error_text(error_text: &str, expected: &Value) -> bool {
11✔
139
        // Check message
140
        if let Some(expected_msg) = expected.get("message").and_then(|v| v.as_str()) {
11✔
141
            if !error_text.contains(expected_msg) {
5✔
142
                return false;
2✔
143
            }
3✔
144
        } else if expected.is_string()
6✔
145
            && let Some(s) = expected.as_str()
2✔
146
            && !error_text.contains(s)
2✔
147
        {
148
            return false;
×
149
        }
6✔
150

151
        true
9✔
152
    }
11✔
153

154
    fn message_matches_status(status: &tonic::Status, expected: &Value) -> bool {
11✔
155
        if let Some(expected_msg) = expected.get("message").and_then(|v| v.as_str()) {
11✔
156
            return status.message() == expected_msg;
10✔
157
        }
1✔
158

159
        if expected.is_string()
1✔
160
            && let Some(s) = expected.as_str()
×
161
        {
NEW
162
            return status.message() == s;
×
163
        }
1✔
164

165
        true
1✔
166
    }
11✔
167

168
    fn code_matches_error_text(error_text: &str, expected: &Value) -> bool {
9✔
169
        // Check code
170
        if let Some(code) = expected.get("code").and_then(|v| v.as_i64())
9✔
171
            && let Some(code_name) = Self::grpc_code_name_from_numeric(code)
6✔
172
        {
173
            let status_marker = format!("status: {}", code_name);
6✔
174
            if !error_text.contains(&status_marker)
6✔
175
                && !error_text.contains(&format!("code: {}", code))
2✔
176
            {
177
                return false;
2✔
178
            }
4✔
179
        }
3✔
180

181
        true
7✔
182
    }
9✔
183

184
    fn code_matches_status(status: &tonic::Status, expected: &Value) -> bool {
9✔
185
        if let Some(code) = expected.get("code").and_then(|v| v.as_i64()) {
9✔
186
            return status.code() as i64 == code;
8✔
187
        }
1✔
188
        true
1✔
189
    }
9✔
190

191
    fn compare_details(expected: &Value, actual_details: Vec<Value>) -> Option<String> {
7✔
192
        if let Some(expected_details) = expected.get("details") {
7✔
193
            let expected_array = match expected_details.as_array() {
6✔
194
                Some(array) => array,
6✔
195
                None => return Some("expected ERROR.details must be an array".to_string()),
×
196
            };
197

198
            if expected_array.len() != actual_details.len() {
6✔
199
                return Some(format!(
3✔
200
                    "details mismatch: expected {} item(s), got {}",
3✔
201
                    expected_array.len(),
3✔
202
                    actual_details.len()
3✔
203
                ));
3✔
204
            }
3✔
205

206
            for (idx, (exp, act)) in expected_array.iter().zip(actual_details.iter()).enumerate() {
6✔
207
                if exp != act {
6✔
208
                    return Some(format!(
2✔
209
                        "details mismatch at index {}: expected {} but got {}",
2✔
210
                        idx, exp, act
2✔
211
                    ));
2✔
212
                }
4✔
213
            }
214

215
            return None;
1✔
216
        }
1✔
217

218
        if expected.is_object() && !actual_details.is_empty() {
1✔
219
            return Some(format!(
1✔
220
                "backend returned details, but ERROR.details is missing in gctf; actual details: {}",
1✔
221
                Value::Array(actual_details)
1✔
222
            ));
1✔
223
        }
×
224

225
        None
×
226
    }
7✔
227

228
    fn decode_status_details(raw: &[u8]) -> Option<Vec<Value>> {
8✔
229
        if raw.is_empty() {
8✔
230
            return Some(Vec::new());
2✔
231
        }
6✔
232

233
        let status = GoogleRpcStatus::decode(raw).ok()?;
6✔
234
        Some(status.details.into_iter().map(Self::any_to_json).collect())
6✔
235
    }
8✔
236

237
    fn any_to_json(any: Any) -> Value {
12✔
238
        let type_url = any.type_url;
12✔
239
        let value = any.value;
12✔
240

241
        if type_url.ends_with("/google.rpc.ErrorInfo") {
12✔
242
            if let Ok(info) = GoogleRpcErrorInfo::decode(value.as_slice()) {
6✔
243
                let mut metadata = Map::new();
6✔
244
                for (k, v) in info.metadata {
12✔
245
                    metadata.insert(k, Value::String(v));
12✔
246
                }
12✔
247

248
                return json!({
6✔
249
                    "@type": type_url,
6✔
250
                    "reason": info.reason,
6✔
251
                    "domain": info.domain,
6✔
252
                    "metadata": metadata,
6✔
253
                });
254
            }
×
255

256
            return json!({
×
257
                "@type": type_url,
×
258
                "_decodeError": "failed to decode ErrorInfo",
×
259
                "_valueHex": Self::hex_encode(value.as_slice()),
×
260
            });
261
        }
6✔
262

263
        if type_url.ends_with("/google.rpc.BadRequest") {
6✔
264
            if let Ok(bad_request) = GoogleRpcBadRequest::decode(value.as_slice()) {
6✔
265
                let field_violations = bad_request
6✔
266
                    .field_violations
6✔
267
                    .into_iter()
6✔
268
                    .map(|violation| {
6✔
269
                        json!({
6✔
270
                            "field": violation.field,
6✔
271
                            "description": violation.description,
6✔
272
                        })
273
                    })
6✔
274
                    .collect::<Vec<_>>();
6✔
275

276
                return json!({
6✔
277
                    "@type": type_url,
6✔
278
                    "fieldViolations": field_violations,
6✔
279
                });
UNCOV
280
            }
×
281

UNCOV
282
            return json!({
×
283
                "@type": type_url,
×
UNCOV
284
                "_decodeError": "failed to decode BadRequest",
×
UNCOV
285
                "_valueHex": Self::hex_encode(value.as_slice()),
×
286
            });
287
        }
×
288

289
        json!({
×
UNCOV
290
            "@type": type_url,
×
UNCOV
291
            "_valueHex": Self::hex_encode(value.as_slice()),
×
292
        })
293
    }
12✔
294

UNCOV
295
    fn hex_encode(bytes: &[u8]) -> String {
×
296
        const HEX: &[u8; 16] = b"0123456789abcdef";
UNCOV
297
        let mut output = String::with_capacity(bytes.len() * 2);
×
UNCOV
298
        for byte in bytes {
×
UNCOV
299
            output.push(HEX[(byte >> 4) as usize] as char);
×
UNCOV
300
            output.push(HEX[(byte & 0x0f) as usize] as char);
×
UNCOV
301
        }
×
UNCOV
302
        output
×
UNCOV
303
    }
×
304

305
    /// Get gRPC code name from numeric code
306
    pub fn grpc_code_name_from_numeric(code: i64) -> Option<&'static str> {
17✔
307
        match code {
17✔
308
            0 => Some("OK"),
2✔
UNCOV
309
            1 => Some("Cancelled"),
×
UNCOV
310
            2 => Some("Unknown"),
×
311
            3 => Some("InvalidArgument"),
3✔
UNCOV
312
            4 => Some("DeadlineExceeded"),
×
313
            5 => Some("NotFound"),
8✔
UNCOV
314
            6 => Some("AlreadyExists"),
×
UNCOV
315
            7 => Some("PermissionDenied"),
×
UNCOV
316
            8 => Some("ResourceExhausted"),
×
UNCOV
317
            9 => Some("FailedPrecondition"),
×
UNCOV
318
            10 => Some("Aborted"),
×
UNCOV
319
            11 => Some("OutOfRange"),
×
UNCOV
320
            12 => Some("Unimplemented"),
×
321
            13 => Some("Internal"),
2✔
322
            14 => Some("Unavailable"),
×
UNCOV
323
            15 => Some("DataLoss"),
×
UNCOV
324
            16 => Some("Unauthenticated"),
×
325
            _ => None,
2✔
326
        }
327
    }
17✔
328

329
    /// Format error message for display
330
    pub fn format_error_message(_error_text: &str, expected: &Value) -> String {
1✔
331
        let mut parts = Vec::new();
1✔
332

333
        if let Some(msg) = expected.get("message").and_then(|v| v.as_str()) {
1✔
334
            parts.push(format!("expected message: {}", msg));
1✔
335
        }
1✔
336

337
        if let Some(code) = expected.get("code").and_then(|v| v.as_i64()) {
1✔
338
            if let Some(code_name) = Self::grpc_code_name_from_numeric(code) {
1✔
339
                parts.push(format!("expected code: {} ({})", code, code_name));
1✔
340
            } else {
1✔
UNCOV
341
                parts.push(format!("expected code: {}", code));
×
UNCOV
342
            }
×
UNCOV
343
        }
×
344

345
        if expected.get("details").is_some() {
1✔
UNCOV
346
            parts.push("expected details".to_string());
×
347
        }
1✔
348

349
        if parts.is_empty() {
1✔
350
            "error expected".to_string()
×
351
        } else {
352
            parts.join(", ")
1✔
353
        }
354
    }
1✔
355

356
    /// Check if error text contains expected code
357
    pub fn error_contains_code(error_text: &str, expected_code: i64) -> bool {
2✔
358
        if let Some(code_name) = Self::grpc_code_name_from_numeric(expected_code) {
2✔
359
            error_text.contains(&format!("status: {}", code_name))
2✔
360
                || error_text.contains(&format!("code: {}", expected_code))
1✔
361
        } else {
UNCOV
362
            error_text.contains(&format!("code: {}", expected_code))
×
363
        }
364
    }
2✔
365

366
    /// Check if error text contains expected message
367
    pub fn error_contains_message(error_text: &str, expected_message: &str) -> bool {
2✔
368
        error_text.contains(expected_message)
2✔
369
    }
2✔
370
}
371

372
#[cfg(test)]
373
mod tests {
374
    use super::*;
375
    use prost::Message;
376
    use serde_json::json;
377
    use tonic::Code;
378

379
    fn status_with_details() -> tonic::Status {
6✔
380
        let error_info = GoogleRpcErrorInfo {
6✔
381
            reason: "API_DISABLED".to_string(),
6✔
382
            domain: "your.service.com".to_string(),
6✔
383
            metadata: std::collections::HashMap::from([
6✔
384
                ("service".to_string(), "your.service.com".to_string()),
6✔
385
                ("consumer".to_string(), "projects/123".to_string()),
6✔
386
            ]),
6✔
387
        };
6✔
388

389
        let bad_request = GoogleRpcBadRequest {
6✔
390
            field_violations: vec![GoogleRpcBadRequestFieldViolation {
6✔
391
                field: "name".to_string(),
6✔
392
                description: "Name must be at least 3 characters".to_string(),
6✔
393
            }],
6✔
394
        };
6✔
395

396
        let status_proto = GoogleRpcStatus {
6✔
397
            code: Code::InvalidArgument as i32,
6✔
398
            message: "Invalid argument provided".to_string(),
6✔
399
            details: vec![
6✔
400
                Any {
6✔
401
                    type_url: "type.googleapis.com/google.rpc.ErrorInfo".to_string(),
6✔
402
                    value: error_info.encode_to_vec(),
6✔
403
                },
6✔
404
                Any {
6✔
405
                    type_url: "type.googleapis.com/google.rpc.BadRequest".to_string(),
6✔
406
                    value: bad_request.encode_to_vec(),
6✔
407
                },
6✔
408
            ],
6✔
409
        };
6✔
410

411
        tonic::Status::with_details(
6✔
412
            Code::InvalidArgument,
6✔
413
            "Invalid argument provided",
414
            status_proto.encode_to_vec().into(),
6✔
415
        )
416
    }
6✔
417

418
    fn status_without_details() -> tonic::Status {
4✔
419
        tonic::Status::new(Code::InvalidArgument, "Invalid argument provided")
4✔
420
    }
4✔
421

422
    #[test]
423
    fn test_error_matches_expected_message() {
1✔
424
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
425
        let expected = json!({"message": "Resource not found"});
1✔
426

427
        assert!(ErrorHandler::error_matches_expected(error_text, &expected));
1✔
428
    }
1✔
429

430
    #[test]
431
    fn test_error_matches_expected_code() {
1✔
432
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
433
        let expected = json!({"code": 5});
1✔
434

435
        assert!(ErrorHandler::error_matches_expected(error_text, &expected));
1✔
436
    }
1✔
437

438
    #[test]
439
    fn test_error_matches_expected_both() {
1✔
440
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
441
        let expected = json!({"code": 5, "message": "Resource not found"});
1✔
442

443
        assert!(ErrorHandler::error_matches_expected(error_text, &expected));
1✔
444
    }
1✔
445

446
    #[test]
447
    fn test_error_matches_expected_wrong_message() {
1✔
448
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
449
        let expected = json!({"message": "Wrong message"});
1✔
450

451
        assert!(!ErrorHandler::error_matches_expected(error_text, &expected));
1✔
452
    }
1✔
453

454
    #[test]
455
    fn test_error_matches_expected_wrong_code() {
1✔
456
        let error_text = "Error: status: NotFound, message: Resource not found";
1✔
457
        let expected = json!({"code": 3});
1✔
458

459
        assert!(!ErrorHandler::error_matches_expected(error_text, &expected));
1✔
460
    }
1✔
461

462
    #[test]
463
    fn test_grpc_code_name_from_numeric() {
1✔
464
        assert_eq!(ErrorHandler::grpc_code_name_from_numeric(0), Some("OK"));
1✔
465
        assert_eq!(
1✔
466
            ErrorHandler::grpc_code_name_from_numeric(5),
1✔
467
            Some("NotFound")
468
        );
469
        assert_eq!(
1✔
470
            ErrorHandler::grpc_code_name_from_numeric(13),
1✔
471
            Some("Internal")
472
        );
473
        assert_eq!(ErrorHandler::grpc_code_name_from_numeric(999), None);
1✔
474
    }
1✔
475

476
    #[test]
477
    fn test_format_error_message() {
1✔
478
        let expected = json!({"code": 5, "message": "Resource not found"});
1✔
479
        let formatted = ErrorHandler::format_error_message("", &expected);
1✔
480

481
        assert!(formatted.contains("expected message: Resource not found"));
1✔
482
        assert!(formatted.contains("expected code: 5 (NotFound)"));
1✔
483
    }
1✔
484

485
    #[test]
486
    fn test_error_contains_code() {
1✔
487
        let error_text = "Error: status: NotFound, code: 5";
1✔
488

489
        assert!(ErrorHandler::error_contains_code(error_text, 5));
1✔
490
        assert!(!ErrorHandler::error_contains_code(error_text, 3));
1✔
491
    }
1✔
492

493
    #[test]
494
    fn test_error_contains_message() {
1✔
495
        let error_text = "Error: Resource not found";
1✔
496

497
        assert!(ErrorHandler::error_contains_message(
1✔
498
            error_text,
1✔
499
            "Resource not found"
1✔
500
        ));
501
        assert!(!ErrorHandler::error_contains_message(
1✔
502
            error_text,
1✔
503
            "Wrong message"
1✔
504
        ));
1✔
505
    }
1✔
506

507
    #[test]
508
    fn test_error_matches_expected_string() {
1✔
509
        let error_text = "Error: Resource not found";
1✔
510
        let expected = json!("Resource not found");
1✔
511

512
        assert!(ErrorHandler::error_matches_expected(error_text, &expected));
1✔
513
    }
1✔
514

515
    #[test]
516
    fn test_status_matches_expected_with_details() {
1✔
517
        let status = status_with_details();
1✔
518
        let expected = json!({
1✔
519
            "code": 3,
1✔
520
            "message": "Invalid argument provided",
1✔
521
            "details": [
1✔
522
                {
523
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
524
                    "reason": "API_DISABLED",
1✔
525
                    "domain": "your.service.com",
1✔
526
                    "metadata": {
1✔
527
                        "service": "your.service.com",
1✔
528
                        "consumer": "projects/123"
1✔
529
                    }
530
                },
531
                {
532
                    "@type": "type.googleapis.com/google.rpc.BadRequest",
1✔
533
                    "fieldViolations": [
1✔
534
                        {
535
                            "field": "name",
1✔
536
                            "description": "Name must be at least 3 characters"
1✔
537
                        }
538
                    ]
539
                }
540
            ]
541
        });
542

543
        assert!(ErrorHandler::status_matches_expected(&status, &expected));
1✔
544
    }
1✔
545

546
    #[test]
547
    fn test_status_matches_expected_with_wrong_details() {
1✔
548
        let status = status_with_details();
1✔
549
        let expected = json!({
1✔
550
            "details": [
1✔
551
                {
552
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
553
                    "reason": "WRONG_REASON"
1✔
554
                }
555
            ]
556
        });
557

558
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
559
    }
1✔
560

561
    #[test]
562
    fn test_status_matches_expected_fails_when_actual_has_unexpected_details() {
1✔
563
        let status = status_with_details();
1✔
564
        let expected = json!({
1✔
565
            "code": 3,
1✔
566
            "message": "Invalid argument provided"
1✔
567
        });
568

569
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
570
    }
1✔
571

572
    #[test]
573
    fn test_status_matches_expected_fails_when_expected_requires_details() {
1✔
574
        let status = status_without_details();
1✔
575
        let expected = json!({
1✔
576
            "code": 3,
1✔
577
            "message": "Invalid argument provided",
1✔
578
            "details": [
1✔
579
                {
580
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
581
                    "reason": "API_DISABLED"
1✔
582
                }
583
            ]
584
        });
585

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

589
    #[test]
590
    fn test_status_matches_expected_passes_when_no_details_on_both_sides() {
1✔
591
        let status = status_without_details();
1✔
592
        let expected = json!({
1✔
593
            "code": 3,
1✔
594
            "message": "Invalid argument provided"
1✔
595
        });
596

597
        assert!(ErrorHandler::status_matches_expected(&status, &expected));
1✔
598
    }
1✔
599

600
    #[test]
601
    fn test_status_matches_expected_rejects_partial_message_match() {
1✔
602
        let status = status_without_details();
1✔
603
        let expected = json!({
1✔
604
            "code": 3,
1✔
605
            "message": "Invalid argument"
1✔
606
        });
607

608
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
609
        let reason = ErrorHandler::status_mismatch_reason(&status, &expected).unwrap();
1✔
610
        assert!(reason.contains("expected 'Invalid argument'"));
1✔
611
        assert!(reason.contains("got 'Invalid argument provided'"));
1✔
612
    }
1✔
613

614
    #[test]
615
    fn test_status_mismatch_reason_for_unexpected_details() {
1✔
616
        let status = status_with_details();
1✔
617
        let expected = json!({
1✔
618
            "code": 3,
1✔
619
            "message": "Invalid argument provided"
1✔
620
        });
621

622
        let reason = ErrorHandler::status_mismatch_reason(&status, &expected).unwrap();
1✔
623
        assert!(reason.contains("ERROR.details is missing"));
1✔
624
        assert!(reason.contains("actual details"));
1✔
625
        assert!(reason.contains("type.googleapis.com/google.rpc.ErrorInfo"));
1✔
626
    }
1✔
627

628
    #[test]
629
    fn test_status_mismatch_reason_for_missing_required_details() {
1✔
630
        let status = status_without_details();
1✔
631
        let expected = json!({
1✔
632
            "code": 3,
1✔
633
            "message": "Invalid argument provided",
1✔
634
            "details": [
1✔
635
                {"@type": "type.googleapis.com/google.rpc.ErrorInfo"}
1✔
636
            ]
637
        });
638

639
        let reason = ErrorHandler::status_mismatch_reason(&status, &expected).unwrap();
1✔
640
        assert!(reason.contains("details mismatch"));
1✔
641
    }
1✔
642

643
    #[test]
644
    fn test_status_to_json_contains_details() {
1✔
645
        let status = status_with_details();
1✔
646
        let json = ErrorHandler::status_to_json(&status);
1✔
647
        assert_eq!(json["code"], 3);
1✔
648
        assert_eq!(json["message"], "Invalid argument provided");
1✔
649
        assert!(json.get("details").is_some());
1✔
650
    }
1✔
651

652
    #[test]
653
    fn test_status_matches_expected_fails_when_details_field_is_missing_in_expected_object() {
1✔
654
        let status = status_with_details();
1✔
655
        let expected = json!({
1✔
656
            "code": 3,
1✔
657
            "message": "Invalid argument provided",
1✔
658
            "details": [
1✔
659
                {
660
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
661
                    "reason": "API_DISABLED",
1✔
662
                    "domain": "your.service.com",
1✔
663
                    "metadata": {
1✔
664
                        "service": "your.service.com",
1✔
665
                        "consumer": "projects/123"
1✔
666
                    }
667
                },
668
                {
669
                    "@type": "type.googleapis.com/google.rpc.BadRequest",
1✔
670
                    "fieldViolations": [
1✔
671
                        {
672
                            "description": "Name must be at least 3 characters"
1✔
673
                        }
674
                    ]
675
                }
676
            ]
677
        });
678

679
        assert!(!ErrorHandler::status_matches_expected(&status, &expected));
1✔
680
        let reason = ErrorHandler::status_mismatch_reason(&status, &expected).unwrap();
1✔
681
        assert!(reason.contains("details mismatch at index 1"));
1✔
682
    }
1✔
683
}
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