guacamole_client/
error.rs1use std::time::Duration;
2
3use reqwest::StatusCode;
4
5#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum Error {
9 #[error("HTTP request failed: {0}")]
11 Request(#[from] reqwest::Error),
12
13 #[error("not authenticated — call login() first")]
15 NotAuthenticated,
16
17 #[error("invalid data source: {0}")]
19 InvalidDataSource(String),
20
21 #[error("invalid username: {0}")]
23 InvalidUsername(String),
24
25 #[error("invalid connection ID: {0}")]
27 InvalidConnectionId(String),
28
29 #[error("invalid sharing profile ID: {0}")]
31 InvalidSharingProfileId(String),
32
33 #[error("invalid user group ID: {0}")]
35 InvalidUserGroupId(String),
36
37 #[error("invalid connection group ID: {0}")]
39 InvalidConnectionGroupId(String),
40
41 #[error("invalid tunnel ID: {0}")]
43 InvalidTunnelId(String),
44
45 #[error("invalid auth token: {0}")]
47 InvalidToken(String),
48
49 #[error("invalid query parameter `{name}`: {reason}")]
51 InvalidQueryParam {
52 name: String,
54 reason: String,
56 },
57
58 #[error("authentication failed (401)")]
60 Unauthorized {
61 body: String,
63 },
64
65 #[error("access denied (403)")]
67 Forbidden {
68 body: String,
70 },
71
72 #[error("resource not found (404): {resource}")]
74 NotFound {
75 resource: String,
77 body: String,
79 },
80
81 #[error("rate limited (429)")]
83 RateLimited {
84 retry_after: Option<Duration>,
86 },
87
88 #[error("API error (HTTP {status}): {body}")]
90 Api {
91 status: StatusCode,
93 body: String,
95 },
96}
97
98pub 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}