volga_oauth_client/
error.rs1use http::StatusCode;
10use std::fmt::{Display, Formatter};
11use volga_oauth_core::OAuthError;
12
13#[derive(Debug)]
15#[non_exhaustive]
16pub enum ClientError {
17 Protocol(OAuthError),
19
20 Http(StatusCode),
23
24 Transport(Box<dyn std::error::Error + Send + Sync>),
26
27 Decode(serde_json::Error),
29
30 InsecureUrl(String),
33
34 Validation(String),
38}
39
40impl ClientError {
41 pub fn transport(err: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
43 Self::Transport(err.into())
44 }
45
46 pub fn validation(reason: impl Into<String>) -> Self {
48 Self::Validation(reason.into())
49 }
50}
51
52impl Display for ClientError {
53 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Self::Protocol(err) => Display::fmt(err, f),
56 Self::Http(status) => write!(f, "unexpected HTTP status: {status}"),
57 Self::Transport(err) => write!(f, "transport error: {err}"),
58 Self::Decode(err) => write!(f, "malformed response body: {err}"),
59 Self::InsecureUrl(url) => write!(f, "insecure URL rejected (HTTPS is enforced): {url}"),
60 Self::Validation(reason) => write!(f, "response validation failed: {reason}"),
61 }
62 }
63}
64
65impl std::error::Error for ClientError {
66 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
67 match self {
68 Self::Protocol(err) => Some(err),
69 Self::Transport(err) => Some(err.as_ref()),
70 Self::Decode(err) => Some(err),
71 _ => None,
72 }
73 }
74}
75
76impl From<OAuthError> for ClientError {
77 #[inline]
78 fn from(err: OAuthError) -> Self {
79 Self::Protocol(err)
80 }
81}
82
83impl From<serde_json::Error> for ClientError {
84 #[inline]
85 fn from(err: serde_json::Error) -> Self {
86 Self::Decode(err)
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use volga_oauth_core::OAuthErrorCode;
94
95 #[test]
96 fn it_displays_all_variants() {
97 let cases: [(ClientError, &str); 6] = [
98 (
99 OAuthError::new(OAuthErrorCode::InvalidGrant)
100 .with_description("expired")
101 .into(),
102 "invalid_grant: expired",
103 ),
104 (
105 ClientError::Http(StatusCode::BAD_GATEWAY),
106 "unexpected HTTP status: 502 Bad Gateway",
107 ),
108 (
109 ClientError::transport(std::io::Error::other("connection reset")),
110 "transport error: connection reset",
111 ),
112 (
113 serde_json::from_str::<serde_json::Value>("{")
114 .unwrap_err()
115 .into(),
116 "malformed response body: EOF while parsing an object at line 1 column 1",
117 ),
118 (
119 ClientError::InsecureUrl("http://auth.example.com".into()),
120 "insecure URL rejected (HTTPS is enforced): http://auth.example.com",
121 ),
122 (
123 ClientError::validation("issuer mismatch"),
124 "response validation failed: issuer mismatch",
125 ),
126 ];
127 for (err, expected) in cases {
128 assert_eq!(err.to_string(), expected);
129 }
130 }
131
132 #[test]
133 fn it_exposes_error_sources() {
134 let err: ClientError = OAuthError::new(OAuthErrorCode::InvalidGrant).into();
135 assert!(std::error::Error::source(&err).is_some());
136
137 let err = ClientError::transport(std::io::Error::other("reset"));
138 assert!(std::error::Error::source(&err).is_some());
139
140 let err = ClientError::Http(StatusCode::BAD_GATEWAY);
141 assert!(std::error::Error::source(&err).is_none());
142 }
143}