Skip to main content

guacamole_client/
error.rs

1use std::time::Duration;
2
3use reqwest::StatusCode;
4
5/// Errors returned by the Guacamole client.
6#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum Error {
9    /// An underlying HTTP request failed.
10    #[error("HTTP request failed: {0}")]
11    Request(#[from] reqwest::Error),
12
13    /// No authentication token is available; call `login()` first.
14    #[error("not authenticated — call login() first")]
15    NotAuthenticated,
16
17    /// The supplied data source name is invalid.
18    #[error("invalid data source: {0}")]
19    InvalidDataSource(String),
20
21    /// The supplied username is invalid.
22    #[error("invalid username: {0}")]
23    InvalidUsername(String),
24
25    /// The supplied connection ID is invalid.
26    #[error("invalid connection ID: {0}")]
27    InvalidConnectionId(String),
28
29    /// The supplied sharing profile ID is invalid.
30    #[error("invalid sharing profile ID: {0}")]
31    InvalidSharingProfileId(String),
32
33    /// The supplied user group ID is invalid.
34    #[error("invalid user group ID: {0}")]
35    InvalidUserGroupId(String),
36
37    /// The supplied connection group ID is invalid.
38    #[error("invalid connection group ID: {0}")]
39    InvalidConnectionGroupId(String),
40
41    /// The supplied tunnel ID is invalid.
42    #[error("invalid tunnel ID: {0}")]
43    InvalidTunnelId(String),
44
45    /// The authentication token is invalid (contains unsafe characters).
46    #[error("invalid auth token: {0}")]
47    InvalidToken(String),
48
49    /// A query parameter value is invalid (contains unsafe characters).
50    #[error("invalid query parameter `{name}`: {reason}")]
51    InvalidQueryParam {
52        /// Name of the query parameter.
53        name: String,
54        /// Reason the value was rejected.
55        reason: String,
56    },
57
58    /// The API returned `401 Unauthorized`.
59    #[error("authentication failed (401)")]
60    Unauthorized {
61        /// Response body from the server.
62        body: String,
63    },
64
65    /// The API returned `403 Forbidden`.
66    #[error("access denied (403)")]
67    Forbidden {
68        /// Response body from the server.
69        body: String,
70    },
71
72    /// The requested resource was not found (`404`).
73    #[error("resource not found (404): {resource}")]
74    NotFound {
75        /// Name of the resource that was not found.
76        resource: String,
77        /// Response body from the server.
78        body: String,
79    },
80
81    /// The API returned `429 Too Many Requests`.
82    #[error("rate limited (429)")]
83    RateLimited {
84        /// Value of the `Retry-After` header, if present.
85        retry_after: Option<Duration>,
86    },
87
88    /// Any other non-success HTTP status code.
89    #[error("API error (HTTP {status}): {body}")]
90    Api {
91        /// HTTP status code.
92        status: StatusCode,
93        /// Response body from the server.
94        body: String,
95    },
96}
97
98/// A specialized [`Result`](std::result::Result) type for Guacamole client operations.
99pub type Result<T> = std::result::Result<T, Error>;
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn error_is_send_and_sync() {
107        fn assert_send_sync<T: Send + Sync>() {}
108        assert_send_sync::<Error>();
109    }
110
111    #[test]
112    fn error_implements_std_error() {
113        fn assert_std_error<T: std::error::Error>() {}
114        assert_std_error::<Error>();
115    }
116
117    #[test]
118    fn display_not_authenticated() {
119        let err = Error::NotAuthenticated;
120        let msg = err.to_string();
121        assert!(msg.contains("not authenticated"), "got: {msg}");
122    }
123
124    #[test]
125    fn display_invalid_data_source() {
126        let err = Error::InvalidDataSource("bad/ds".to_string());
127        let msg = err.to_string();
128        assert!(msg.contains("invalid data source"), "got: {msg}");
129        assert!(msg.contains("bad/ds"), "got: {msg}");
130    }
131
132    #[test]
133    fn display_invalid_username() {
134        let err = Error::InvalidUsername("bad\0user".to_string());
135        let msg = err.to_string();
136        assert!(msg.contains("invalid username"), "got: {msg}");
137    }
138
139    #[test]
140    fn display_invalid_connection_id() {
141        let err = Error::InvalidConnectionId("abc".to_string());
142        let msg = err.to_string();
143        assert!(msg.contains("invalid connection ID"), "got: {msg}");
144        assert!(msg.contains("abc"), "got: {msg}");
145    }
146
147    #[test]
148    fn display_invalid_sharing_profile_id() {
149        let err = Error::InvalidSharingProfileId("bad".to_string());
150        let msg = err.to_string();
151        assert!(msg.contains("invalid sharing profile ID"), "got: {msg}");
152    }
153
154    #[test]
155    fn display_invalid_user_group_id() {
156        let err = Error::InvalidUserGroupId("bad".to_string());
157        let msg = err.to_string();
158        assert!(msg.contains("invalid user group ID"), "got: {msg}");
159    }
160
161    #[test]
162    fn display_invalid_connection_group_id() {
163        let err = Error::InvalidConnectionGroupId("bad".to_string());
164        let msg = err.to_string();
165        assert!(msg.contains("invalid connection group ID"), "got: {msg}");
166        assert!(msg.contains("bad"), "got: {msg}");
167    }
168
169    #[test]
170    fn display_invalid_tunnel_id() {
171        let err = Error::InvalidTunnelId("bad".to_string());
172        let msg = err.to_string();
173        assert!(msg.contains("invalid tunnel ID"), "got: {msg}");
174        assert!(msg.contains("bad"), "got: {msg}");
175    }
176
177    #[test]
178    fn display_invalid_token() {
179        let err = Error::InvalidToken("a&b".to_string());
180        let msg = err.to_string();
181        assert!(msg.contains("invalid auth token"), "got: {msg}");
182        assert!(msg.contains("a&b"), "got: {msg}");
183    }
184
185    #[test]
186    fn display_invalid_query_param() {
187        let err = Error::InvalidQueryParam {
188            name: "order".to_string(),
189            reason: "contains unsafe characters".to_string(),
190        };
191        let msg = err.to_string();
192        assert!(msg.contains("invalid query parameter"), "got: {msg}");
193        assert!(msg.contains("order"), "got: {msg}");
194    }
195
196    #[test]
197    fn display_unauthorized() {
198        let err = Error::Unauthorized {
199            body: "nope".to_string(),
200        };
201        let msg = err.to_string();
202        assert!(msg.contains("401"), "got: {msg}");
203        assert!(!msg.contains("nope"), "body should not appear in Display: {msg}");
204    }
205
206    #[test]
207    fn display_forbidden() {
208        let err = Error::Forbidden {
209            body: "nope".to_string(),
210        };
211        let msg = err.to_string();
212        assert!(msg.contains("403"), "got: {msg}");
213        assert!(!msg.contains("nope"), "body should not appear in Display: {msg}");
214    }
215
216    #[test]
217    fn display_not_found() {
218        let err = Error::NotFound {
219            resource: "user admin".to_string(),
220            body: "not here".to_string(),
221        };
222        let msg = err.to_string();
223        assert!(msg.contains("404"), "got: {msg}");
224        assert!(msg.contains("user admin"), "got: {msg}");
225    }
226
227    #[test]
228    fn display_rate_limited() {
229        let err = Error::RateLimited {
230            retry_after: Some(Duration::from_secs(60)),
231        };
232        let msg = err.to_string();
233        assert!(msg.contains("429"), "got: {msg}");
234    }
235
236    #[test]
237    fn display_api_error() {
238        let err = Error::Api {
239            status: StatusCode::INTERNAL_SERVER_ERROR,
240            body: "kaboom".to_string(),
241        };
242        let msg = err.to_string();
243        assert!(msg.contains("500"), "got: {msg}");
244        assert!(msg.contains("kaboom"), "got: {msg}");
245    }
246
247    #[test]
248    fn display_rate_limited_no_retry_after() {
249        let err = Error::RateLimited { retry_after: None };
250        let msg = err.to_string();
251        assert!(msg.contains("429"), "got: {msg}");
252    }
253}