bitwarden_auth/send_access/api/
token_api_error_response.rs

1use serde::{Deserialize, Serialize};
2#[cfg(feature = "wasm")]
3use tsify::Tsify;
4
5#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
6#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
7#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
8#[serde(rename_all = "snake_case")]
9/// Invalid request errors - typically due to missing parameters.
10pub enum SendAccessTokenInvalidRequestError {
11    #[allow(missing_docs)]
12    SendIdRequired,
13
14    #[allow(missing_docs)]
15    PasswordHashB64Required,
16
17    #[allow(missing_docs)]
18    EmailRequired,
19
20    #[allow(missing_docs)]
21    EmailAndOtpRequiredOtpSent,
22
23    /// Fallback for unknown variants for forward compatibility
24    #[serde(other)]
25    Unknown,
26}
27
28#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
29#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
30#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
31#[serde(rename_all = "snake_case")]
32/// Invalid grant errors - typically due to invalid credentials.
33pub enum SendAccessTokenInvalidGrantError {
34    #[allow(missing_docs)]
35    SendIdInvalid,
36
37    #[allow(missing_docs)]
38    PasswordHashB64Invalid,
39
40    #[allow(missing_docs)]
41    EmailInvalid,
42
43    #[allow(missing_docs)]
44    OtpInvalid,
45
46    #[allow(missing_docs)]
47    OtpGenerationFailed,
48
49    /// Fallback for unknown variants for forward compatibility
50    #[serde(other)]
51    Unknown,
52}
53
54#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
55#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
56#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
57#[serde(rename_all = "snake_case")]
58#[serde(tag = "error")]
59// ^ "error" becomes the variant discriminator which matches against the rename annotations;
60// "error_description" is the payload for that variant which can be optional.
61/// Represents the possible, expected errors that can occur when requesting a send access token.
62pub enum SendAccessTokenApiErrorResponse {
63    /// Invalid request error, typically due to missing parameters for a specific
64    /// credential flow. Ex. `send_id` is required.
65    InvalidRequest {
66        #[serde(default, skip_serializing_if = "Option::is_none")]
67        #[cfg_attr(feature = "wasm", tsify(optional))]
68        /// The optional error description for invalid request errors.
69        error_description: Option<String>,
70
71        #[serde(default, skip_serializing_if = "Option::is_none")]
72        #[cfg_attr(feature = "wasm", tsify(optional))]
73        /// The optional specific error type for invalid request errors.
74        send_access_error_type: Option<SendAccessTokenInvalidRequestError>,
75    },
76
77    /// Invalid grant error, typically due to invalid credentials.
78    InvalidGrant {
79        #[serde(default, skip_serializing_if = "Option::is_none")]
80        #[cfg_attr(feature = "wasm", tsify(optional))]
81        /// The optional error description for invalid grant errors.
82        error_description: Option<String>,
83
84        #[serde(default, skip_serializing_if = "Option::is_none")]
85        #[cfg_attr(feature = "wasm", tsify(optional))]
86        /// The optional specific error type for invalid grant errors.
87        send_access_error_type: Option<SendAccessTokenInvalidGrantError>,
88    },
89
90    /// Invalid client error, typically due to an invalid client secret or client ID.
91    InvalidClient {
92        #[serde(default, skip_serializing_if = "Option::is_none")]
93        #[cfg_attr(feature = "wasm", tsify(optional))]
94        /// The optional error description for invalid client errors.
95        error_description: Option<String>,
96    },
97
98    /// Unauthorized client error, typically due to an unauthorized client.
99    UnauthorizedClient {
100        #[serde(default, skip_serializing_if = "Option::is_none")]
101        #[cfg_attr(feature = "wasm", tsify(optional))]
102        /// The optional error description for unauthorized client errors.
103        error_description: Option<String>,
104    },
105
106    /// Unsupported grant type error, typically due to an unsupported credential flow.
107    /// Note: during initial feature rollout, this will be used to indicate that the
108    /// feature flag is disabled.
109    UnsupportedGrantType {
110        #[serde(default, skip_serializing_if = "Option::is_none")]
111        #[cfg_attr(feature = "wasm", tsify(optional))]
112        /// The optional error description for unsupported grant type errors.
113        error_description: Option<String>,
114    },
115
116    /// Invalid scope error, typically due to an invalid scope requested.
117    InvalidScope {
118        #[serde(default, skip_serializing_if = "Option::is_none")]
119        #[cfg_attr(feature = "wasm", tsify(optional))]
120        /// The optional error description for invalid scope errors.
121        error_description: Option<String>,
122    },
123
124    /// Invalid target error which is shown if the requested
125    /// resource is invalid, missing, unknown, or malformed.
126    InvalidTarget {
127        #[serde(default, skip_serializing_if = "Option::is_none")]
128        #[cfg_attr(feature = "wasm", tsify(optional))]
129        /// The optional error description for invalid target errors.
130        error_description: Option<String>,
131    },
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    mod send_access_token_invalid_request_error_tests {
139        use serde_json::{Value, from_str, json, to_string, to_value};
140
141        use super::*;
142
143        #[test]
144        fn invalid_request_variants_serde_tests() {
145            // (expected_variant, send_access_error_type)
146            let cases: &[(SendAccessTokenInvalidRequestError, &str)] = &[
147                (
148                    SendAccessTokenInvalidRequestError::SendIdRequired,
149                    "\"send_id_required\"",
150                ),
151                (
152                    SendAccessTokenInvalidRequestError::PasswordHashB64Required,
153                    "\"password_hash_b64_required\"",
154                ),
155                (
156                    SendAccessTokenInvalidRequestError::EmailRequired,
157                    "\"email_required\"",
158                ),
159                (
160                    SendAccessTokenInvalidRequestError::EmailAndOtpRequiredOtpSent,
161                    "\"email_and_otp_required_otp_sent\"",
162                ),
163            ];
164
165            for (expected_variant, send_access_error_type_json) in cases {
166                // Deserialize from send_access_error_type to enum
167                let error_from_send_access_error_type: SendAccessTokenInvalidRequestError =
168                    from_str(send_access_error_type_json).unwrap();
169                assert_eq!(
170                    &error_from_send_access_error_type, expected_variant,
171                    "send_access_error_type should map to the expected variant"
172                );
173
174                // Serializing enum -> JSON string containing send_access_error_type
175                let json_from_variant = to_string(expected_variant).unwrap();
176                assert_eq!(
177                    json_from_variant, *send_access_error_type_json,
178                    "serialization should emit the send_access_error_type_json"
179                );
180
181                // Type-safe check: to_value() → Value::String, then compare the
182                // code; this avoids formatting/quoting concerns from to_string().
183                let value_from_variant = to_value(expected_variant).unwrap();
184                assert_eq!(
185                    value_from_variant,
186                    Value::String(send_access_error_type_json.trim_matches('"').to_string()),
187                    "serialization as value should match json generated from enum"
188                );
189
190                // Round-trip: send_access_error_type -> enum -> send_access_error_type
191                let round_tripped_code = to_string(&error_from_send_access_error_type).unwrap();
192                assert_eq!(
193                    round_tripped_code, *send_access_error_type_json,
194                    "round-trip should preserve the send_access_error_type_json"
195                );
196            }
197        }
198
199        #[test]
200        fn invalid_request_full_payload_with_both_fields_parses() {
201            let payload = json!({
202                "error": "invalid_request",
203                "error_description": "send_id is required.",
204                "send_access_error_type": "send_id_required"
205            })
206            .to_string();
207
208            let parsed: SendAccessTokenApiErrorResponse = from_str(&payload).unwrap();
209            match parsed {
210                SendAccessTokenApiErrorResponse::InvalidRequest {
211                    error_description,
212                    send_access_error_type,
213                } => {
214                    assert_eq!(error_description.as_deref(), Some("send_id is required."));
215                    assert_eq!(
216                        send_access_error_type,
217                        Some(SendAccessTokenInvalidRequestError::SendIdRequired)
218                    );
219                }
220                _ => panic!("expected invalid_request"),
221            }
222        }
223
224        #[test]
225        fn invalid_request_payload_without_description_is_allowed() {
226            let payload = r#"
227            {
228                "error": "invalid_request",
229                "send_access_error_type": "email_required"
230            }"#;
231
232            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
233            match parsed {
234                SendAccessTokenApiErrorResponse::InvalidRequest {
235                    error_description,
236                    send_access_error_type,
237                } => {
238                    assert!(error_description.is_none());
239                    assert_eq!(
240                        send_access_error_type,
241                        Some(SendAccessTokenInvalidRequestError::EmailRequired)
242                    );
243                }
244                _ => panic!("expected invalid_request"),
245            }
246        }
247
248        #[test]
249        fn invalid_request_unknown_code_maps_to_unknown() {
250            let payload = r#"
251            {
252                "error": "invalid_request",
253                "error_description": "something new",
254                "send_access_error_type": "brand_new_code"
255            }"#;
256
257            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
258            match parsed {
259                SendAccessTokenApiErrorResponse::InvalidRequest {
260                    error_description,
261                    send_access_error_type,
262                } => {
263                    assert_eq!(error_description.as_deref(), Some("something new"));
264                    assert_eq!(
265                        send_access_error_type,
266                        Some(SendAccessTokenInvalidRequestError::Unknown)
267                    );
268                }
269                _ => panic!("expected invalid_request"),
270            }
271        }
272
273        #[test]
274        fn invalid_request_minimal_payload_is_allowed() {
275            let payload = r#"{ "error": "invalid_request" }"#;
276            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
277            match parsed {
278                SendAccessTokenApiErrorResponse::InvalidRequest {
279                    error_description,
280                    send_access_error_type,
281                } => {
282                    assert!(error_description.is_none());
283                    assert!(send_access_error_type.is_none());
284                }
285                _ => panic!("expected invalid_request"),
286            }
287        }
288
289        #[test]
290        fn invalid_request_null_fields_become_none() {
291            let payload = r#"
292            {
293                "error": "invalid_request",
294                "error_description": null,
295                "send_access_error_type": null
296            }"#;
297
298            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
299            match parsed {
300                SendAccessTokenApiErrorResponse::InvalidRequest {
301                    error_description,
302                    send_access_error_type,
303                } => {
304                    assert!(error_description.is_none());
305                    assert!(send_access_error_type.is_none());
306                }
307                _ => panic!("expected invalid_request"),
308            }
309        }
310    }
311
312    mod send_access_token_invalid_grant_error_tests {
313        use serde_json::{Value, from_str, json, to_string, to_value};
314
315        use super::*;
316
317        #[test]
318        fn invalid_grant_variants_serde_tests() {
319            // (expected_variant, send_access_error_type)
320            let cases: &[(SendAccessTokenInvalidGrantError, &str)] = &[
321                (
322                    SendAccessTokenInvalidGrantError::SendIdInvalid,
323                    "\"send_id_invalid\"",
324                ),
325                (
326                    SendAccessTokenInvalidGrantError::PasswordHashB64Invalid,
327                    "\"password_hash_b64_invalid\"",
328                ),
329                (
330                    SendAccessTokenInvalidGrantError::EmailInvalid,
331                    "\"email_invalid\"",
332                ),
333                (
334                    SendAccessTokenInvalidGrantError::OtpInvalid,
335                    "\"otp_invalid\"",
336                ),
337                (
338                    SendAccessTokenInvalidGrantError::OtpGenerationFailed,
339                    "\"otp_generation_failed\"",
340                ),
341            ];
342
343            for (expected_variant, send_access_error_type_json) in cases {
344                // Deserialize from send_access_error_type to enum
345                let error_from_send_access_error_type: SendAccessTokenInvalidGrantError =
346                    from_str(send_access_error_type_json).unwrap();
347                assert_eq!(
348                    &error_from_send_access_error_type, expected_variant,
349                    "send_access_error_type should map to the expected variant"
350                );
351
352                // Serializing enum -> JSON string containing send_access_error_type
353                let json_from_variant = to_string(expected_variant).unwrap();
354                assert_eq!(
355                    json_from_variant, *send_access_error_type_json,
356                    "serialization should emit the send_access_error_type_json"
357                );
358
359                // Type-safe check: to_value() → Value::String
360                let value_from_variant = to_value(expected_variant).unwrap();
361                assert_eq!(
362                    value_from_variant,
363                    Value::String(send_access_error_type_json.trim_matches('"').to_string()),
364                    "serialization as value should match json generated from enum"
365                );
366
367                // Round-trip: send_access_error_type -> enum -> send_access_error_type
368                let round_tripped_code = to_string(&error_from_send_access_error_type).unwrap();
369                assert_eq!(
370                    round_tripped_code, *send_access_error_type_json,
371                    "round-trip should preserve the send_access_error_type_json"
372                );
373            }
374        }
375
376        #[test]
377        fn invalid_grant_full_payload_with_both_fields_parses() {
378            let payload = json!({
379                "error": "invalid_grant",
380                "error_description": "password_hash_b64 is invalid.",
381                "send_access_error_type": "password_hash_b64_invalid"
382            })
383            .to_string();
384
385            let parsed: SendAccessTokenApiErrorResponse = from_str(&payload).unwrap();
386            match parsed {
387                SendAccessTokenApiErrorResponse::InvalidGrant {
388                    error_description,
389                    send_access_error_type,
390                } => {
391                    assert_eq!(
392                        error_description.as_deref(),
393                        Some("password_hash_b64 is invalid.")
394                    );
395                    assert_eq!(
396                        send_access_error_type,
397                        Some(SendAccessTokenInvalidGrantError::PasswordHashB64Invalid)
398                    );
399                }
400                _ => panic!("expected invalid_grant"),
401            }
402        }
403
404        #[test]
405        fn invalid_grant_payload_without_description_is_allowed() {
406            let payload = r#"
407            {
408                "error": "invalid_grant",
409                "send_access_error_type": "otp_invalid"
410            }"#;
411
412            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
413            match parsed {
414                SendAccessTokenApiErrorResponse::InvalidGrant {
415                    error_description,
416                    send_access_error_type,
417                } => {
418                    assert!(error_description.is_none());
419                    assert_eq!(
420                        send_access_error_type,
421                        Some(SendAccessTokenInvalidGrantError::OtpInvalid)
422                    );
423                }
424                _ => panic!("expected invalid_grant"),
425            }
426        }
427
428        #[test]
429        fn invalid_grant_unknown_code_maps_to_unknown() {
430            let payload = r#"
431            {
432                "error": "invalid_grant",
433                "error_description": "new server-side reason",
434                "send_access_error_type": "brand_new_grant_code"
435            }"#;
436
437            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
438            match parsed {
439                SendAccessTokenApiErrorResponse::InvalidGrant {
440                    error_description,
441                    send_access_error_type,
442                } => {
443                    assert_eq!(error_description.as_deref(), Some("new server-side reason"));
444                    assert_eq!(
445                        send_access_error_type,
446                        Some(SendAccessTokenInvalidGrantError::Unknown)
447                    );
448                }
449                _ => panic!("expected invalid_grant"),
450            }
451        }
452
453        #[test]
454        fn invalid_grant_minimal_payload_is_allowed() {
455            let payload = r#"{ "error": "invalid_grant" }"#;
456            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
457            match parsed {
458                SendAccessTokenApiErrorResponse::InvalidGrant {
459                    error_description,
460                    send_access_error_type,
461                } => {
462                    assert!(error_description.is_none());
463                    assert!(send_access_error_type.is_none());
464                }
465                _ => panic!("expected invalid_grant"),
466            }
467        }
468
469        #[test]
470        fn invalid_grant_null_fields_become_none() {
471            let payload = r#"
472            {
473                "error": "invalid_grant",
474                "error_description": null,
475                "send_access_error_type": null
476            }"#;
477
478            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
479            match parsed {
480                SendAccessTokenApiErrorResponse::InvalidGrant {
481                    error_description,
482                    send_access_error_type,
483                } => {
484                    assert!(error_description.is_none());
485                    assert!(send_access_error_type.is_none());
486                }
487                _ => panic!("expected invalid_grant"),
488            }
489        }
490    }
491
492    mod send_access_token_invalid_client_error_tests {
493        use serde_json::{from_str, json, to_value};
494
495        use super::*;
496
497        #[test]
498        fn invalid_client_full_payload_with_description_parses() {
499            let payload = json!({
500                "error": "invalid_client",
501                "error_description": "Invalid client credentials."
502            })
503            .to_string();
504
505            let parsed: SendAccessTokenApiErrorResponse = from_str(&payload).unwrap();
506            match parsed {
507                SendAccessTokenApiErrorResponse::InvalidClient { error_description } => {
508                    assert_eq!(
509                        error_description.as_deref(),
510                        Some("Invalid client credentials.")
511                    );
512                }
513                _ => panic!("expected invalid_client"),
514            }
515        }
516
517        #[test]
518        fn invalid_client_without_description_is_allowed() {
519            let payload = r#"{ "error": "invalid_client" }"#;
520
521            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
522            match parsed {
523                SendAccessTokenApiErrorResponse::InvalidClient { error_description } => {
524                    assert!(error_description.is_none());
525                }
526                _ => panic!("expected invalid_client"),
527            }
528        }
529
530        #[test]
531        fn invalid_client_serializes_back() {
532            let value = SendAccessTokenApiErrorResponse::InvalidClient {
533                error_description: Some("Invalid client credentials.".into()),
534            };
535            let j = to_value(value).unwrap();
536            assert_eq!(
537                j,
538                json!({
539                    "error": "invalid_client",
540                    "error_description": "Invalid client credentials."
541                })
542            );
543        }
544
545        #[test]
546        fn invalid_client_minimal_payload_is_allowed() {
547            let payload = r#"{ "error": "invalid_client" }"#;
548            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
549            match parsed {
550                SendAccessTokenApiErrorResponse::InvalidClient { error_description } => {
551                    assert!(error_description.is_none());
552                }
553                _ => panic!("expected invalid_client"),
554            }
555        }
556
557        #[test]
558        fn invalid_client_null_description_becomes_none() {
559            let payload = r#"
560            {
561                "error": "invalid_client",
562                "error_description": null
563            }"#;
564
565            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
566            match parsed {
567                SendAccessTokenApiErrorResponse::InvalidClient { error_description } => {
568                    assert!(error_description.is_none());
569                }
570                _ => panic!("expected invalid_client"),
571            }
572        }
573
574        #[test]
575        fn invalid_client_ignores_send_access_error_type_and_extra_fields() {
576            let payload = r#"
577            {
578                "error": "invalid_client",
579                "send_access_error_type": "should_be_ignored",
580                "extra_field": 123,
581                "error_description": "desc"
582            }"#;
583
584            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
585            match parsed {
586                SendAccessTokenApiErrorResponse::InvalidClient { error_description } => {
587                    assert_eq!(error_description.as_deref(), Some("desc"));
588                }
589                _ => panic!("expected invalid_client"),
590            }
591        }
592    }
593
594    mod send_access_token_unauthorized_client_error_tests {
595        use serde_json::{from_str, json, to_value};
596
597        use super::*;
598
599        #[test]
600        fn unauthorized_client_full_payload_with_description_parses() {
601            let payload = json!({
602                "error": "unauthorized_client",
603                "error_description": "Client not permitted to use this grant."
604            })
605            .to_string();
606
607            let parsed: SendAccessTokenApiErrorResponse = from_str(&payload).unwrap();
608            match parsed {
609                SendAccessTokenApiErrorResponse::UnauthorizedClient { error_description } => {
610                    assert_eq!(
611                        error_description.as_deref(),
612                        Some("Client not permitted to use this grant.")
613                    );
614                }
615                _ => panic!("expected unauthorized_client"),
616            }
617        }
618
619        #[test]
620        fn unauthorized_client_without_description_is_allowed() {
621            let payload = r#"{ "error": "unauthorized_client" }"#;
622
623            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
624            match parsed {
625                SendAccessTokenApiErrorResponse::UnauthorizedClient { error_description } => {
626                    assert!(error_description.is_none());
627                }
628                _ => panic!("expected unauthorized_client"),
629            }
630        }
631
632        #[test]
633        fn unauthorized_client_serializes_back() {
634            let value = SendAccessTokenApiErrorResponse::UnauthorizedClient {
635                error_description: None,
636            };
637            let j = to_value(value).unwrap();
638            assert_eq!(j, json!({ "error": "unauthorized_client" }));
639        }
640
641        #[test]
642        fn unauthorized_client_minimal_payload_is_allowed() {
643            let payload = r#"{ "error": "unauthorized_client" }"#;
644            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
645            match parsed {
646                SendAccessTokenApiErrorResponse::UnauthorizedClient { error_description } => {
647                    assert!(error_description.is_none());
648                }
649                _ => panic!("expected unauthorized_client"),
650            }
651        }
652
653        #[test]
654        fn unauthorized_client_null_description_becomes_none() {
655            let payload = r#"
656            {
657                "error": "unauthorized_client",
658                "error_description": null
659            }"#;
660
661            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
662            match parsed {
663                SendAccessTokenApiErrorResponse::UnauthorizedClient { error_description } => {
664                    assert!(error_description.is_none());
665                }
666                _ => panic!("expected unauthorized_client"),
667            }
668        }
669
670        #[test]
671        fn unauthorized_client_ignores_send_access_error_type_and_extra_fields() {
672            let payload = r#"
673            {
674                "error": "unauthorized_client",
675                "send_access_error_type": "should_be_ignored",
676                "extra_field": true
677            }"#;
678
679            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
680            match parsed {
681                SendAccessTokenApiErrorResponse::UnauthorizedClient { error_description } => {
682                    assert!(error_description.is_none());
683                }
684                _ => panic!("expected unauthorized_client"),
685            }
686        }
687    }
688
689    mod send_access_token_unsupported_grant_type_error_tests {
690        use serde_json::{from_str, json, to_value};
691
692        use super::*;
693
694        #[test]
695        fn unsupported_grant_type_full_payload_with_description_parses() {
696            let payload = json!({
697                "error": "unsupported_grant_type",
698                "error_description": "This grant type is not enabled."
699            })
700            .to_string();
701
702            let parsed: SendAccessTokenApiErrorResponse = from_str(&payload).unwrap();
703            match parsed {
704                SendAccessTokenApiErrorResponse::UnsupportedGrantType { error_description } => {
705                    assert_eq!(
706                        error_description.as_deref(),
707                        Some("This grant type is not enabled.")
708                    );
709                }
710                _ => panic!("expected unsupported_grant_type"),
711            }
712        }
713
714        #[test]
715        fn unsupported_grant_type_without_description_is_allowed() {
716            let payload = r#"{ "error": "unsupported_grant_type" }"#;
717
718            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
719            match parsed {
720                SendAccessTokenApiErrorResponse::UnsupportedGrantType { error_description } => {
721                    assert!(error_description.is_none());
722                }
723                _ => panic!("expected unsupported_grant_type"),
724            }
725        }
726
727        #[test]
728        fn unsupported_grant_type_serializes_back() {
729            let value = SendAccessTokenApiErrorResponse::UnsupportedGrantType {
730                error_description: Some("Disabled by feature flag".into()),
731            };
732            let j = to_value(value).unwrap();
733            assert_eq!(
734                j,
735                json!({
736                    "error": "unsupported_grant_type",
737                    "error_description": "Disabled by feature flag"
738                })
739            );
740        }
741
742        #[test]
743        fn unsupported_grant_type_minimal_payload_is_allowed() {
744            let payload = r#"{ "error": "unsupported_grant_type" }"#;
745            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
746            match parsed {
747                SendAccessTokenApiErrorResponse::UnsupportedGrantType { error_description } => {
748                    assert!(error_description.is_none());
749                }
750                _ => panic!("expected unsupported_grant_type"),
751            }
752        }
753
754        #[test]
755        fn unsupported_grant_type_null_description_becomes_none() {
756            let payload = r#"
757        {
758          "error": "unsupported_grant_type",
759          "error_description": null
760        }"#;
761
762            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
763            match parsed {
764                SendAccessTokenApiErrorResponse::UnsupportedGrantType { error_description } => {
765                    assert!(error_description.is_none());
766                }
767                _ => panic!("expected unsupported_grant_type"),
768            }
769        }
770
771        #[test]
772        fn unsupported_grant_type_ignores_send_access_error_type_and_extra_fields() {
773            let payload = r#"
774            {
775                "error": "unsupported_grant_type",
776                "send_access_error_type": "should_be_ignored",
777                "extra_field": "noise"
778            }"#;
779
780            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
781            match parsed {
782                SendAccessTokenApiErrorResponse::UnsupportedGrantType { error_description } => {
783                    assert!(error_description.is_none());
784                }
785                _ => panic!("expected unsupported_grant_type"),
786            }
787        }
788    }
789
790    mod send_access_token_invalid_scope_error_tests {
791        use serde_json::{from_str, json, to_value};
792
793        use super::*;
794
795        #[test]
796        fn invalid_scope_full_payload_with_description_parses() {
797            let payload = json!({
798                "error": "invalid_scope",
799                "error_description": "Requested scope is not allowed."
800            })
801            .to_string();
802
803            let parsed: SendAccessTokenApiErrorResponse = from_str(&payload).unwrap();
804            match parsed {
805                SendAccessTokenApiErrorResponse::InvalidScope { error_description } => {
806                    assert_eq!(
807                        error_description.as_deref(),
808                        Some("Requested scope is not allowed.")
809                    );
810                }
811                _ => panic!("expected invalid_scope"),
812            }
813        }
814
815        #[test]
816        fn invalid_scope_without_description_is_allowed() {
817            let payload = r#"{ "error": "invalid_scope" }"#;
818
819            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
820            match parsed {
821                SendAccessTokenApiErrorResponse::InvalidScope { error_description } => {
822                    assert!(error_description.is_none());
823                }
824                _ => panic!("expected invalid_scope"),
825            }
826        }
827
828        #[test]
829        fn invalid_scope_serializes_back() {
830            let value = SendAccessTokenApiErrorResponse::InvalidScope {
831                error_description: None,
832            };
833            let j = to_value(value).unwrap();
834            assert_eq!(j, json!({ "error": "invalid_scope" }));
835        }
836
837        #[test]
838        fn invalid_scope_minimal_payload_is_allowed() {
839            let payload = r#"{ "error": "invalid_scope" }"#;
840            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
841            match parsed {
842                SendAccessTokenApiErrorResponse::InvalidScope { error_description } => {
843                    assert!(error_description.is_none());
844                }
845                _ => panic!("expected invalid_scope"),
846            }
847        }
848
849        #[test]
850        fn invalid_scope_null_description_becomes_none() {
851            let payload = r#"
852        {
853          "error": "invalid_scope",
854          "error_description": null
855        }"#;
856
857            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
858            match parsed {
859                SendAccessTokenApiErrorResponse::InvalidScope { error_description } => {
860                    assert!(error_description.is_none());
861                }
862                _ => panic!("expected invalid_scope"),
863            }
864        }
865
866        #[test]
867        fn invalid_scope_ignores_send_access_error_type_and_extra_fields() {
868            let payload = r#"
869            {
870                "error": "invalid_scope",
871                "send_access_error_type": "should_be_ignored",
872                "extra_field": [1,2,3]
873            }"#;
874
875            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
876            match parsed {
877                SendAccessTokenApiErrorResponse::InvalidScope { error_description } => {
878                    assert!(error_description.is_none());
879                }
880                _ => panic!("expected invalid_scope"),
881            }
882        }
883    }
884
885    mod send_access_token_invalid_target_error_tests {
886        use serde_json::{from_str, json, to_value};
887
888        use super::*;
889
890        #[test]
891        fn invalid_target_full_payload_with_description_parses() {
892            let payload = json!({
893                "error": "invalid_target",
894                "error_description": "Unknown or disallowed resource indicator."
895            })
896            .to_string();
897
898            let parsed: SendAccessTokenApiErrorResponse = from_str(&payload).unwrap();
899            match parsed {
900                SendAccessTokenApiErrorResponse::InvalidTarget { error_description } => {
901                    assert_eq!(
902                        error_description.as_deref(),
903                        Some("Unknown or disallowed resource indicator.")
904                    );
905                }
906                _ => panic!("expected invalid_target"),
907            }
908        }
909
910        #[test]
911        fn invalid_target_without_description_is_allowed() {
912            let payload = r#"{ "error": "invalid_target" }"#;
913
914            let parsed: SendAccessTokenApiErrorResponse = serde_json::from_str(payload).unwrap();
915            match parsed {
916                SendAccessTokenApiErrorResponse::InvalidTarget { error_description } => {
917                    assert!(error_description.is_none());
918                }
919                _ => panic!("expected invalid_target"),
920            }
921        }
922
923        #[test]
924        fn invalid_target_serializes_back() {
925            let value = SendAccessTokenApiErrorResponse::InvalidTarget {
926                error_description: Some("Bad resource parameter".into()),
927            };
928            let j = to_value(value).unwrap();
929            assert_eq!(
930                j,
931                json!({
932                    "error": "invalid_target",
933                    "error_description": "Bad resource parameter"
934                })
935            );
936        }
937
938        #[test]
939        fn invalid_target_minimal_payload_is_allowed() {
940            let payload = r#"{ "error": "invalid_target" }"#;
941            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
942            match parsed {
943                SendAccessTokenApiErrorResponse::InvalidTarget { error_description } => {
944                    assert!(error_description.is_none());
945                }
946                _ => panic!("expected invalid_target"),
947            }
948        }
949
950        #[test]
951        fn invalid_target_null_description_becomes_none() {
952            let payload = r#"
953        {
954          "error": "invalid_target",
955          "error_description": null
956        }"#;
957
958            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
959            match parsed {
960                SendAccessTokenApiErrorResponse::InvalidTarget { error_description } => {
961                    assert!(error_description.is_none());
962                }
963                _ => panic!("expected invalid_target"),
964            }
965        }
966
967        #[test]
968        fn invalid_target_ignores_send_access_error_type_and_extra_fields() {
969            let payload = r#"
970            {
971                "error": "invalid_target",
972                "send_access_error_type": "should_be_ignored",
973                "extra_field": {"k":"v"}
974            }"#;
975
976            let parsed: SendAccessTokenApiErrorResponse = from_str(payload).unwrap();
977            match parsed {
978                SendAccessTokenApiErrorResponse::InvalidTarget { error_description } => {
979                    assert!(error_description.is_none());
980                }
981                _ => panic!("expected invalid_target"),
982            }
983        }
984    }
985
986    #[test]
987    fn unknown_top_level_error_rejects() {
988        let payload = r#"{ "error": "totally_new_error" }"#;
989        let err = serde_json::from_str::<SendAccessTokenApiErrorResponse>(payload).unwrap_err();
990        let _ = err;
991    }
992}