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}