1use crate::api::types::{CreateUserInfo, CreateUserRequest};
2use crate::api::{ApiError, ZoomClient};
3use crate::output::{self, OutputConfig};
4
5pub async fn list(
6 client: &mut ZoomClient,
7 out: &OutputConfig,
8 status: Option<&str>,
9 limit: Option<u32>,
10 offset: Option<u32>,
11 fields: Option<&[String]>,
12) -> Result<(), ApiError> {
13 let result = client.list_users(status).await?;
14
15 if out.json {
16 let mut items: Vec<serde_json::Value> = result
17 .users
18 .iter()
19 .map(|u| serde_json::to_value(u).expect("serialize"))
20 .collect();
21
22 if let Some(field_list) = fields {
23 items = items
24 .into_iter()
25 .map(|mut item| {
26 if let Some(obj) = item.as_object_mut() {
27 obj.retain(|k, _| field_list.iter().any(|f| f == k));
28 }
29 item
30 })
31 .collect();
32 }
33
34 let total = result.total_records.unwrap_or(items.len() as u64);
35 let offset_val = offset.unwrap_or(0) as usize;
36 let limited: Vec<serde_json::Value> = items
37 .into_iter()
38 .skip(offset_val)
39 .take(limit.unwrap_or(u32::MAX) as usize)
40 .collect();
41 let actual_limit = limit.unwrap_or(limited.len() as u32);
42
43 let envelope = serde_json::json!({
44 "items": limited,
45 "total": total,
46 "limit": actual_limit,
47 "offset": offset.unwrap_or(0)
48 });
49 out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
50 } else {
51 if result.users.is_empty() {
52 out.print_message("No users found.");
53 return Ok(());
54 }
55 let rows: Vec<Vec<String>> = result
56 .users
57 .iter()
58 .map(|u| {
59 vec![
60 u.id.clone(),
61 u.email.clone(),
62 u.display_name.clone().unwrap_or_default(),
63 u.status.clone().unwrap_or_default(),
64 ]
65 })
66 .collect();
67 out.print_data(&output::table(&["ID", "EMAIL", "NAME", "STATUS"], &rows));
68 if let Some(total) = result.total_records {
69 out.print_message(&format!("{total} user(s) total"));
70 }
71 }
72 Ok(())
73}
74
75pub async fn get(
76 client: &mut ZoomClient,
77 out: &OutputConfig,
78 id_or_email: &str,
79) -> Result<(), ApiError> {
80 let user = client.get_user(id_or_email).await?;
81
82 if out.json {
83 out.print_data(&serde_json::to_string_pretty(&user).expect("serialize"));
84 } else {
85 let name = user
86 .display_name
87 .clone()
88 .or_else(|| match (&user.first_name, &user.last_name) {
89 (Some(f), Some(l)) => Some(format!("{f} {l}")),
90 (Some(f), None) => Some(f.clone()),
91 _ => None,
92 })
93 .unwrap_or_default();
94 out.print_data(&output::kv_block(&[
95 ("id", user.id.clone()),
96 ("email", user.email.clone()),
97 ("name", name),
98 ("status", user.status.clone().unwrap_or_default()),
99 ]));
100 }
101 Ok(())
102}
103
104pub async fn me(client: &mut ZoomClient, out: &OutputConfig) -> Result<(), ApiError> {
105 get(client, out, "me").await
106}
107
108pub async fn create(
109 client: &mut ZoomClient,
110 out: &OutputConfig,
111 email: String,
112 first_name: Option<String>,
113 last_name: Option<String>,
114 user_type: u8,
115) -> Result<(), ApiError> {
116 let req = CreateUserRequest {
117 action: "create".into(),
118 user_info: CreateUserInfo {
119 email,
120 user_type,
121 first_name,
122 last_name,
123 },
124 };
125 let user = client.create_user(req).await?;
126 if out.json {
127 out.print_data(&serde_json::to_string_pretty(&user).expect("serialize"));
128 } else {
129 out.print_message(&format!("Created user: {} ({})", user.email, user.id));
130 }
131 Ok(())
132}
133
134pub async fn deactivate(
135 client: &mut ZoomClient,
136 out: &OutputConfig,
137 id_or_email: &str,
138) -> Result<(), ApiError> {
139 client.set_user_status(id_or_email, "deactivate").await?;
140 out.print_message(&format!("User {} deactivated.", id_or_email));
141 Ok(())
142}
143
144pub async fn activate(
145 client: &mut ZoomClient,
146 out: &OutputConfig,
147 id_or_email: &str,
148) -> Result<(), ApiError> {
149 client.set_user_status(id_or_email, "activate").await?;
150 out.print_message(&format!("User {} activated.", id_or_email));
151 Ok(())
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::api::ZoomClient;
158 use wiremock::matchers::{method, path};
159 use wiremock::{Mock, MockServer, ResponseTemplate};
160
161 fn test_out_json() -> OutputConfig {
162 OutputConfig::for_test()
163 }
164
165 #[tokio::test]
166 async fn users_list_json_includes_total_records() {
167 let server = MockServer::start().await;
168 Mock::given(method("GET"))
169 .and(path("/v2/users"))
170 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
171 "users": [
172 {"id": "u1", "email": "a@test.com", "display_name": "Alice", "status": "active"}
173 ],
174 "total_records": 1
175 })))
176 .mount(&server)
177 .await;
178
179 let mut client =
180 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
181 list(&mut client, &test_out_json(), None, None, None, None)
182 .await
183 .unwrap();
184 }
185
186 #[tokio::test]
187 async fn users_get_returns_not_found_for_unknown_user() {
188 let server = MockServer::start().await;
189 Mock::given(method("GET"))
190 .and(path("/v2/users/nobody@example.com"))
191 .respond_with(ResponseTemplate::new(404).set_body_string("User not found"))
192 .mount(&server)
193 .await;
194
195 let mut client =
196 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
197 let err = get(&mut client, &test_out_json(), "nobody@example.com")
198 .await
199 .unwrap_err();
200 assert!(matches!(err, ApiError::NotFound(_)));
201 }
202
203 #[tokio::test]
204 async fn users_me_delegates_to_get_with_me() {
205 let server = MockServer::start().await;
206 Mock::given(method("GET"))
207 .and(path("/v2/users/me"))
208 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
209 "id": "self-user", "email": "me@example.com", "display_name": "Me"
210 })))
211 .mount(&server)
212 .await;
213
214 let mut client =
215 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
216 me(&mut client, &test_out_json()).await.unwrap();
217 }
218
219 #[tokio::test]
220 async fn users_create_returns_created_user() {
221 let server = MockServer::start().await;
222 Mock::given(method("POST"))
223 .and(path("/v2/users"))
224 .and(wiremock::matchers::body_json(serde_json::json!({
225 "action": "create",
226 "user_info": {
227 "email": "jane@example.com",
228 "type": 1,
229 "first_name": "Jane",
230 "last_name": "Doe"
231 }
232 })))
233 .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
234 "id": "new-user-id",
235 "email": "jane@example.com",
236 "first_name": "Jane",
237 "last_name": "Doe",
238 "status": "active"
239 })))
240 .mount(&server)
241 .await;
242
243 let mut client =
244 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
245 create(
246 &mut client,
247 &test_out_json(),
248 "jane@example.com".into(),
249 Some("Jane".into()),
250 Some("Doe".into()),
251 1,
252 )
253 .await
254 .unwrap();
255 }
256
257 #[tokio::test]
258 async fn users_deactivate_sends_correct_action() {
259 let server = MockServer::start().await;
260 Mock::given(method("PUT"))
261 .and(path("/v2/users/u1/status"))
262 .and(wiremock::matchers::body_json(
263 serde_json::json!({"action": "deactivate"}),
264 ))
265 .respond_with(ResponseTemplate::new(204))
266 .mount(&server)
267 .await;
268
269 let mut client =
270 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
271 deactivate(&mut client, &test_out_json(), "u1")
272 .await
273 .unwrap();
274 }
275
276 #[tokio::test]
277 async fn users_activate_sends_correct_action() {
278 let server = MockServer::start().await;
279 Mock::given(method("PUT"))
280 .and(path("/v2/users/u1/status"))
281 .and(wiremock::matchers::body_json(
282 serde_json::json!({"action": "activate"}),
283 ))
284 .respond_with(ResponseTemplate::new(204))
285 .mount(&server)
286 .await;
287
288 let mut client =
289 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
290 activate(&mut client, &test_out_json(), "u1").await.unwrap();
291 }
292}