use std::time::Duration;
use reqwest::StatusCode;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("HTTP request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("not authenticated — call login() first")]
NotAuthenticated,
#[error("invalid data source: {0}")]
InvalidDataSource(String),
#[error("invalid username: {0}")]
InvalidUsername(String),
#[error("invalid connection ID: {0}")]
InvalidConnectionId(String),
#[error("invalid sharing profile ID: {0}")]
InvalidSharingProfileId(String),
#[error("invalid user group ID: {0}")]
InvalidUserGroupId(String),
#[error("invalid connection group ID: {0}")]
InvalidConnectionGroupId(String),
#[error("invalid tunnel ID: {0}")]
InvalidTunnelId(String),
#[error("invalid auth token: {0}")]
InvalidToken(String),
#[error("invalid query parameter `{name}`: {reason}")]
InvalidQueryParam {
name: String,
reason: String,
},
#[error("authentication failed (401)")]
Unauthorized {
body: String,
},
#[error("access denied (403)")]
Forbidden {
body: String,
},
#[error("resource not found (404): {resource}")]
NotFound {
resource: String,
body: String,
},
#[error("rate limited (429)")]
RateLimited {
retry_after: Option<Duration>,
},
#[error("API error (HTTP {status}): {body}")]
Api {
status: StatusCode,
body: String,
},
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Error>();
}
#[test]
fn error_implements_std_error() {
fn assert_std_error<T: std::error::Error>() {}
assert_std_error::<Error>();
}
#[test]
fn display_not_authenticated() {
let err = Error::NotAuthenticated;
let msg = err.to_string();
assert!(msg.contains("not authenticated"), "got: {msg}");
}
#[test]
fn display_invalid_data_source() {
let err = Error::InvalidDataSource("bad/ds".to_string());
let msg = err.to_string();
assert!(msg.contains("invalid data source"), "got: {msg}");
assert!(msg.contains("bad/ds"), "got: {msg}");
}
#[test]
fn display_invalid_username() {
let err = Error::InvalidUsername("bad\0user".to_string());
let msg = err.to_string();
assert!(msg.contains("invalid username"), "got: {msg}");
}
#[test]
fn display_invalid_connection_id() {
let err = Error::InvalidConnectionId("abc".to_string());
let msg = err.to_string();
assert!(msg.contains("invalid connection ID"), "got: {msg}");
assert!(msg.contains("abc"), "got: {msg}");
}
#[test]
fn display_invalid_sharing_profile_id() {
let err = Error::InvalidSharingProfileId("bad".to_string());
let msg = err.to_string();
assert!(msg.contains("invalid sharing profile ID"), "got: {msg}");
}
#[test]
fn display_invalid_user_group_id() {
let err = Error::InvalidUserGroupId("bad".to_string());
let msg = err.to_string();
assert!(msg.contains("invalid user group ID"), "got: {msg}");
}
#[test]
fn display_invalid_connection_group_id() {
let err = Error::InvalidConnectionGroupId("bad".to_string());
let msg = err.to_string();
assert!(msg.contains("invalid connection group ID"), "got: {msg}");
assert!(msg.contains("bad"), "got: {msg}");
}
#[test]
fn display_invalid_tunnel_id() {
let err = Error::InvalidTunnelId("bad".to_string());
let msg = err.to_string();
assert!(msg.contains("invalid tunnel ID"), "got: {msg}");
assert!(msg.contains("bad"), "got: {msg}");
}
#[test]
fn display_invalid_token() {
let err = Error::InvalidToken("a&b".to_string());
let msg = err.to_string();
assert!(msg.contains("invalid auth token"), "got: {msg}");
assert!(msg.contains("a&b"), "got: {msg}");
}
#[test]
fn display_invalid_query_param() {
let err = Error::InvalidQueryParam {
name: "order".to_string(),
reason: "contains unsafe characters".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("invalid query parameter"), "got: {msg}");
assert!(msg.contains("order"), "got: {msg}");
}
#[test]
fn display_unauthorized() {
let err = Error::Unauthorized {
body: "nope".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("401"), "got: {msg}");
assert!(!msg.contains("nope"), "body should not appear in Display: {msg}");
}
#[test]
fn display_forbidden() {
let err = Error::Forbidden {
body: "nope".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("403"), "got: {msg}");
assert!(!msg.contains("nope"), "body should not appear in Display: {msg}");
}
#[test]
fn display_not_found() {
let err = Error::NotFound {
resource: "user admin".to_string(),
body: "not here".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("404"), "got: {msg}");
assert!(msg.contains("user admin"), "got: {msg}");
}
#[test]
fn display_rate_limited() {
let err = Error::RateLimited {
retry_after: Some(Duration::from_secs(60)),
};
let msg = err.to_string();
assert!(msg.contains("429"), "got: {msg}");
}
#[test]
fn display_api_error() {
let err = Error::Api {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: "kaboom".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("500"), "got: {msg}");
assert!(msg.contains("kaboom"), "got: {msg}");
}
#[test]
fn display_rate_limited_no_retry_after() {
let err = Error::RateLimited { retry_after: None };
let msg = err.to_string();
assert!(msg.contains("429"), "got: {msg}");
}
}