Skip to main content

zoom_cli/commands/
reports.rs

1use crate::api::{ApiError, ZoomClient};
2use crate::output::{self, OutputConfig};
3
4#[allow(clippy::too_many_arguments)]
5pub async fn meetings(
6    client: &mut ZoomClient,
7    out: &OutputConfig,
8    user: &str,
9    from: &str,
10    to: Option<&str>,
11    limit: Option<u32>,
12    offset: Option<u32>,
13    fields: Option<&[String]>,
14) -> Result<(), ApiError> {
15    let result = client.list_user_meeting_reports(user, from, to).await?;
16
17    if out.json {
18        let mut items: Vec<serde_json::Value> = result
19            .meetings
20            .iter()
21            .map(|m| serde_json::to_value(m).expect("serialize"))
22            .collect();
23
24        if let Some(field_list) = fields {
25            items = items
26                .into_iter()
27                .map(|mut item| {
28                    if let Some(obj) = item.as_object_mut() {
29                        obj.retain(|k, _| field_list.iter().any(|f| f == k));
30                    }
31                    item
32                })
33                .collect();
34        }
35
36        let total = result.total_records.unwrap_or(items.len() as u64);
37        let offset_val = offset.unwrap_or(0) as usize;
38        let limited: Vec<serde_json::Value> = items
39            .into_iter()
40            .skip(offset_val)
41            .take(limit.unwrap_or(u32::MAX) as usize)
42            .collect();
43        let actual_limit = limit.unwrap_or(limited.len() as u32);
44
45        let envelope = serde_json::json!({
46            "items": limited,
47            "total": total,
48            "limit": actual_limit,
49            "offset": offset.unwrap_or(0)
50        });
51        out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
52    } else {
53        if result.meetings.is_empty() {
54            out.print_message("No meetings found.");
55            return Ok(());
56        }
57        let rows: Vec<Vec<String>> = result
58            .meetings
59            .iter()
60            .map(|m| {
61                vec![
62                    m.id.map(|id| id.to_string()).unwrap_or_default(),
63                    m.topic.clone().unwrap_or_default(),
64                    m.start_time
65                        .as_deref()
66                        .map(output::format_timestamp)
67                        .unwrap_or_else(|| "-".into()),
68                    m.duration
69                        .map(|d| format!("{d} min"))
70                        .unwrap_or_else(|| "-".into()),
71                    m.participants_count
72                        .map(|c| c.to_string())
73                        .unwrap_or_else(|| "-".into()),
74                ]
75            })
76            .collect();
77        out.print_data(&output::table(
78            &["ID", "TOPIC", "START TIME", "DURATION", "PARTICIPANTS"],
79            &rows,
80        ));
81        if let Some(total) = result.total_records {
82            out.print_message(&format!("{total} meeting(s) total"));
83        }
84    }
85    Ok(())
86}
87
88pub async fn participants(
89    client: &mut ZoomClient,
90    out: &OutputConfig,
91    meeting_id: &str,
92) -> Result<(), ApiError> {
93    let result = client.list_meeting_participant_reports(meeting_id).await?;
94
95    if out.json {
96        out.print_data(&serde_json::to_string_pretty(&result).expect("serialize"));
97    } else {
98        if result.participants.is_empty() {
99            out.print_message("No participants found.");
100            return Ok(());
101        }
102        let rows: Vec<Vec<String>> = result
103            .participants
104            .iter()
105            .map(|p| {
106                vec![
107                    p.name.clone().unwrap_or_default(),
108                    p.user_email.clone().unwrap_or_default(),
109                    p.join_time
110                        .as_deref()
111                        .map(output::format_timestamp)
112                        .unwrap_or_else(|| "-".into()),
113                    p.duration
114                        .map(|d| format!("{} min", d / 60))
115                        .unwrap_or_else(|| "-".into()),
116                ]
117            })
118            .collect();
119        out.print_data(&output::table(
120            &["NAME", "EMAIL", "JOIN TIME", "DURATION"],
121            &rows,
122        ));
123        if let Some(total) = result.total_records {
124            out.print_message(&format!("{total} participant(s) total"));
125        }
126    }
127    Ok(())
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::api::ZoomClient;
134    use wiremock::matchers::{method, path, query_param};
135    use wiremock::{Mock, MockServer, ResponseTemplate};
136
137    fn test_out() -> OutputConfig {
138        OutputConfig::for_test()
139    }
140
141    #[tokio::test]
142    async fn reports_meetings_empty_is_ok() {
143        let server = MockServer::start().await;
144        Mock::given(method("GET"))
145            .and(path("/v2/report/users/me/meetings"))
146            .and(query_param("from", "2026-04-01"))
147            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
148                "meetings": [],
149                "total_records": 0,
150                "from": "2026-04-01",
151                "to": "2026-04-30"
152            })))
153            .mount(&server)
154            .await;
155        let mut client =
156            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
157        meetings(
158            &mut client,
159            &test_out(),
160            "me",
161            "2026-04-01",
162            None,
163            None,
164            None,
165            None,
166        )
167        .await
168        .unwrap();
169    }
170
171    #[tokio::test]
172    async fn reports_meetings_renders_topic_and_duration() {
173        let server = MockServer::start().await;
174        Mock::given(method("GET"))
175            .and(path("/v2/report/users/me/meetings"))
176            .and(query_param("from", "2026-04-01"))
177            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
178                "meetings": [
179                    {
180                        "uuid": "abc==",
181                        "id": 123456789,
182                        "topic": "Weekly Standup",
183                        "start_time": "2026-04-01T09:00:00Z",
184                        "duration": 30,
185                        "participants_count": 8
186                    }
187                ],
188                "total_records": 1,
189                "from": "2026-04-01",
190                "to": "2026-04-30"
191            })))
192            .mount(&server)
193            .await;
194        let mut client =
195            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
196        let (out, buf) = OutputConfig::capturing();
197        meetings(
198            &mut client,
199            &out,
200            "me",
201            "2026-04-01",
202            None,
203            None,
204            None,
205            None,
206        )
207        .await
208        .unwrap();
209        let captured = buf.lock().unwrap().join("\n");
210        assert!(
211            captured.contains("Weekly Standup"),
212            "table must include topic"
213        );
214        assert!(captured.contains("30 min"), "table must include duration");
215        assert!(
216            captured.contains("8"),
217            "table must include participant count"
218        );
219    }
220
221    #[tokio::test]
222    async fn reports_participants_renders_name_email_and_duration() {
223        let server = MockServer::start().await;
224        Mock::given(method("GET"))
225            .and(path("/v2/report/meetings/123456789/participants"))
226            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
227                "participants": [
228                    {
229                        "name": "Alice",
230                        "user_email": "alice@example.com",
231                        "join_time": "2026-04-01T09:00:00Z",
232                        "duration": 1800
233                    }
234                ],
235                "total_records": 1
236            })))
237            .mount(&server)
238            .await;
239
240        let mut client =
241            ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
242        let (out, buf) = OutputConfig::capturing();
243        participants(&mut client, &out, "123456789").await.unwrap();
244        let captured = buf.lock().unwrap().join("\n");
245        assert!(
246            captured.contains("Alice"),
247            "table must include participant name"
248        );
249        assert!(
250            captured.contains("alice@example.com"),
251            "table must include email"
252        );
253        assert!(captured.contains("30 min"), "1800s must render as 30 min");
254    }
255}