Skip to main content

bitwarden_auth/send_access/api/
token_api_error_response.rs

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