Skip to main content

zoom_cli/commands/
users.rs

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}