Skip to main content

zoom_cli/commands/
webinars.rs

1use crate::api::{ApiError, ZoomClient};
2use crate::output::{self, OutputConfig};
3
4pub async fn list(
5    client: &mut ZoomClient,
6    out: &OutputConfig,
7    user: &str,
8    limit: Option<u32>,
9    offset: Option<u32>,
10    fields: Option<&[String]>,
11) -> Result<(), ApiError> {
12    let result = client.list_webinars(user).await?;
13
14    if out.json {
15        let mut items: Vec<serde_json::Value> = result
16            .webinars
17            .iter()
18            .map(|w| serde_json::to_value(w).expect("serialize"))
19            .collect();
20
21        if let Some(field_list) = fields {
22            items = items
23                .into_iter()
24                .map(|mut item| {
25                    if let Some(obj) = item.as_object_mut() {
26                        obj.retain(|k, _| field_list.iter().any(|f| f == k));
27                    }
28                    item
29                })
30                .collect();
31        }
32
33        let total = result.total_records.unwrap_or(items.len() as u64);
34        let offset_val = offset.unwrap_or(0) as usize;
35        let limited: Vec<serde_json::Value> = items
36            .into_iter()
37            .skip(offset_val)
38            .take(limit.unwrap_or(u32::MAX) as usize)
39            .collect();
40        let actual_limit = limit.unwrap_or(limited.len() as u32);
41
42        let envelope = serde_json::json!({
43            "items": limited,
44            "total": total,
45            "limit": actual_limit,
46            "offset": offset.unwrap_or(0)
47        });
48        out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
49    } else {
50        if result.webinars.is_empty() {
51            out.print_message("No webinars found.");
52            return Ok(());
53        }
54        let rows: Vec<Vec<String>> = result
55            .webinars
56            .iter()
57            .map(|w| {
58                vec![
59                    w.id.to_string(),
60                    w.topic.clone(),
61                    w.start_time
62                        .as_deref()
63                        .map(output::format_timestamp)
64                        .unwrap_or_else(|| "-".into()),
65                    w.duration
66                        .map(|d| format!("{d} min"))
67                        .unwrap_or_else(|| "-".into()),
68                ]
69            })
70            .collect();
71        out.print_data(&output::table(
72            &["ID", "TOPIC", "START TIME", "DURATION"],
73            &rows,
74        ));
75        if let Some(total) = result.total_records {
76            out.print_message(&format!("{total} webinar(s) total"));
77        }
78    }
79    Ok(())
80}
81
82pub async fn get(
83    client: &mut ZoomClient,
84    out: &OutputConfig,
85    webinar_id: u64,
86) -> Result<(), ApiError> {
87    let webinar = client.get_webinar(webinar_id).await?;
88
89    if out.json {
90        out.print_data(&serde_json::to_string_pretty(&webinar).expect("serialize"));
91    } else {
92        let join_url = webinar.join_url.clone().unwrap_or_else(|| "-".into());
93        out.print_data(&output::kv_block(&[
94            ("id", webinar.id.to_string()),
95            ("topic", webinar.topic.clone()),
96            (
97                "start_time",
98                webinar
99                    .start_time
100                    .as_deref()
101                    .map(output::format_timestamp)
102                    .unwrap_or_else(|| "-".into()),
103            ),
104            (
105                "duration",
106                webinar
107                    .duration
108                    .map(|d| format!("{d} min"))
109                    .unwrap_or_else(|| "-".into()),
110            ),
111            (
112                "status",
113                webinar.status.clone().unwrap_or_else(|| "-".into()),
114            ),
115            ("join_url", output::hyperlink(&join_url)),
116            (
117                "agenda",
118                webinar.agenda.clone().unwrap_or_else(|| "-".into()),
119            ),
120        ]));
121    }
122    Ok(())
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::api::ZoomClient;
129    use wiremock::matchers::{method, path};
130    use wiremock::{Mock, MockServer, ResponseTemplate};
131
132    fn test_out() -> OutputConfig {
133        OutputConfig::for_test()
134    }
135
136    #[tokio::test]
137    async fn webinars_list_empty_is_ok() {
138        let server = MockServer::start().await;
139        Mock::given(method("GET"))
140            .and(path("/v2/users/me/webinars"))
141            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
142                "webinars": [],
143                "total_records": 0
144            })))
145            .mount(&server)
146            .await;
147
148        let mut client =
149            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
150        list(&mut client, &test_out(), "me", None, None, None)
151            .await
152            .unwrap();
153    }
154
155    #[tokio::test]
156    async fn webinars_list_returns_table_data() {
157        let server = MockServer::start().await;
158        Mock::given(method("GET"))
159            .and(path("/v2/users/me/webinars"))
160            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
161                "webinars": [
162                    {
163                        "id": 12345678,
164                        "topic": "Annual Summit",
165                        "start_time": "2026-06-01T09:00:00Z",
166                        "duration": 120,
167                        "type": 5
168                    }
169                ],
170                "total_records": 1
171            })))
172            .mount(&server)
173            .await;
174
175        let mut client =
176            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
177        list(&mut client, &test_out(), "me", None, None, None)
178            .await
179            .unwrap();
180    }
181
182    #[tokio::test]
183    async fn webinars_get_returns_webinar() {
184        let server = MockServer::start().await;
185        Mock::given(method("GET"))
186            .and(path("/v2/webinars/12345678"))
187            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
188                "id": 12345678,
189                "topic": "Annual Summit",
190                "start_time": "2026-06-01T09:00:00Z",
191                "duration": 120,
192                "join_url": "https://zoom.us/j/12345678",
193                "status": "waiting",
194                "agenda": "Keynote and breakouts",
195                "type": 5
196            })))
197            .mount(&server)
198            .await;
199
200        let mut client =
201            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
202        get(&mut client, &test_out(), 12345678).await.unwrap();
203    }
204
205    #[tokio::test]
206    async fn webinars_get_not_found_propagates() {
207        let server = MockServer::start().await;
208        Mock::given(method("GET"))
209            .and(path("/v2/webinars/99999999"))
210            .respond_with(ResponseTemplate::new(404).set_body_string("Webinar not found"))
211            .mount(&server)
212            .await;
213
214        let mut client =
215            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
216        let err = get(&mut client, &test_out(), 99999999).await.unwrap_err();
217        assert!(matches!(err, ApiError::NotFound(_)));
218    }
219}