Skip to main content

zoom_cli/commands/
reports.rs

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