Skip to main content

zoom_cli/api/
mod.rs

1pub mod client;
2pub mod types;
3
4pub use client::ZoomClient;
5pub use types::*;
6
7use std::fmt;
8
9#[derive(Debug)]
10pub enum ApiError {
11    /// Bad credentials or forbidden (401/403).
12    Auth(String),
13    /// Resource not found (404).
14    NotFound(String),
15    /// Invalid user input or missing config.
16    InvalidInput(String),
17    /// HTTP 429 rate limit.
18    RateLimit,
19    /// Non-2xx response from the Zoom API.
20    Api { status: u16, message: String },
21    /// Network / TLS error.
22    Http(reqwest::Error),
23    /// Any other error.
24    Other(String),
25}
26
27impl fmt::Display for ApiError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            ApiError::Auth(msg) => write!(
31                f,
32                "Authentication failed: {msg}\nCheck your credentials or run `zoom config show`."
33            ),
34            ApiError::NotFound(msg) => write!(f, "Not found: {msg}"),
35            ApiError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
36            ApiError::RateLimit => write!(
37                f,
38                "Rate limited by Zoom (429). Please wait and try again.\nNote: meeting creation is capped at 100 requests/day per user."
39            ),
40            ApiError::Api { status, message } => write!(f, "API error {status}: {message}"),
41            ApiError::Http(e) => write!(f, "HTTP error: {e}"),
42            ApiError::Other(msg) => write!(f, "{msg}"),
43        }
44    }
45}
46
47impl std::error::Error for ApiError {
48    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
49        match self {
50            ApiError::Http(e) => Some(e),
51            _ => None,
52        }
53    }
54}
55
56impl From<reqwest::Error> for ApiError {
57    fn from(e: reqwest::Error) -> Self {
58        ApiError::Http(e)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use std::error::Error;
66
67    #[test]
68    fn auth_error_display_includes_guidance() {
69        let err = ApiError::Auth("invalid_token".into());
70        let msg = err.to_string();
71        assert!(msg.contains("Authentication failed"));
72        assert!(msg.contains("invalid_token"));
73        assert!(msg.contains("credentials"), "should hint at how to fix");
74    }
75
76    #[test]
77    fn not_found_error_display_includes_message() {
78        let err = ApiError::NotFound("meeting 123456789 not found".into());
79        let msg = err.to_string();
80        assert!(msg.contains("Not found"));
81        assert!(msg.contains("123456789"));
82    }
83
84    #[test]
85    fn invalid_input_error_display_includes_message() {
86        let err = ApiError::InvalidInput("account_id is required".into());
87        let msg = err.to_string();
88        assert!(msg.contains("Invalid input"));
89        assert!(msg.contains("account_id is required"));
90    }
91
92    #[test]
93    fn rate_limit_error_mentions_daily_cap() {
94        let err = ApiError::RateLimit;
95        let msg = err.to_string();
96        assert!(msg.to_lowercase().contains("rate limit") || msg.contains("Rate limit"));
97        assert!(
98            msg.contains("100"),
99            "should mention the 100/day meeting cap"
100        );
101    }
102
103    #[test]
104    fn api_error_display_includes_status_and_message() {
105        let err = ApiError::Api {
106            status: 400,
107            message: "Invalid parameter: duration".into(),
108        };
109        let msg = err.to_string();
110        assert!(msg.contains("400"));
111        assert!(msg.contains("Invalid parameter: duration"));
112    }
113
114    #[test]
115    fn other_error_display_is_verbatim() {
116        let err = ApiError::Other("unexpected failure".into());
117        assert_eq!(err.to_string(), "unexpected failure");
118    }
119
120    #[test]
121    fn http_error_source_is_underlying_reqwest_error() {
122        let rt = tokio::runtime::Runtime::new().unwrap();
123        let reqwest_err = rt.block_on(async {
124            reqwest::Client::new()
125                .get("http://127.0.0.1:1")
126                .send()
127                .await
128                .unwrap_err()
129        });
130        let api_err = ApiError::Http(reqwest_err);
131        assert!(api_err.source().is_some());
132    }
133
134    #[test]
135    fn non_http_variants_have_no_source() {
136        assert!(ApiError::Auth("x".into()).source().is_none());
137        assert!(ApiError::NotFound("x".into()).source().is_none());
138        assert!(ApiError::InvalidInput("x".into()).source().is_none());
139        assert!(ApiError::RateLimit.source().is_none());
140        assert!(ApiError::Other("x".into()).source().is_none());
141    }
142}