zoom_cli/commands/
reports.rs1use 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}