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